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