001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau;
014
015import static java.util.Collections.*;
016
017import java.lang.annotation.*;
018import java.lang.reflect.*;
019import java.util.*;
020import java.util.concurrent.*;
021import java.util.regex.*;
022
023import org.apache.juneau.PropertyStore.*;
024import org.apache.juneau.annotation.*;
025import org.apache.juneau.csv.annotation.*;
026import org.apache.juneau.html.annotation.*;
027import org.apache.juneau.internal.*;
028import org.apache.juneau.jso.annotation.*;
029import org.apache.juneau.json.*;
030import org.apache.juneau.json.annotation.*;
031import org.apache.juneau.jsonschema.annotation.*;
032import org.apache.juneau.marshall.*;
033import org.apache.juneau.msgpack.annotation.*;
034import org.apache.juneau.oapi.annotation.*;
035import org.apache.juneau.parser.annotation.*;
036import org.apache.juneau.plaintext.annotation.*;
037import org.apache.juneau.reflect.*;
038import org.apache.juneau.serializer.annotation.*;
039import org.apache.juneau.soap.annotation.*;
040import org.apache.juneau.svl.*;
041import org.apache.juneau.uon.annotation.*;
042import org.apache.juneau.urlencoding.annotation.*;
043import org.apache.juneau.xml.annotation.*;
044
045/**
046 * A builder for {@link PropertyStore} objects.
047 */
048public class PropertyStoreBuilder {
049
050   // Contains a cache of all created PropertyStore objects keyed by hashcode.
051   // Used to minimize memory consumption by reusing identical PropertyStores.
052   private static final Map<Integer,PropertyStore> CACHE = new ConcurrentHashMap<>();
053
054   // Maps property suffixes (e.g. "lc") to PropertyType (e.g. LIST_CLASS)
055   static final Map<String,PropertyType> SUFFIX_MAP = new ConcurrentHashMap<>();
056   static {
057      for (PropertyType pt : PropertyType.values())
058         SUFFIX_MAP.put(pt.getSuffix(), pt);
059   }
060
061   private final Map<String,PropertyGroupBuilder> groups = new ConcurrentSkipListMap<>();
062
063   // Previously-created property store.
064   private volatile PropertyStore propertyStore;
065
066   // Called by PropertyStore.builder()
067   PropertyStoreBuilder(PropertyStore ps) {
068      apply(ps);
069   }
070
071   // Called by PropertyStore.create()
072   PropertyStoreBuilder() {}
073
074   /**
075    * Creates a new {@link PropertyStore} based on the values in this builder.
076    *
077    * @return A new {@link PropertyStore} based on the values in this builder.
078    */
079   public synchronized PropertyStore build() {
080
081      // Reused the last one if we haven't change this builder.
082      if (propertyStore == null)
083         propertyStore = new PropertyStore(groups);
084
085      PropertyStore ps = CACHE.get(propertyStore.hashCode());
086      if (ps == null)
087         CACHE.put(propertyStore.hashCode(), propertyStore);
088      else if (! ps.equals(propertyStore))
089         throw new RuntimeException("Property store mismatch!  This shouldn't happen.  hashCode=["+propertyStore.hashCode()+"]\n---PS#1---\n" + ps + "\n---PS#2---\n" + propertyStore);
090      else
091         propertyStore = ps;
092
093      return propertyStore;
094   }
095
096   /**
097    * Copies all the values in the specified property store into this builder.
098    *
099    * @param copyFrom The property store to copy the values from.
100    * @return This object (for method chaining).
101    */
102   public synchronized PropertyStoreBuilder apply(PropertyStore copyFrom) {
103      propertyStore = null;
104
105      if (copyFrom != null)
106         for (Map.Entry<String,PropertyGroup> e : copyFrom.groups.entrySet()) {
107            String gName = e.getKey();
108            PropertyGroupBuilder g1 = this.groups.get(gName);
109            PropertyGroup g2 = e.getValue();
110            if (g1 == null)
111               this.groups.put(gName, g2.builder());
112            else
113               g1.apply(g2);
114         }
115      return this;
116   }
117
118   /**
119    * Applies the settings in the specified annotations to this property store.
120    *
121    * @param al The list of annotations to apply.
122    * @param r The string resolver used to resolve any variables in the annotations.
123    * @return This object (for method chaining).
124    */
125   @SuppressWarnings("unchecked")
126   public PropertyStoreBuilder applyAnnotations(AnnotationList al, VarResolverSession r) {
127      for (AnnotationInfo<?> ai : al.sort()) {
128         try {
129            ai.getConfigApply(r).apply((AnnotationInfo<Annotation>)ai, this);
130         } catch (ConfigException ex) {
131            throw ex;
132         } catch (Exception ex) {
133            throw new ConfigException(ex, "Could not instantiate ConfigApply class {0}", ai);
134         }
135      }
136      return this;
137   }
138
139   /**
140    * Applies any of the various <ja>@XConfig</ja> annotations on the specified class to this property store.
141    *
142    * <p>
143    * Applies any of the following annotations:
144    * <ul class='javatree'>
145    *    <li class ='ja'>{@link BeanConfig}
146    *    <li class ='ja'>{@link CsvConfig}
147    *    <li class ='ja'>{@link HtmlConfig}
148    *    <li class ='ja'>{@link HtmlDocConfig}
149    *    <li class ='ja'>{@link JsoConfig}
150    *    <li class ='ja'>{@link JsonConfig}
151    *    <li class ='ja'>{@link JsonSchemaConfig}
152    *    <li class ='ja'>{@link MsgPackConfig}
153    *    <li class ='ja'>{@link OpenApiConfig}
154    *    <li class ='ja'>{@link ParserConfig}
155    *    <li class ='ja'>{@link PlainTextConfig}
156    *    <li class ='ja'>{@link SerializerConfig}
157    *    <li class ='ja'>{@link SoapXmlConfig}
158    *    <li class ='ja'>{@link UonConfig}
159    *    <li class ='ja'>{@link UrlEncodingConfig}
160    *    <li class ='ja'>{@link XmlConfig}
161    *    <li class ='ja'><c>RdfConfig</c>
162    * </ul>
163    *
164    * <p>
165    * Annotations are appended in the following order:
166    * <ol>
167    *    <li>On the package of this class.
168    *    <li>On interfaces ordered parent-to-child.
169    *    <li>On parent classes ordered parent-to-child.
170    *    <li>On this class.
171    * </ol>
172    *
173    * @param fromClass The class on which the annotations are defined.
174    * @return This object (for method chaining).
175    */
176   public PropertyStoreBuilder applyAnnotations(Class<?> fromClass) {
177      applyAnnotations(ClassInfo.of(fromClass).getAnnotationListParentFirst(ConfigAnnotationFilter.INSTANCE), VarResolver.DEFAULT.createSession());
178      return this;
179   }
180
181   /**
182    * Applies any of the various <ja>@XConfig</ja> annotations on the specified method to this property store.
183    *
184    * <p>
185    * Applies any of the following annotations:
186    * <ul class='javatree'>
187    *    <li class ='ja'>{@link BeanConfig}
188    *    <li class ='ja'>{@link CsvConfig}
189    *    <li class ='ja'>{@link HtmlConfig}
190    *    <li class ='ja'>{@link HtmlDocConfig}
191    *    <li class ='ja'>{@link JsoConfig}
192    *    <li class ='ja'>{@link JsonConfig}
193    *    <li class ='ja'>{@link JsonSchemaConfig}
194    *    <li class ='ja'>{@link MsgPackConfig}
195    *    <li class ='ja'>{@link OpenApiConfig}
196    *    <li class ='ja'>{@link ParserConfig}
197    *    <li class ='ja'>{@link PlainTextConfig}
198    *    <li class ='ja'>{@link SerializerConfig}
199    *    <li class ='ja'>{@link SoapXmlConfig}
200    *    <li class ='ja'>{@link UonConfig}
201    *    <li class ='ja'>{@link UrlEncodingConfig}
202    *    <li class ='ja'>{@link XmlConfig}
203    *    <li class ='ja'><c>RdfConfig</c>
204    * </ul>
205    *
206    * <p>
207    * Annotations are appended in the following orders:
208    * <ol>
209    *    <li>On the package of the method class.
210    *    <li>On interfaces ordered parent-to-child.
211    *    <li>On parent classes ordered parent-to-child.
212    *    <li>On the method class.
213    *    <li>On this method and matching methods ordered parent-to-child.
214    * </ol>
215    *
216    * @param fromMethod The method on which the annotations are defined.
217    * @return This object (for method chaining).
218    */
219   public PropertyStoreBuilder applyAnnotations(Method fromMethod) {
220      applyAnnotations(MethodInfo.of(fromMethod).getAnnotationListParentFirst(ConfigAnnotationFilter.INSTANCE), VarResolver.DEFAULT.createSession());
221      return this;
222   }
223
224   /**
225    * Sets a configuration property value on this object.
226    *
227    * @param key
228    *    The configuration property key.
229    *    <br>(e.g <js>"BeanContext.foo.ss/add.1"</js>)
230    *    <br>If name ends with <l>/add</l>, then the specified value is added to the existing property value as an entry
231    *    in a SET or LIST property.
232    *    <br>If name ends with <l>/add.{key}</l>, then the specified value is added to the existing property value as a
233    *    key/value pair in a MAP property.
234    *    <br>If name ends with <l>/remove</l>, then the specified value is removed from the existing property property
235    *    value in a SET or LIST property.
236    * @param value
237    *    The new value.
238    *    If <jk>null</jk>, the property value is deleted.
239    *    In general, the value type can be anything.
240    * @return This object (for method chaining).
241    */
242   public synchronized PropertyStoreBuilder set(String key, Object value) {
243      propertyStore = null;
244
245      String g = group(key);
246
247      int i = key.indexOf('/');
248      if (i != -1) {
249         String command = key.substring(i+1), arg = null;
250         String key2 = key.substring(0, i);
251         int j = command.indexOf('.');
252         if (j != -1) {
253            arg = command.substring(j+1);
254            command = command.substring(0, j);
255         }
256
257         if ("add".equals(command)) {
258            return addTo(key2, arg, value);
259         } else if ("remove".equals(command)) {
260            if (arg != null)
261               throw new ConfigException("Invalid key specified: ''{0}''", key);
262            return removeFrom(key2, value);
263         } else {
264            throw new ConfigException("Invalid key specified: ''{0}''", key);
265         }
266      }
267
268      String n = g.isEmpty() ? key : key.substring(g.length()+1);
269
270      PropertyGroupBuilder gb = groups.get(g);
271      if (gb == null) {
272         gb = new PropertyGroupBuilder();
273         groups.put(g, gb);
274      }
275
276      gb.set(n, value);
277
278      if (gb.isEmpty())
279         groups.remove(g);
280
281      return this;
282   }
283
284   /**
285    * Removes the property with the specified key.
286    *
287    * <p>
288    * This is equivalent to calling <code>set(key, <jk>null</jk>);</code>
289    *
290    * @param key The property key.
291    * @return This object (for method chaining).
292    */
293   public synchronized PropertyStoreBuilder remove(String key) {
294      propertyStore = null;
295      return set(key, null);
296   }
297
298   /**
299    * Convenience method for setting multiple properties in one call.
300    *
301    * <p>
302    * This replaces any previous configuration properties set on this store.
303    *
304    * @param newProperties The new properties to set.
305    * @return This object (for method chaining).
306    */
307   public synchronized PropertyStoreBuilder set(Map<String,Object> newProperties) {
308      propertyStore = null;
309      clear();
310      add(newProperties);
311      return this;
312   }
313
314   /**
315    * Convenience method for setting multiple properties in one call.
316    *
317    * <p>
318    * This appends to any previous configuration properties set on this store.
319    *
320    * @param newProperties The new properties to set.
321    * @return This object (for method chaining).
322    */
323   public synchronized PropertyStoreBuilder add(Map<String,Object> newProperties) {
324      propertyStore = null;
325
326      if (newProperties != null)
327         for (Map.Entry<String,Object> e : newProperties.entrySet())
328            set(e.getKey(), e.getValue());
329
330      return this;
331   }
332
333   /**
334    * Adds one or more values to a SET, LIST, or MAP property.
335    *
336    * @param key The property key.
337    * @param arg
338    *    The argument.
339    *    <br>For SETs, this must always be <jk>null</jk>.
340    *    <br>For LISTs, this can be <jk>null</jk> or a numeric index.
341    *       Out-of-range indexes are simply 'adjusted' to the beginning or the end of the list.
342    *       So, for example, a value of <js>"-100"</js> will always just cause the entry to be added to the beginning
343    *       of the list.
344    *       <br>NOTE:  If <jk>null</jk>, value will be inserted at position 0.
345    *    <br>For MAPs, this can be <jk>null</jk> if we're adding a map, or a string key if we're adding an entry.
346    * @param value
347    *    The new value to add to the property.
348    *    <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
349    *    <br>For MAPs, this can be a single value, Map, or JSON object string.
350    *    <br><jk>null</jk> values have the effect of removing entries.
351    * @return This object (for method chaining).
352    * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
353    */
354   public synchronized PropertyStoreBuilder addTo(String key, String arg, Object value) {
355      propertyStore = null;
356      String g = group(key);
357      String n = g.isEmpty() ? key : key.substring(g.length()+1);
358
359      PropertyGroupBuilder gb = groups.get(g);
360      if (gb == null) {
361         gb = new PropertyGroupBuilder();
362         groups.put(g, gb);
363      }
364
365      gb.addTo(n, arg, value);
366
367      if (gb.isEmpty())
368         groups.remove(g);
369
370      return this;
371   }
372
373   /**
374    * Adds/prepends a value to a SET, LIST, or MAP property.
375    *
376    * <p>
377    * Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>.
378    *
379    * <p>
380    * NOTE:  When adding to a list, the value is inserted at the beginning of the list.
381    *
382    * @param key The property key.
383    * @param value
384    *    The new value to add to the property.
385    *    <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
386    *    <br>for MAPs, this can be a single value, Map, or JSON object string.
387    *    <br><jk>null</jk> values have the effect of removing entries.
388    * @return This object (for method chaining).
389    * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
390    */
391   public synchronized PropertyStoreBuilder addTo(String key, Object value) {
392      propertyStore = null;
393      return addTo(key, null, value);
394   }
395
396   /**
397    * Removes a value from a SET or LIST property.
398    *
399    * @param key The property key.
400    * @param value The property value in the property.
401    * @return This object (for method chaining).
402    * @throws ConfigException If property is not a SET or LIST property.
403    */
404   public synchronized PropertyStoreBuilder removeFrom(String key, Object value) {
405      propertyStore = null;
406      String g = group(key);
407      String n = g.isEmpty() ? key : key.substring(g.length()+1);
408
409      PropertyGroupBuilder gb = groups.get(g);
410
411      // Create property group anyway to generate a good error message.
412      if (gb == null)
413         gb = new PropertyGroupBuilder();
414
415      gb.removeFrom(n, value);
416
417      if (gb.isEmpty())
418         groups.remove(g);
419
420      return this;
421   }
422
423   /**
424    * Peeks at a property value.
425    *
426    * <p>
427    * Used for debugging purposes.
428    *
429    * @param key The property key.
430    * @return The property value, or <jk>null</jk> if it doesn't exist.
431    */
432   public Object peek(String key) {
433      String g = group(key);
434      String n = g.isEmpty() ? key : key.substring(g.length()+1);
435
436      PropertyGroupBuilder gb = groups.get(g);
437
438      // Create property group anyway to generate a good error message.
439      if (gb == null)
440         return null;
441
442      MutableProperty bp = gb.properties.get(n);
443      if (bp == null)
444         return null;
445
446      return bp.peek();
447   }
448
449   /**
450    * Same as {@link #peek(String)} but converts the value to the specified type.
451    *
452    * @param <T> The type to convert to.
453    * @param c The type to convert to.
454    * @param key The property key.
455    * @return The property value, or <jk>null</jk> if it doesn't exist.
456    */
457   public <T> T peek(Class<T> c, String key)  {
458      Object o = peek(key);
459      if (o == null)
460         return null;
461      return BeanContext.DEFAULT.createBeanSession().convertToType(o, c);
462   }
463
464   /**
465    * Clears all entries in this property store.
466    */
467   public synchronized void clear() {
468      propertyStore = null;
469      this.groups.clear();
470   }
471
472   //-------------------------------------------------------------------------------------------------------------------
473   // PropertyGroupBuilder
474   //-------------------------------------------------------------------------------------------------------------------
475
476   static class PropertyGroupBuilder {
477      final Map<String,MutableProperty> properties = new ConcurrentSkipListMap<>();
478
479      PropertyGroupBuilder() {
480         this(emptyMap());
481      }
482
483      synchronized void apply(PropertyGroup copyFrom) {
484         for (Map.Entry<String,Property> e : copyFrom.properties.entrySet()) {
485            String pName = e.getKey();
486            MutableProperty p1 = properties.get(pName);
487            Property p2 = e.getValue();
488            if (p1 == null)
489               properties.put(pName, p2.mutable());
490            else
491               p1.apply(p2.value);
492         }
493      }
494
495      PropertyGroupBuilder(Map<String,Property> properties) {
496         for (Map.Entry<String,Property> p : properties.entrySet())
497            this.properties.put(p.getKey(), p.getValue().mutable());
498      }
499
500      synchronized void set(String key, Object value) {
501         MutableProperty p = properties.get(key);
502         if (p == null) {
503            p = MutableProperty.create(key, value);
504            if (! p.isEmpty())
505               properties.put(key, p);
506         } else {
507            p.set(value);
508            if (p.isEmpty())
509               properties.remove(key);
510            else
511               properties.put(key, p);
512         }
513      }
514
515      synchronized void addTo(String key, String arg, Object value) {
516         MutableProperty p = properties.get(key);
517         if (p == null) {
518            p = MutableProperty.create(key, null);
519            p.add(arg, value);
520            if (! p.isEmpty())
521               properties.put(key, p);
522         } else {
523            p.add(arg, value);
524            if (p.isEmpty())
525               properties.remove(key);
526            else
527               properties.put(key, p);
528         }
529      }
530
531      synchronized void removeFrom(String key, Object value) {
532         MutableProperty p = properties.get(key);
533         if (p == null) {
534            // Create property anyway to generate a good error message.
535            p = MutableProperty.create(key, null);
536            p.remove(value);
537         } else {
538            p.remove(value);
539            if (p.isEmpty())
540               properties.remove(key);
541            else
542               properties.put(key, p);
543         }
544      }
545
546      synchronized boolean isEmpty() {
547         return properties.isEmpty();
548      }
549
550      synchronized PropertyGroup build() {
551         return new PropertyGroup(properties);
552      }
553
554      /**
555       * Used by the <c>toString()</c> method for debugging.
556       *
557       * @param bs The bean session.
558       * @return This object converted to a map.
559       */
560      public Map<String,MutableProperty> swap(BeanSession bs) {
561         return properties;
562      }
563   }
564
565   //-------------------------------------------------------------------------------------------------------------------
566   // MutableProperty
567   //-------------------------------------------------------------------------------------------------------------------
568
569   static abstract class MutableProperty {
570      final String name;
571      final PropertyType type;
572
573      MutableProperty(String name, PropertyType type) {
574         this.name = name;
575         this.type = type;
576      }
577
578      static MutableProperty create(String name, Object value) {
579         int i = name.lastIndexOf('.');
580         String type = i == -1 ? "s" : name.substring(i+1);
581         PropertyType pt = SUFFIX_MAP.get(type);
582
583         if (pt == null)
584            throw new ConfigException("Invalid type specified on property ''{0}''", name);
585
586         switch (pt) {
587            case STRING:
588            case BOOLEAN:
589            case INTEGER:
590            case CLASS:
591            case OBJECT:  return new MutableSimpleProperty(name, pt, value);
592            case SET_STRING:
593            case SET_INTEGER:
594            case SET_CLASS: return new MutableSetProperty(name, pt, value);
595            case LIST_STRING:
596            case LIST_INTEGER:
597            case LIST_CLASS:
598            case LIST_OBJECT: return new MutableListProperty(name, pt, value);
599            case SORTED_MAP_STRING:
600            case SORTED_MAP_INTEGER:
601            case SORTED_MAP_CLASS:
602            case SORTED_MAP_OBJECT: return new MutableMapProperty(name, pt, value);
603            case ORDERED_MAP_STRING:
604            case ORDERED_MAP_INTEGER:
605            case ORDERED_MAP_CLASS:
606            case ORDERED_MAP_OBJECT: return new MutableLinkedMapProperty(name, pt, value);
607            default: return new MutableSimpleProperty(name, PropertyType.STRING, value);
608         }
609      }
610
611      abstract Property build();
612
613      abstract boolean isEmpty();
614
615      abstract void set(Object value);
616
617      abstract void apply(Object value);
618
619      abstract Object peek();
620
621      void add(String arg, Object value) {
622         throw new ConfigException("Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(value), className(value), name, type);
623      }
624
625      void remove(Object value) {
626         throw new ConfigException("Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(value), className(value), name, type);
627      }
628
629      Object convert(Object value) {
630         return value == null ? null : type.converter.convert(value);
631      }
632
633      @Override /* Object */
634      public String toString() {
635         return StringUtils.stringify(peek());
636      }
637   }
638
639   //-------------------------------------------------------------------------------------------------------------------
640   // MutableSimpleProperty
641   //-------------------------------------------------------------------------------------------------------------------
642
643   static class MutableSimpleProperty extends MutableProperty {
644      private Object value;
645
646      MutableSimpleProperty(String name, PropertyType type, Object value) {
647         super(name, type);
648         set(value);
649      }
650
651      @Override /* MutableProperty */
652      synchronized Property build() {
653         return new Property(name, value, type);
654      }
655
656      @Override /* MutableProperty */
657      synchronized void set(Object value) {
658         this.value = convert(value);
659      }
660
661      @Override /* MutableProperty */
662      synchronized void apply(Object value) {
663         this.value = convert(value);
664      }
665
666      @Override /* MutableProperty */
667      synchronized boolean isEmpty() {
668         return this.value == null;
669      }
670
671      @Override /* MutableProperty */
672      synchronized Object peek() {
673         return value;
674      }
675   }
676
677   //-------------------------------------------------------------------------------------------------------------------
678   // MutableSetProperty
679   //-------------------------------------------------------------------------------------------------------------------
680
681   static class MutableSetProperty extends MutableProperty {
682      private final Set<Object> set;
683
684      MutableSetProperty(String name, PropertyType type, Object value) {
685         super(name, type);
686         set = new ConcurrentSkipListSet<>(type.comparator());
687         set(value);
688      }
689
690      @Override /* MutableProperty */
691      synchronized Property build() {
692         return new Property(name, unmodifiableSet(new LinkedHashSet<>(set)), type);
693      }
694
695      @Override /* MutableProperty */
696      synchronized void set(Object value) {
697         try {
698            Set<Object> newSet = merge(set, type.converter, value);
699            set.clear();
700            set.addAll(newSet);
701         } catch (Exception e) {
702            throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
703         }
704      }
705
706      @Override /* MutableProperty */
707      synchronized void apply(Object values) {
708         set.addAll((Set<?>)values);
709      }
710
711      @Override /* MutableProperty */
712      synchronized void add(String arg, Object o) {
713         if (arg != null)
714            throw new ConfigException("Cannot use argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);
715         try {
716            set.addAll(normalize(type.converter, o));
717         } catch (Exception e) {
718            throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
719         }
720      }
721
722      @Override /* MutableProperty */
723      synchronized void remove(Object o) {
724         try {
725            set.removeAll(normalize(type.converter, o));
726         } catch (Exception e) {
727            throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
728         }
729      }
730
731      @Override /* MutableProperty */
732      synchronized boolean isEmpty() {
733         return set.isEmpty();
734      }
735
736      @Override /* MutableProperty */
737      synchronized Object peek() {
738         return set;
739      }
740   }
741
742   //-------------------------------------------------------------------------------------------------------------------
743   // MutableListProperty
744   //-------------------------------------------------------------------------------------------------------------------
745
746   static class MutableListProperty extends MutableProperty {
747      private final List<Object> list = synchronizedList(new LinkedList<>());
748
749      MutableListProperty(String name, PropertyType type, Object value) {
750         super(name, type);
751         set(value);
752      }
753
754      @Override /* MutableProperty */
755      synchronized Property build() {
756         return new Property(name, unmodifiableList(new ArrayList<>(list)), type);
757      }
758
759      @Override /* MutableProperty */
760      synchronized void set(Object value) {
761         try {
762            List<Object> newList = merge(list, type.converter, value);
763            list.clear();
764            list.addAll(newList);
765         } catch (Exception e) {
766            throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
767         }
768      }
769
770      @Override /* MutableProperty */
771      synchronized void apply(Object values) {
772         list.addAll((List<?>)values);
773      }
774
775      @Override /* MutableProperty */
776      synchronized void add(String arg, Object o) {
777         if (arg != null && ! StringUtils.isNumeric(arg))
778            throw new ConfigException("Invalid argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);
779
780         int index = arg == null ? 0 : Integer.parseInt(arg);
781         if (index < 0)
782            index = 0;
783         else if (index > list.size())
784            index = list.size();
785
786         try {
787            List<Object> l = normalize(type.converter, o);
788            list.removeAll(l);
789            list.addAll(index, l);
790         } catch (Exception e) {
791            throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
792         }
793      }
794
795      @Override /* MutableProperty */
796      synchronized void remove(Object o) {
797         try {
798            list.removeAll(normalize(type.converter, o));
799         } catch (Exception e) {
800            throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
801         }
802      }
803
804      @Override /* MutableProperty */
805      synchronized boolean isEmpty() {
806         return list.isEmpty();
807      }
808
809      @Override /* MutableProperty */
810      synchronized Object peek() {
811         return list;
812      }
813   }
814
815   //-------------------------------------------------------------------------------------------------------------------
816   // MutableMapProperty
817   //-------------------------------------------------------------------------------------------------------------------
818
819   @SuppressWarnings("rawtypes")
820   static class MutableMapProperty extends MutableProperty {
821      protected Map<String,Object> map;
822
823      MutableMapProperty(String name, PropertyType type, Object value) {
824         super(name, type);
825         this.map = createMap();
826         set(value);
827      }
828
829      protected Map<String,Object> createMap() {
830         return new ConcurrentHashMap<>();
831      }
832
833      @Override /* MutableProperty */
834      synchronized Property build() {
835         return new Property(name, unmodifiableMap(new TreeMap<>(map)), type);
836      }
837
838      @Override /* MutableProperty */
839      synchronized void set(Object value) {
840         this.map.clear();
841         add(null, value);
842      }
843
844      @SuppressWarnings("unchecked")
845      @Override /* MutableProperty */
846      synchronized void apply(Object values) {
847         for (Map.Entry<String,Object> e : ((Map<String,Object>)values).entrySet())
848            add(e.getKey(), e.getValue());
849      }
850
851      @Override /* MutableProperty */
852      synchronized void add(String arg, Object o) {
853         if (arg != null) {
854            o = convert(o);
855            if (o == null)
856               this.map.remove(arg);
857            else
858               this.map.put(arg, o);
859
860         } else if (o != null) {
861            if (o instanceof Map) {
862               Map m = (Map)o;
863               for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
864                  if (e.getKey() != null)
865                     add(e.getKey().toString(), e.getValue());
866            } else if (isObjectMap(o)) {
867               try {
868                  add(null, new ObjectMap(o.toString()));
869               } catch (Exception e) {
870                  throw new ConfigException(e, "Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
871               }
872            } else {
873               throw new ConfigException("Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
874            }
875         }
876      }
877
878      @Override /* MutableProperty */
879      synchronized boolean isEmpty() {
880         return this.map.isEmpty();
881      }
882
883      @Override /* MutableProperty */
884      synchronized Object peek() {
885         return map;
886      }
887   }
888
889   //-------------------------------------------------------------------------------------------------------------------
890   // MutableLinkedMapProperty
891   //-------------------------------------------------------------------------------------------------------------------
892
893   static class MutableLinkedMapProperty extends MutableMapProperty {
894
895      MutableLinkedMapProperty(String name, PropertyType type, Object value) {
896         super(name, type, value);
897         set(value);
898      }
899
900      @Override
901      protected Map<String,Object> createMap() {
902         return synchronizedMap(new LinkedHashMap<String,Object>());
903      }
904
905      @Override /* MutableProperty */
906      synchronized Property build() {
907         return new Property(name, unmodifiableMap(new LinkedHashMap<>(map)), type);
908      }
909   }
910
911
912   //-------------------------------------------------------------------------------------------------------------------
913   // Utility methods
914   //-------------------------------------------------------------------------------------------------------------------
915
916   static Set<Object> merge(Set<Object> oldSet, PropertyConverter<?> pc, Object o) throws Exception {
917      return merge(oldSet, new LinkedHashSet<>(), normalize(pc, o));
918   }
919
920   private static Set<Object> merge(Set<Object> oldSet, Set<Object> newSet, List<Object> l) {
921      for (Object o : l) {
922         if (isNone(o))
923            newSet.clear();
924         else if (isInherit(o))
925            newSet.addAll(oldSet);
926         else
927            newSet.add(o);
928      }
929      return newSet;
930   }
931
932   static List<Object> merge(List<Object> oldList, PropertyConverter<?> pc, Object o) throws Exception {
933      return merge(oldList, new ArrayList<>(), normalize(pc, o));
934   }
935
936   private static List<Object> merge(List<Object> oldList, List<Object> newList, List<Object> l) {
937      for (Object o : l) {
938         if (isIndexed(o)) {
939            Matcher lm = INDEXED_LINK_PATTERN.matcher(o.toString());
940            lm.matches();
941            String key = lm.group(1);
942            int i2 = Math.min(newList.size(), Integer.parseInt(lm.group(2)));
943            String remainder = lm.group(3);
944            newList.add(i2, key.isEmpty() ? remainder : key + ":" + remainder);
945         } else if (isNone(o)) {
946            newList.clear();
947         } else if (isInherit(o)) {
948            if (oldList != null)
949               for (Object o2 : oldList)
950                  newList.add(o2);
951         } else {
952            newList.remove(o);
953            newList.add(o);
954         }
955      }
956
957      return newList;
958   }
959
960   static List<Object> normalize(PropertyConverter<?> pc, Object o) throws Exception {
961      return normalize(new ArrayList<>(), pc, o);
962   }
963
964   @SuppressWarnings("unchecked")
965   static List<Object> normalize(List<Object> l, PropertyConverter<?> pc, Object o) throws Exception {
966      if (o != null) {
967         if (o.getClass().isArray()) {
968            for (int i = 0; i < Array.getLength(o); i++)
969               normalize(l, pc, Array.get(o, i));
970         } else if (o instanceof Collection) {
971            for (Object o2 : (Collection<Object>)o)
972               normalize(l, pc, o2);
973         } else if (isObjectList(o)) {
974            normalize(l, pc, new ObjectList(o.toString()));
975         } else {
976            l.add(pc == null ? o : pc.convert(o));
977         }
978      }
979      return l;
980   }
981
982   static String string(Object value) {
983      return SimpleJsonSerializer.DEFAULT.toString(value);
984   }
985
986   static String className(Object value) {
987      return value.getClass().getSimpleName();
988   }
989
990   static boolean isObjectMap(Object o) {
991      if (o instanceof CharSequence) {
992         String s = o.toString();
993         return (s.startsWith("{") && s.endsWith("}") && BeanContext.DEFAULT != null);
994      }
995      return false;
996   }
997
998   private static String group(String key) {
999      if (key == null || key.indexOf('.') == -1)
1000         return "";
1001      return key.substring(0, key.indexOf('.'));
1002   }
1003
1004   static boolean isObjectList(Object o) {
1005      if (o instanceof CharSequence) {
1006         String s = o.toString();
1007         return (s.startsWith("[") && s.endsWith("]") && BeanContext.DEFAULT != null);
1008      }
1009      return false;
1010   }
1011
1012   private static boolean isNone(Object o) {
1013      if (o instanceof CharSequence) {
1014         String s = o.toString();
1015         return "NONE".equals(s);
1016      }
1017      return false;
1018   }
1019
1020   private static boolean isIndexed(Object o) {
1021      if (o instanceof CharSequence) {
1022         String s = o.toString();
1023         return s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches();
1024      }
1025      return false;
1026   }
1027
1028   private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)");
1029
1030   private static boolean isInherit(Object o) {
1031      if (o instanceof CharSequence) {
1032         String s = o.toString();
1033         return "INHERIT".equals(s);
1034      }
1035      return false;
1036   }
1037
1038   @Override /* Object */
1039   public String toString() {
1040      return SimpleJson.DEFAULT_READABLE.toString(groups);
1041   }
1042}