001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall;
018
019import static java.util.Collections.*;
020import static org.apache.juneau.commons.function.Suppliers.*;
021import static org.apache.juneau.commons.utils.CollectionUtils.*;
022import static org.apache.juneau.commons.utils.Shorts.*;
023import static org.apache.juneau.commons.utils.StringUtils.*;
024
025import java.util.*;
026import java.util.function.*;
027
028import org.apache.juneau.commons.collections.*;
029import org.apache.juneau.commons.conversion.*;
030import org.apache.juneau.commons.function.*;
031import org.apache.juneau.commons.reflect.*;
032
033/**
034 * A one-time-use non-thread-safe object that's meant to be used once and then thrown away.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not typically thread safe.
038 * </ul>
039 *
040 */
041@SuppressWarnings({
042   "java:S1192" // Duplicated literals (argument/property names) read more clearly inline than as constants
043})
044public abstract class ContextSession {
045
046   /**
047    * Builder class.
048    */
049   @SuppressWarnings({
050      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
051   })
052   public abstract static class Builder<SELF extends Builder<SELF>> {
053
054      /**
055       * Returns this object cast to the self (CRTP) type.
056       *
057       * @return This object.
058       */
059      @SuppressWarnings({
060         "unchecked" // CRTP self-type cast is safe by construction.
061      })
062      protected final SELF self() {
063         return (SELF)this;
064      }
065
066      private Boolean debug;
067      private boolean unmodifiable;
068      private final Context ctx;
069      private final Memoizer<LinkedHashMap<String,Object>> properties;
070
071      /**
072       * Convenience method for converting values using {@link BasicConverter}.
073       *
074       * @param <T> The target type.
075       * @param value The value to convert.
076       *    <br>Can be <jk>null</jk> (returns <jk>null</jk>).
077       * @param type The target class.
078       * @return The converted value, or <jk>null</jk> if the input value was <jk>null</jk>.
079       */
080      protected static <T> T cvt(Object value, Class<T> type) {
081         return BasicConverter.INSTANCE.to(value, null, null, type);
082      }
083
084      /**
085       * Constructor.
086       *
087       * @param ctx The context creating this session.
088       *    <br>Cannot be <jk>null</jk>.
089       */
090      protected Builder(Context ctx) {
091         this.ctx = reqnn("ctx", ctx);
092         this.properties = memoizer(LinkedHashMap::new);
093      }
094
095      /**
096       * Applies a consumer to this builder if it's the specified type.
097       *
098       * @param <T> The expected type.
099       * @param type The expected type.
100       *    <br>Cannot be <jk>null</jk>.
101       * @param apply   The consumer to apply.
102       *    <br>Cannot be <jk>null</jk>.
103       * @return This object.
104       */
105      public <T> SELF apply(Class<T> type, Consumer<T> apply) {
106         if (reqnn("type", type).isInstance(this))
107            reqnn("apply", apply).accept(type.cast(this));
108         return self();
109      }
110
111      /**
112       * Build the object.
113       *
114       * @return The built object.
115       */
116      public abstract ContextSession build();
117
118      /**
119       * Debug mode.
120       *
121       * <p>
122       * Enables the following additional information during parsing:
123       * <ul>
124       *    <li> When bean setters throws exceptions, the exception includes the object stack information in order to determine how that method was invoked.
125       * </ul>
126       *
127       * <p>
128       * If not specified, defaults to {@link Context.Builder#debug()}.
129       *
130       * <h5 class='section'>See Also:</h5><ul>
131       *    <li class='ja'>{@link MarshalledConfig#debug()}
132       *    <li class='jm'>{@link Context.Builder#debug()}
133       *
134       * @param value
135       *    The new value for this property.
136       *    <br>If <jk>null</jk>, defaults to {@link Context#isDebug()}.
137       * @return This object.
138       */
139      public SELF debug(Boolean value) {
140         debug = value;
141         return self();
142      }
143
144      /**
145       * Session properties.
146       *
147       * <p>
148       * Session properties are generic key-value pairs that can be passed through the session and made
149       * available to any customized serializers/parsers or swaps.
150       *
151       * @param value
152       *    The new value for this property.
153       *    <br>Cannot be <jk>null</jk>.
154       * @return This object.
155       */
156      public SELF properties(Map<String,Object> value) {
157         reqnn("value", value);
158         properties.reset();
159         value.forEach(this::property);
160         return self();
161      }
162
163      /**
164       * Adds a property to this session.
165       *
166       * <p>
167       * If the key matches a known session property (e.g. <js>"debug"</js> or <js>"ContextSession.debug"</js>),
168       * the value is converted via {@link BasicConverter} and routed to the corresponding typed setter.
169       * Unknown keys are stored in the raw session properties map.
170       *
171       * @param key The property key.
172       *    <br>Cannot be <jk>null</jk>.
173       * @param value The property value.
174       *    <br>Can be <jk>null</jk> (resets typed properties to their default; removes raw properties).
175       * @return This object.
176       */
177      public SELF property(String key, Object value) {
178         reqnn("key", key);
179         switch (key) {
180            case "debug", "ContextSession.debug":
181               return debug(cvt(value, Boolean.class));
182            default:
183               var map = properties.get();
184               if (value == null) {
185                  map.remove(key);
186               } else {
187                  map.put(key, value);
188               }
189               return self();
190         }
191      }
192
193      /**
194       * Create an unmodifiable session.
195       *
196       * <p>
197       * The created ContextSession object will be unmodifiable which makes it suitable for caching and reuse.
198       *
199       * @return This object.
200       */
201      public SELF unmodifiable() {
202         unmodifiable = true;
203         return self();
204      }
205   }
206
207   private final boolean debug;
208   private final boolean unmodifiable;
209   private final Context ctx;
210   private final Map<String,Object> properties;
211   private List<String> warnings;   // Any warnings encountered.
212
213   /**
214    * Default constructor.
215    *
216    * @param builder The builder for this object.
217    *    <br>Cannot be <jk>null</jk>.
218    */
219   protected ContextSession(Builder<?> builder) {
220      reqnn("builder", builder);
221      ctx = builder.ctx;
222      debug = o(builder.debug).orElse(ctx.isDebug());
223      unmodifiable = builder.unmodifiable;
224      var sp = builder.properties.get();
225      if (unmodifiable) {
226         properties = sp.isEmpty() ? Collections.emptyMap() : u(sp);
227      } else {
228         properties = sp;
229      }
230   }
231
232   /**
233    * Logs a warning message.
234    *
235    * @param msg The warning message.
236    *    <br>Cannot be <jk>null</jk>.
237    * @param args Optional {@link String#format(String, Object...) String.format}-style arguments.
238    *    <br>Cannot be <jk>null</jk> (individual values may be <jk>null</jk>).
239    */
240   public void addWarning(String msg, Object...args) {
241      reqnn("msg", msg, "args", args);
242      if (unmodifiable)
243         return;
244      if (warnings == null)
245         warnings = ll();
246      warnings.add((warnings.size() + 1) + ": " + f(msg, args));
247   }
248
249   /**
250    * Throws a {@link BeanRuntimeException} if any warnings occurred in this session and debug is enabled.
251    */
252   public void checkForWarnings() {
253      if (debug && ! getWarnings().isEmpty())
254         throw brex("Warnings occurred in session: \n" + join(getWarnings(), "\n"));
255   }
256
257   /**
258    * Returns the context that created this session.
259    *
260    * @return The context that created this session.
261    */
262   public Context getContext() { return ctx; }
263
264   /**
265    * Returns the session properties on this session.
266    *
267    * @return The session properties on this session.  Never <jk>null</jk>.
268    */
269   public final Map<String,Object> getSessionProperties() { return properties; }
270
271   /**
272    * Returns the warnings that occurred in this session.
273    *
274    * @return The warnings that occurred in this session, or an empty list if no warnings occurred.
275    */
276   public final List<String> getWarnings() { return warnings == null ? emptyList() : warnings; }
277
278   /**
279    * Debug mode enabled.
280    *
281    * @see Context.Builder#debug()
282    * @return
283    *    <jk>true</jk> if debug mode is enabled.
284    */
285   public boolean isDebug() { return debug; }
286
287   @Override /* Overridden from Object */
288   public String toString() {
289      return r(properties());
290   }
291
292   /**
293    * Returns the properties on this bean as a map for debugging.
294    *
295    * @return The properties on this bean as a map for debugging.
296    */
297   protected FluentMap<String,Object> properties() {
298      return filteredBeanPropertyMap()
299         .a("debug", debug);
300   }
301}