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) {
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>For MAPs, this can be <jk>null</jk> if we're adding a map, or a string key if we're adding an entry.
345    * @param value
346    *    The new value to add to the property.
347    *    <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
348    *    <br>For MAPs, this can be a single value, Map, or JSON object string.
349    *    <br><jk>null</jk> values have the effect of removing entries.
350    * @return This object (for method chaining).
351    * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
352    */
353   public synchronized PropertyStoreBuilder addTo(String key, String arg, Object value) {
354      propertyStore = null;
355      String g = group(key);
356      String n = g.isEmpty() ? key : key.substring(g.length()+1);
357
358      PropertyGroupBuilder gb = groups.get(g);
359      if (gb == null) {
360         gb = new PropertyGroupBuilder();
361         groups.put(g, gb);
362      }
363
364      gb.addTo(n, arg, value);
365
366      if (gb.isEmpty())
367         groups.remove(g);
368
369      return this;
370   }
371
372   /**
373    * Adds a value to a SET, LIST, or MAP property.
374    *
375    * <p>
376    * Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>.
377    *
378    * @param key The property key.
379    * @param value
380    *    The new value to add to the property.
381    *    <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string.
382    *    <br>for MAPs, this can be a single value, Map, or JSON object string.
383    *    <br><jk>null</jk> values have the effect of removing entries.
384    * @return This object (for method chaining).
385    * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid.
386    */
387   public synchronized PropertyStoreBuilder addTo(String key, Object value) {
388      propertyStore = null;
389      return addTo(key, null, value);
390   }
391
392   /**
393    * Removes a value from a SET or LIST property.
394    *
395    * @param key The property key.
396    * @param value The property value in the property.
397    * @return This object (for method chaining).
398    * @throws ConfigException If property is not a SET or LIST property.
399    */
400   public synchronized PropertyStoreBuilder removeFrom(String key, Object value) {
401      propertyStore = null;
402      String g = group(key);
403      String n = g.isEmpty() ? key : key.substring(g.length()+1);
404
405      PropertyGroupBuilder gb = groups.get(g);
406
407      // Create property group anyway to generate a good error message.
408      if (gb == null)
409         gb = new PropertyGroupBuilder();
410
411      gb.removeFrom(n, value);
412
413      if (gb.isEmpty())
414         groups.remove(g);
415
416      return this;
417   }
418
419   /**
420    * Peeks at a property value.
421    *
422    * <p>
423    * Used for debugging purposes.
424    *
425    * @param key The property key.
426    * @return The property value, or <jk>null</jk> if it doesn't exist.
427    */
428   public Object peek(String key) {
429      String g = group(key);
430      String n = g.isEmpty() ? key : key.substring(g.length()+1);
431
432      PropertyGroupBuilder gb = groups.get(g);
433
434      // Create property group anyway to generate a good error message.
435      if (gb == null)
436         return null;
437
438      MutableProperty bp = gb.properties.get(n);
439      if (bp == null)
440         return null;
441
442      return bp.peek();
443   }
444
445   /**
446    * Same as {@link #peek(String)} but converts the value to the specified type.
447    *
448    * @param <T> The type to convert to.
449    * @param c The type to convert to.
450    * @param key The property key.
451    * @return The property value, or <jk>null</jk> if it doesn't exist.
452    */
453   public <T> T peek(Class<T> c, String key)  {
454      Object o = peek(key);
455      if (o == null)
456         return null;
457      return BeanContext.DEFAULT.createBeanSession().convertToType(o, c);
458   }
459
460   /**
461    * Clears all entries in this property store.
462    */
463   public synchronized void clear() {
464      propertyStore = null;
465      this.groups.clear();
466   }
467
468   //-------------------------------------------------------------------------------------------------------------------
469   // PropertyGroupBuilder
470   //-------------------------------------------------------------------------------------------------------------------
471
472   static class PropertyGroupBuilder {
473      final Map<String,MutableProperty> properties = new ConcurrentSkipListMap<>();
474
475      PropertyGroupBuilder() {
476         this(emptyMap());
477      }
478
479      synchronized void apply(PropertyGroup copyFrom) {
480         for (Map.Entry<String,Property> e : copyFrom.properties.entrySet()) {
481            String pName = e.getKey();
482            MutableProperty p1 = properties.get(pName);
483            Property p2 = e.getValue();
484            if (p1 == null)
485               properties.put(pName, p2.mutable());
486            else
487               p1.apply(p2.value);
488         }
489      }
490
491      PropertyGroupBuilder(Map<String,Property> properties) {
492         for (Map.Entry<String,Property> p : properties.entrySet())
493            this.properties.put(p.getKey(), p.getValue().mutable());
494      }
495
496      synchronized void set(String key, Object value) {
497         MutableProperty p = properties.get(key);
498         if (p == null) {
499            p = MutableProperty.create(key, value);
500            if (! p.isEmpty())
501               properties.put(key, p);
502         } else {
503            p.set(value);
504            if (p.isEmpty())
505               properties.remove(key);
506            else
507               properties.put(key, p);
508         }
509      }
510
511      synchronized void addTo(String key, String arg, Object value) {
512         MutableProperty p = properties.get(key);
513         if (p == null) {
514            p = MutableProperty.create(key, null);
515            p.add(arg, value);
516            if (! p.isEmpty())
517               properties.put(key, p);
518         } else {
519            p.add(arg, value);
520            if (p.isEmpty())
521               properties.remove(key);
522            else
523               properties.put(key, p);
524         }
525      }
526
527      synchronized void removeFrom(String key, Object value) {
528         MutableProperty p = properties.get(key);
529         if (p == null) {
530            // Create property anyway to generate a good error message.
531            p = MutableProperty.create(key, null);
532            p.remove(value);
533         } else {
534            p.remove(value);
535            if (p.isEmpty())
536               properties.remove(key);
537            else
538               properties.put(key, p);
539         }
540      }
541
542      synchronized boolean isEmpty() {
543         return properties.isEmpty();
544      }
545
546      synchronized PropertyGroup build() {
547         return new PropertyGroup(properties);
548      }
549
550      /**
551       * Used by the <c>toString()</c> method for debugging.
552       *
553       * @param bs The bean session.
554       * @return This object converted to a map.
555       */
556      public Map<String,MutableProperty> swap(BeanSession bs) {
557         return properties;
558      }
559   }
560
561   //-------------------------------------------------------------------------------------------------------------------
562   // MutableProperty
563   //-------------------------------------------------------------------------------------------------------------------
564
565   static abstract class MutableProperty {
566      final String name;
567      final PropertyType type;
568
569      MutableProperty(String name, PropertyType type) {
570         this.name = name;
571         this.type = type;
572      }
573
574      static MutableProperty create(String name, Object value) {
575         int i = name.lastIndexOf('.');
576         String type = i == -1 ? "s" : name.substring(i+1);
577         PropertyType pt = SUFFIX_MAP.get(type);
578
579         if (pt == null)
580            throw new ConfigException("Invalid type specified on property ''{0}''", name);
581
582         switch (pt) {
583            case STRING:
584            case BOOLEAN:
585            case INTEGER:
586            case CLASS:
587            case OBJECT:  return new MutableSimpleProperty(name, pt, value);
588            case SET_STRING:
589            case SET_INTEGER:
590            case SET_CLASS: return new MutableSetProperty(name, pt, value);
591            case LIST_STRING:
592            case LIST_INTEGER:
593            case LIST_CLASS:
594            case LIST_OBJECT: return new MutableListProperty(name, pt, value);
595            case SORTED_MAP_STRING:
596            case SORTED_MAP_INTEGER:
597            case SORTED_MAP_CLASS:
598            case SORTED_MAP_OBJECT: return new MutableMapProperty(name, pt, value);
599            case ORDERED_MAP_STRING:
600            case ORDERED_MAP_INTEGER:
601            case ORDERED_MAP_CLASS:
602            case ORDERED_MAP_OBJECT: return new MutableLinkedMapProperty(name, pt, value);
603            default: return new MutableSimpleProperty(name, PropertyType.STRING, value);
604         }
605      }
606
607      abstract Property build();
608
609      abstract boolean isEmpty();
610
611      abstract void set(Object value);
612
613      abstract void apply(Object value);
614
615      abstract Object peek();
616
617      void add(String arg, Object value) {
618         throw new ConfigException("Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(value), className(value), name, type);
619      }
620
621      void remove(Object value) {
622         throw new ConfigException("Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(value), className(value), name, type);
623      }
624
625      Object convert(Object value) {
626         return value == null ? null : type.converter.convert(value);
627      }
628
629      @Override /* Object */
630      public String toString() {
631         return StringUtils.stringify(peek());
632      }
633   }
634
635   //-------------------------------------------------------------------------------------------------------------------
636   // MutableSimpleProperty
637   //-------------------------------------------------------------------------------------------------------------------
638
639   static class MutableSimpleProperty extends MutableProperty {
640      private Object value;
641
642      MutableSimpleProperty(String name, PropertyType type, Object value) {
643         super(name, type);
644         set(value);
645      }
646
647      @Override /* MutableProperty */
648      synchronized Property build() {
649         return new Property(name, value, type);
650      }
651
652      @Override /* MutableProperty */
653      synchronized void set(Object value) {
654         this.value = convert(value);
655      }
656
657      @Override /* MutableProperty */
658      synchronized void apply(Object value) {
659         this.value = convert(value);
660      }
661
662      @Override /* MutableProperty */
663      synchronized boolean isEmpty() {
664         return this.value == null;
665      }
666
667      @Override /* MutableProperty */
668      synchronized Object peek() {
669         return value;
670      }
671   }
672
673   //-------------------------------------------------------------------------------------------------------------------
674   // MutableSetProperty
675   //-------------------------------------------------------------------------------------------------------------------
676
677   static class MutableSetProperty extends MutableProperty {
678      private final Set<Object> set;
679
680      MutableSetProperty(String name, PropertyType type, Object value) {
681         super(name, type);
682         set = new ConcurrentSkipListSet<>(type.comparator());
683         set(value);
684      }
685
686      @Override /* MutableProperty */
687      synchronized Property build() {
688         return new Property(name, unmodifiableSet(new LinkedHashSet<>(set)), type);
689      }
690
691      @Override /* MutableProperty */
692      synchronized void set(Object value) {
693         try {
694            Set<Object> newSet = merge(set, type.converter, value);
695            set.clear();
696            set.addAll(newSet);
697         } catch (Exception e) {
698            throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
699         }
700      }
701
702      @Override /* MutableProperty */
703      synchronized void apply(Object values) {
704         set.addAll((Set<?>)values);
705      }
706
707      @Override /* MutableProperty */
708      synchronized void add(String arg, Object o) {
709         if (arg != null)
710            throw new ConfigException("Cannot use argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);
711         try {
712            set.addAll(normalize(type.converter, o));
713         } catch (Exception e) {
714            throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
715         }
716      }
717
718      @Override /* MutableProperty */
719      synchronized void remove(Object o) {
720         try {
721            set.removeAll(normalize(type.converter, o));
722         } catch (Exception e) {
723            throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
724         }
725      }
726
727      @Override /* MutableProperty */
728      synchronized boolean isEmpty() {
729         return set.isEmpty();
730      }
731
732      @Override /* MutableProperty */
733      synchronized Object peek() {
734         return set;
735      }
736   }
737
738   //-------------------------------------------------------------------------------------------------------------------
739   // MutableListProperty
740   //-------------------------------------------------------------------------------------------------------------------
741
742   static class MutableListProperty extends MutableProperty {
743      private final List<Object> list = synchronizedList(new LinkedList<>());
744
745      MutableListProperty(String name, PropertyType type, Object value) {
746         super(name, type);
747         set(value);
748      }
749
750      @Override /* MutableProperty */
751      synchronized Property build() {
752         return new Property(name, unmodifiableList(new ArrayList<>(list)), type);
753      }
754
755      @Override /* MutableProperty */
756      synchronized void set(Object value) {
757         try {
758            List<Object> newList = merge(list, type.converter, value);
759            list.clear();
760            list.addAll(newList);
761         } catch (Exception e) {
762            throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type);
763         }
764      }
765
766      @Override /* MutableProperty */
767      synchronized void apply(Object values) {
768         list.addAll((List<?>)values);
769      }
770
771      @Override /* MutableProperty */
772      synchronized void add(String arg, Object o) {
773         if (arg != null && ! StringUtils.isNumeric(arg))
774            throw new ConfigException("Invalid argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type);
775
776         int index = arg == null ? 0 : Integer.parseInt(arg);
777         if (index < 0)
778            index = 0;
779         else if (index > list.size())
780            index = list.size();
781
782         try {
783            List<Object> l = normalize(type.converter, o);
784            list.removeAll(l);
785            list.addAll(index, l);
786         } catch (Exception e) {
787            throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
788         }
789      }
790
791      @Override /* MutableProperty */
792      synchronized void remove(Object o) {
793         try {
794            list.removeAll(normalize(type.converter, o));
795         } catch (Exception e) {
796            throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type);
797         }
798      }
799
800      @Override /* MutableProperty */
801      synchronized boolean isEmpty() {
802         return list.isEmpty();
803      }
804
805      @Override /* MutableProperty */
806      synchronized Object peek() {
807         return list;
808      }
809   }
810
811   //-------------------------------------------------------------------------------------------------------------------
812   // MutableMapProperty
813   //-------------------------------------------------------------------------------------------------------------------
814
815   @SuppressWarnings("rawtypes")
816   static class MutableMapProperty extends MutableProperty {
817      protected Map<String,Object> map;
818
819      MutableMapProperty(String name, PropertyType type, Object value) {
820         super(name, type);
821         this.map = createMap();
822         set(value);
823      }
824
825      protected Map<String,Object> createMap() {
826         return new ConcurrentHashMap<>();
827      }
828
829      @Override /* MutableProperty */
830      synchronized Property build() {
831         return new Property(name, unmodifiableMap(new TreeMap<>(map)), type);
832      }
833
834      @Override /* MutableProperty */
835      synchronized void set(Object value) {
836         this.map.clear();
837         add(null, value);
838      }
839
840      @SuppressWarnings("unchecked")
841      @Override /* MutableProperty */
842      synchronized void apply(Object values) {
843         for (Map.Entry<String,Object> e : ((Map<String,Object>)values).entrySet())
844            add(e.getKey(), e.getValue());
845      }
846
847      @Override /* MutableProperty */
848      synchronized void add(String arg, Object o) {
849         if (arg != null) {
850            o = convert(o);
851            if (o == null)
852               this.map.remove(arg);
853            else
854               this.map.put(arg, o);
855
856         } else if (o != null) {
857            if (o instanceof Map) {
858               Map m = (Map)o;
859               for (Map.Entry e : (Set<Map.Entry>)m.entrySet())
860                  if (e.getKey() != null)
861                     add(e.getKey().toString(), e.getValue());
862            } else if (isObjectMap(o)) {
863               try {
864                  add(null, new ObjectMap(o.toString()));
865               } catch (Exception e) {
866                  throw new ConfigException(e, "Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
867               }
868            } else {
869               throw new ConfigException("Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type);
870            }
871         }
872      }
873
874      @Override /* MutableProperty */
875      synchronized boolean isEmpty() {
876         return this.map.isEmpty();
877      }
878
879      @Override /* MutableProperty */
880      synchronized Object peek() {
881         return map;
882      }
883   }
884
885   //-------------------------------------------------------------------------------------------------------------------
886   // MutableLinkedMapProperty
887   //-------------------------------------------------------------------------------------------------------------------
888
889   static class MutableLinkedMapProperty extends MutableMapProperty {
890
891      MutableLinkedMapProperty(String name, PropertyType type, Object value) {
892         super(name, type, value);
893         set(value);
894      }
895
896      @Override
897      protected Map<String,Object> createMap() {
898         return synchronizedMap(new LinkedHashMap<String,Object>());
899      }
900
901      @Override /* MutableProperty */
902      synchronized Property build() {
903         return new Property(name, unmodifiableMap(new LinkedHashMap<>(map)), type);
904      }
905   }
906
907
908   //-------------------------------------------------------------------------------------------------------------------
909   // Utility methods
910   //-------------------------------------------------------------------------------------------------------------------
911
912   static Set<Object> merge(Set<Object> oldSet, PropertyConverter<?> pc, Object o) throws Exception {
913      return merge(oldSet, new LinkedHashSet<>(), normalize(pc, o));
914   }
915
916   private static Set<Object> merge(Set<Object> oldSet, Set<Object> newSet, List<Object> l) {
917      for (Object o : l) {
918         if (isNone(o))
919            newSet.clear();
920         else if (isInherit(o))
921            newSet.addAll(oldSet);
922         else
923            newSet.add(o);
924      }
925      return newSet;
926   }
927
928   static List<Object> merge(List<Object> oldList, PropertyConverter<?> pc, Object o) throws Exception {
929      return merge(oldList, new ArrayList<>(), normalize(pc, o));
930   }
931
932   private static List<Object> merge(List<Object> oldList, List<Object> newList, List<Object> l) {
933      for (Object o : l) {
934         if (isIndexed(o)) {
935            Matcher lm = INDEXED_LINK_PATTERN.matcher(o.toString());
936            lm.matches();
937            String key = lm.group(1);
938            int i2 = Math.min(newList.size(), Integer.parseInt(lm.group(2)));
939            String remainder = lm.group(3);
940            newList.add(i2, key.isEmpty() ? remainder : key + ":" + remainder);
941         } else if (isNone(o)) {
942            newList.clear();
943         } else if (isInherit(o)) {
944            if (oldList != null)
945               for (Object o2 : oldList)
946                  newList.add(o2);
947         } else {
948            newList.remove(o);
949            newList.add(o);
950         }
951      }
952
953      return newList;
954   }
955
956   static List<Object> normalize(PropertyConverter<?> pc, Object o) throws Exception {
957      return normalize(new ArrayList<>(), pc, o);
958   }
959
960   @SuppressWarnings("unchecked")
961   static List<Object> normalize(List<Object> l, PropertyConverter<?> pc, Object o) throws Exception {
962      if (o != null) {
963         if (o.getClass().isArray()) {
964            for (int i = 0; i < Array.getLength(o); i++)
965               normalize(l, pc, Array.get(o, i));
966         } else if (o instanceof Collection) {
967            for (Object o2 : (Collection<Object>)o)
968               normalize(l, pc, o2);
969         } else if (isObjectList(o)) {
970            normalize(l, pc, new ObjectList(o.toString()));
971         } else {
972            l.add(pc == null ? o : pc.convert(o));
973         }
974      }
975      return l;
976   }
977
978   static String string(Object value) {
979      return SimpleJsonSerializer.DEFAULT.toString(value);
980   }
981
982   static String className(Object value) {
983      return value.getClass().getSimpleName();
984   }
985
986   static boolean isObjectMap(Object o) {
987      if (o instanceof CharSequence) {
988         String s = o.toString();
989         return (s.startsWith("{") && s.endsWith("}") && BeanContext.DEFAULT != null);
990      }
991      return false;
992   }
993
994   private static String group(String key) {
995      if (key == null || key.indexOf('.') == -1)
996         return "";
997      return key.substring(0, key.indexOf('.'));
998   }
999
1000   static boolean isObjectList(Object o) {
1001      if (o instanceof CharSequence) {
1002         String s = o.toString();
1003         return (s.startsWith("[") && s.endsWith("]") && BeanContext.DEFAULT != null);
1004      }
1005      return false;
1006   }
1007
1008   private static boolean isNone(Object o) {
1009      if (o instanceof CharSequence) {
1010         String s = o.toString();
1011         return "NONE".equals(s);
1012      }
1013      return false;
1014   }
1015
1016   private static boolean isIndexed(Object o) {
1017      if (o instanceof CharSequence) {
1018         String s = o.toString();
1019         return s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches();
1020      }
1021      return false;
1022   }
1023
1024   private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)");
1025
1026   private static boolean isInherit(Object o) {
1027      if (o instanceof CharSequence) {
1028         String s = o.toString();
1029         return "INHERIT".equals(s);
1030      }
1031      return false;
1032   }
1033
1034   @Override /* Object */
1035   public String toString() {
1036      return SimpleJson.DEFAULT_READABLE.toString(groups);
1037   }
1038}