| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau; |
| |
| import static java.util.Collections.*; |
| |
| import java.lang.annotation.*; |
| import java.lang.reflect.*; |
| import java.util.*; |
| import java.util.concurrent.*; |
| import java.util.regex.*; |
| |
| import org.apache.juneau.PropertyStore.*; |
| import org.apache.juneau.annotation.*; |
| import org.apache.juneau.csv.annotation.*; |
| import org.apache.juneau.html.annotation.*; |
| import org.apache.juneau.internal.*; |
| import org.apache.juneau.jso.annotation.*; |
| import org.apache.juneau.json.*; |
| import org.apache.juneau.json.annotation.*; |
| import org.apache.juneau.jsonschema.annotation.*; |
| import org.apache.juneau.marshall.*; |
| import org.apache.juneau.msgpack.annotation.*; |
| import org.apache.juneau.oapi.annotation.*; |
| import org.apache.juneau.parser.annotation.*; |
| import org.apache.juneau.plaintext.annotation.*; |
| import org.apache.juneau.reflect.*; |
| import org.apache.juneau.serializer.annotation.*; |
| import org.apache.juneau.soap.annotation.*; |
| import org.apache.juneau.svl.*; |
| import org.apache.juneau.uon.annotation.*; |
| import org.apache.juneau.urlencoding.annotation.*; |
| import org.apache.juneau.xml.annotation.*; |
| |
| /** |
| * A builder for {@link PropertyStore} objects. |
| */ |
| public class PropertyStoreBuilder { |
| |
| // Contains a cache of all created PropertyStore objects keyed by hashcode. |
| // Used to minimize memory consumption by reusing identical PropertyStores. |
| private static final Map<Integer,PropertyStore> CACHE = new ConcurrentHashMap<>(); |
| |
| // Maps property suffixes (e.g. "lc") to PropertyType (e.g. LIST_CLASS) |
| static final Map<String,PropertyType> SUFFIX_MAP = new ConcurrentHashMap<>(); |
| static { |
| for (PropertyType pt : PropertyType.values()) |
| SUFFIX_MAP.put(pt.getSuffix(), pt); |
| } |
| |
| private final Map<String,PropertyGroupBuilder> groups = new ConcurrentSkipListMap<>(); |
| |
| // Previously-created property store. |
| private volatile PropertyStore propertyStore; |
| |
| // Called by PropertyStore.builder() |
| PropertyStoreBuilder(PropertyStore ps) { |
| apply(ps); |
| } |
| |
| // Called by PropertyStore.create() |
| PropertyStoreBuilder() {} |
| |
| /** |
| * Creates a new {@link PropertyStore} based on the values in this builder. |
| * |
| * @return A new {@link PropertyStore} based on the values in this builder. |
| */ |
| public synchronized PropertyStore build() { |
| |
| // Reused the last one if we haven't change this builder. |
| if (propertyStore == null) |
| propertyStore = new PropertyStore(groups); |
| |
| PropertyStore ps = CACHE.get(propertyStore.hashCode()); |
| if (ps == null) |
| CACHE.put(propertyStore.hashCode(), propertyStore); |
| else if (! ps.equals(propertyStore)) |
| throw new RuntimeException("Property store mismatch! This shouldn't happen. hashCode=["+propertyStore.hashCode()+"]\n---PS#1---\n" + ps + "\n---PS#2---\n" + propertyStore); |
| else |
| propertyStore = ps; |
| |
| return propertyStore; |
| } |
| |
| /** |
| * Copies all the values in the specified property store into this builder. |
| * |
| * @param copyFrom The property store to copy the values from. |
| * @return This object (for method chaining). |
| */ |
| public synchronized PropertyStoreBuilder apply(PropertyStore copyFrom) { |
| propertyStore = null; |
| |
| if (copyFrom != null) |
| for (Map.Entry<String,PropertyGroup> e : copyFrom.groups.entrySet()) { |
| String gName = e.getKey(); |
| PropertyGroupBuilder g1 = this.groups.get(gName); |
| PropertyGroup g2 = e.getValue(); |
| if (g1 == null) |
| this.groups.put(gName, g2.builder()); |
| else |
| g1.apply(g2); |
| } |
| return this; |
| } |
| |
| /** |
| * Applies the settings in the specified annotations to this property store. |
| * |
| * @param al The list of annotations to apply. |
| * @param r The string resolver used to resolve any variables in the annotations. |
| * @return This object (for method chaining). |
| */ |
| @SuppressWarnings("unchecked") |
| public PropertyStoreBuilder applyAnnotations(AnnotationList al, VarResolverSession r) { |
| for (AnnotationInfo<?> ai : al.sort()) { |
| try { |
| ai.getConfigApply(r).apply((AnnotationInfo<Annotation>)ai, this); |
| } catch (ConfigException ex) { |
| throw ex; |
| } catch (Exception ex) { |
| throw new ConfigException(ex, "Could not instantiate ConfigApply class {0}", ai); |
| } |
| } |
| return this; |
| } |
| |
| /** |
| * Applies any of the various <ja>@XConfig</ja> annotations on the specified class to this property store. |
| * |
| * <p> |
| * Applies any of the following annotations: |
| * <ul class='javatree'> |
| * <li class ='ja'>{@link BeanConfig} |
| * <li class ='ja'>{@link CsvConfig} |
| * <li class ='ja'>{@link HtmlConfig} |
| * <li class ='ja'>{@link HtmlDocConfig} |
| * <li class ='ja'>{@link JsoConfig} |
| * <li class ='ja'>{@link JsonConfig} |
| * <li class ='ja'>{@link JsonSchemaConfig} |
| * <li class ='ja'>{@link MsgPackConfig} |
| * <li class ='ja'>{@link OpenApiConfig} |
| * <li class ='ja'>{@link ParserConfig} |
| * <li class ='ja'>{@link PlainTextConfig} |
| * <li class ='ja'>{@link SerializerConfig} |
| * <li class ='ja'>{@link SoapXmlConfig} |
| * <li class ='ja'>{@link UonConfig} |
| * <li class ='ja'>{@link UrlEncodingConfig} |
| * <li class ='ja'>{@link XmlConfig} |
| * <li class ='ja'><c>RdfConfig</c> |
| * </ul> |
| * |
| * <p> |
| * Annotations are appended in the following order: |
| * <ol> |
| * <li>On the package of this class. |
| * <li>On interfaces ordered parent-to-child. |
| * <li>On parent classes ordered parent-to-child. |
| * <li>On this class. |
| * </ol> |
| * |
| * @param fromClass The class on which the annotations are defined. |
| * @return This object (for method chaining). |
| */ |
| public PropertyStoreBuilder applyAnnotations(Class<?> fromClass) { |
| applyAnnotations(ClassInfo.of(fromClass).getAnnotationListParentFirst(ConfigAnnotationFilter.INSTANCE), VarResolver.DEFAULT.createSession()); |
| return this; |
| } |
| |
| /** |
| * Applies any of the various <ja>@XConfig</ja> annotations on the specified method to this property store. |
| * |
| * <p> |
| * Applies any of the following annotations: |
| * <ul class='javatree'> |
| * <li class ='ja'>{@link BeanConfig} |
| * <li class ='ja'>{@link CsvConfig} |
| * <li class ='ja'>{@link HtmlConfig} |
| * <li class ='ja'>{@link HtmlDocConfig} |
| * <li class ='ja'>{@link JsoConfig} |
| * <li class ='ja'>{@link JsonConfig} |
| * <li class ='ja'>{@link JsonSchemaConfig} |
| * <li class ='ja'>{@link MsgPackConfig} |
| * <li class ='ja'>{@link OpenApiConfig} |
| * <li class ='ja'>{@link ParserConfig} |
| * <li class ='ja'>{@link PlainTextConfig} |
| * <li class ='ja'>{@link SerializerConfig} |
| * <li class ='ja'>{@link SoapXmlConfig} |
| * <li class ='ja'>{@link UonConfig} |
| * <li class ='ja'>{@link UrlEncodingConfig} |
| * <li class ='ja'>{@link XmlConfig} |
| * <li class ='ja'><c>RdfConfig</c> |
| * </ul> |
| * |
| * <p> |
| * Annotations are appended in the following orders: |
| * <ol> |
| * <li>On the package of the method class. |
| * <li>On interfaces ordered parent-to-child. |
| * <li>On parent classes ordered parent-to-child. |
| * <li>On the method class. |
| * <li>On this method and matching methods ordered parent-to-child. |
| * </ol> |
| * |
| * @param fromMethod The method on which the annotations are defined. |
| * @return This object (for method chaining). |
| */ |
| public PropertyStoreBuilder applyAnnotations(Method fromMethod) { |
| applyAnnotations(MethodInfo.of(fromMethod).getAnnotationListParentFirst(ConfigAnnotationFilter.INSTANCE), VarResolver.DEFAULT.createSession()); |
| return this; |
| } |
| |
| /** |
| * Sets a configuration property value on this object. |
| * |
| * @param key |
| * The configuration property key. |
| * <br>(e.g <js>"BeanContext.foo.ss/add.1"</js>) |
| * <br>If name ends with <l>/add</l>, then the specified value is added to the existing property value as an entry |
| * in a SET or LIST property. |
| * <br>If name ends with <l>/add.{key}</l>, then the specified value is added to the existing property value as a |
| * key/value pair in a MAP property. |
| * <br>If name ends with <l>/remove</l>, then the specified value is removed from the existing property property |
| * value in a SET or LIST property. |
| * @param value |
| * The new value. |
| * If <jk>null</jk>, the property value is deleted. |
| * In general, the value type can be anything. |
| * @return This object (for method chaining). |
| */ |
| public synchronized PropertyStoreBuilder set(String key, Object value) { |
| propertyStore = null; |
| |
| String g = group(key); |
| |
| int i = key.indexOf('/'); |
| if (i != -1) { |
| String command = key.substring(i+1), arg = null; |
| String key2 = key.substring(0, i); |
| int j = command.indexOf('.'); |
| if (j != -1) { |
| arg = command.substring(j+1); |
| command = command.substring(0, j); |
| } |
| |
| if ("add".equals(command)) { |
| return addTo(key2, arg, value); |
| } else if ("remove".equals(command)) { |
| if (arg != null) |
| throw new ConfigException("Invalid key specified: ''{0}''", key); |
| return removeFrom(key2, value); |
| } else { |
| throw new ConfigException("Invalid key specified: ''{0}''", key); |
| } |
| } |
| |
| String n = g.isEmpty() ? key : key.substring(g.length()+1); |
| |
| PropertyGroupBuilder gb = groups.get(g); |
| if (gb == null) { |
| gb = new PropertyGroupBuilder(); |
| groups.put(g, gb); |
| } |
| |
| gb.set(n, value); |
| |
| if (gb.isEmpty()) |
| groups.remove(g); |
| |
| return this; |
| } |
| |
| /** |
| * Removes the property with the specified key. |
| * |
| * <p> |
| * This is equivalent to calling <code>set(key, <jk>null</jk>);</code> |
| * |
| * @param key The property key. |
| * @return This object (for method chaining). |
| */ |
| public synchronized PropertyStoreBuilder remove(String key) { |
| propertyStore = null; |
| return set(key, null); |
| } |
| |
| /** |
| * Convenience method for setting multiple properties in one call. |
| * |
| * <p> |
| * This replaces any previous configuration properties set on this store. |
| * |
| * @param newProperties The new properties to set. |
| * @return This object (for method chaining). |
| */ |
| public synchronized PropertyStoreBuilder set(Map<String,Object> newProperties) { |
| propertyStore = null; |
| clear(); |
| add(newProperties); |
| return this; |
| } |
| |
| /** |
| * Convenience method for setting multiple properties in one call. |
| * |
| * <p> |
| * This appends to any previous configuration properties set on this store. |
| * |
| * @param newProperties The new properties to set. |
| * @return This object (for method chaining). |
| */ |
| public synchronized PropertyStoreBuilder add(Map<String,Object> newProperties) { |
| propertyStore = null; |
| |
| if (newProperties != null) |
| for (Map.Entry<String,Object> e : newProperties.entrySet()) |
| set(e.getKey(), e.getValue()); |
| |
| return this; |
| } |
| |
| /** |
| * Adds one or more values to a SET, LIST, or MAP property. |
| * |
| * @param key The property key. |
| * @param arg |
| * The argument. |
| * <br>For SETs, this must always be <jk>null</jk>. |
| * <br>For LISTs, this can be <jk>null</jk> or a numeric index. |
| * Out-of-range indexes are simply 'adjusted' to the beginning or the end of the list. |
| * So, for example, a value of <js>"-100"</js> will always just cause the entry to be added to the beginning |
| * of the list. |
| * <br>NOTE: If <jk>null</jk>, value will be inserted at position 0. |
| * <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. |
| * @param value |
| * The new value to add to the property. |
| * <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string. |
| * <br>For MAPs, this can be a single value, Map, or JSON object string. |
| * <br><jk>null</jk> values have the effect of removing entries. |
| * @return This object (for method chaining). |
| * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid. |
| */ |
| public synchronized PropertyStoreBuilder addTo(String key, String arg, Object value) { |
| propertyStore = null; |
| String g = group(key); |
| String n = g.isEmpty() ? key : key.substring(g.length()+1); |
| |
| PropertyGroupBuilder gb = groups.get(g); |
| if (gb == null) { |
| gb = new PropertyGroupBuilder(); |
| groups.put(g, gb); |
| } |
| |
| gb.addTo(n, arg, value); |
| |
| if (gb.isEmpty()) |
| groups.remove(g); |
| |
| return this; |
| } |
| |
| /** |
| * Adds/prepends a value to a SET, LIST, or MAP property. |
| * |
| * <p> |
| * Shortcut for calling <code>addTo(key, <jk>null</jk>, value);</code>. |
| * |
| * <p> |
| * NOTE: When adding to a list, the value is inserted at the beginning of the list. |
| * |
| * @param key The property key. |
| * @param value |
| * The new value to add to the property. |
| * <br>For SETs and LISTs, this can be a single value, Collection, array, or JSON array string. |
| * <br>for MAPs, this can be a single value, Map, or JSON object string. |
| * <br><jk>null</jk> values have the effect of removing entries. |
| * @return This object (for method chaining). |
| * @throws ConfigException If property is not a SET/LIST/MAP property, or the argument is invalid. |
| */ |
| public synchronized PropertyStoreBuilder addTo(String key, Object value) { |
| propertyStore = null; |
| return addTo(key, null, value); |
| } |
| |
| /** |
| * Removes a value from a SET or LIST property. |
| * |
| * @param key The property key. |
| * @param value The property value in the property. |
| * @return This object (for method chaining). |
| * @throws ConfigException If property is not a SET or LIST property. |
| */ |
| public synchronized PropertyStoreBuilder removeFrom(String key, Object value) { |
| propertyStore = null; |
| String g = group(key); |
| String n = g.isEmpty() ? key : key.substring(g.length()+1); |
| |
| PropertyGroupBuilder gb = groups.get(g); |
| |
| // Create property group anyway to generate a good error message. |
| if (gb == null) |
| gb = new PropertyGroupBuilder(); |
| |
| gb.removeFrom(n, value); |
| |
| if (gb.isEmpty()) |
| groups.remove(g); |
| |
| return this; |
| } |
| |
| /** |
| * Peeks at a property value. |
| * |
| * <p> |
| * Used for debugging purposes. |
| * |
| * @param key The property key. |
| * @return The property value, or <jk>null</jk> if it doesn't exist. |
| */ |
| public Object peek(String key) { |
| String g = group(key); |
| String n = g.isEmpty() ? key : key.substring(g.length()+1); |
| |
| PropertyGroupBuilder gb = groups.get(g); |
| |
| // Create property group anyway to generate a good error message. |
| if (gb == null) |
| return null; |
| |
| MutableProperty bp = gb.properties.get(n); |
| if (bp == null) |
| return null; |
| |
| return bp.peek(); |
| } |
| |
| /** |
| * Same as {@link #peek(String)} but converts the value to the specified type. |
| * |
| * @param <T> The type to convert to. |
| * @param c The type to convert to. |
| * @param key The property key. |
| * @return The property value, or <jk>null</jk> if it doesn't exist. |
| */ |
| public <T> T peek(Class<T> c, String key) { |
| Object o = peek(key); |
| if (o == null) |
| return null; |
| return BeanContext.DEFAULT.createBeanSession().convertToType(o, c); |
| } |
| |
| /** |
| * Clears all entries in this property store. |
| */ |
| public synchronized void clear() { |
| propertyStore = null; |
| this.groups.clear(); |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // PropertyGroupBuilder |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static class PropertyGroupBuilder { |
| final Map<String,MutableProperty> properties = new ConcurrentSkipListMap<>(); |
| |
| PropertyGroupBuilder() { |
| this(emptyMap()); |
| } |
| |
| synchronized void apply(PropertyGroup copyFrom) { |
| for (Map.Entry<String,Property> e : copyFrom.properties.entrySet()) { |
| String pName = e.getKey(); |
| MutableProperty p1 = properties.get(pName); |
| Property p2 = e.getValue(); |
| if (p1 == null) |
| properties.put(pName, p2.mutable()); |
| else |
| p1.apply(p2.value); |
| } |
| } |
| |
| PropertyGroupBuilder(Map<String,Property> properties) { |
| for (Map.Entry<String,Property> p : properties.entrySet()) |
| this.properties.put(p.getKey(), p.getValue().mutable()); |
| } |
| |
| synchronized void set(String key, Object value) { |
| MutableProperty p = properties.get(key); |
| if (p == null) { |
| p = MutableProperty.create(key, value); |
| if (! p.isEmpty()) |
| properties.put(key, p); |
| } else { |
| p.set(value); |
| if (p.isEmpty()) |
| properties.remove(key); |
| else |
| properties.put(key, p); |
| } |
| } |
| |
| synchronized void addTo(String key, String arg, Object value) { |
| MutableProperty p = properties.get(key); |
| if (p == null) { |
| p = MutableProperty.create(key, null); |
| p.add(arg, value); |
| if (! p.isEmpty()) |
| properties.put(key, p); |
| } else { |
| p.add(arg, value); |
| if (p.isEmpty()) |
| properties.remove(key); |
| else |
| properties.put(key, p); |
| } |
| } |
| |
| synchronized void removeFrom(String key, Object value) { |
| MutableProperty p = properties.get(key); |
| if (p == null) { |
| // Create property anyway to generate a good error message. |
| p = MutableProperty.create(key, null); |
| p.remove(value); |
| } else { |
| p.remove(value); |
| if (p.isEmpty()) |
| properties.remove(key); |
| else |
| properties.put(key, p); |
| } |
| } |
| |
| synchronized boolean isEmpty() { |
| return properties.isEmpty(); |
| } |
| |
| synchronized PropertyGroup build() { |
| return new PropertyGroup(properties); |
| } |
| |
| /** |
| * Used by the <c>toString()</c> method for debugging. |
| * |
| * @param bs The bean session. |
| * @return This object converted to a map. |
| */ |
| public Map<String,MutableProperty> swap(BeanSession bs) { |
| return properties; |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static abstract class MutableProperty { |
| final String name; |
| final PropertyType type; |
| |
| MutableProperty(String name, PropertyType type) { |
| this.name = name; |
| this.type = type; |
| } |
| |
| static MutableProperty create(String name, Object value) { |
| int i = name.lastIndexOf('.'); |
| String type = i == -1 ? "s" : name.substring(i+1); |
| PropertyType pt = SUFFIX_MAP.get(type); |
| |
| if (pt == null) |
| throw new ConfigException("Invalid type specified on property ''{0}''", name); |
| |
| switch (pt) { |
| case STRING: |
| case BOOLEAN: |
| case INTEGER: |
| case CLASS: |
| case OBJECT: return new MutableSimpleProperty(name, pt, value); |
| case SET_STRING: |
| case SET_INTEGER: |
| case SET_CLASS: return new MutableSetProperty(name, pt, value); |
| case LIST_STRING: |
| case LIST_INTEGER: |
| case LIST_CLASS: |
| case LIST_OBJECT: return new MutableListProperty(name, pt, value); |
| case SORTED_MAP_STRING: |
| case SORTED_MAP_INTEGER: |
| case SORTED_MAP_CLASS: |
| case SORTED_MAP_OBJECT: return new MutableMapProperty(name, pt, value); |
| case ORDERED_MAP_STRING: |
| case ORDERED_MAP_INTEGER: |
| case ORDERED_MAP_CLASS: |
| case ORDERED_MAP_OBJECT: return new MutableLinkedMapProperty(name, pt, value); |
| default: return new MutableSimpleProperty(name, PropertyType.STRING, value); |
| } |
| } |
| |
| abstract Property build(); |
| |
| abstract boolean isEmpty(); |
| |
| abstract void set(Object value); |
| |
| abstract void apply(Object value); |
| |
| abstract Object peek(); |
| |
| void add(String arg, Object value) { |
| throw new ConfigException("Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(value), className(value), name, type); |
| } |
| |
| void remove(Object value) { |
| throw new ConfigException("Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(value), className(value), name, type); |
| } |
| |
| Object convert(Object value) { |
| return value == null ? null : type.converter.convert(value); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return StringUtils.stringify(peek()); |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableSimpleProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static class MutableSimpleProperty extends MutableProperty { |
| private Object value; |
| |
| MutableSimpleProperty(String name, PropertyType type, Object value) { |
| super(name, type); |
| set(value); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Property build() { |
| return new Property(name, value, type); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void set(Object value) { |
| this.value = convert(value); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void apply(Object value) { |
| this.value = convert(value); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized boolean isEmpty() { |
| return this.value == null; |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Object peek() { |
| return value; |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableSetProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static class MutableSetProperty extends MutableProperty { |
| private final Set<Object> set; |
| |
| MutableSetProperty(String name, PropertyType type, Object value) { |
| super(name, type); |
| set = new ConcurrentSkipListSet<>(type.comparator()); |
| set(value); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Property build() { |
| return new Property(name, unmodifiableSet(new LinkedHashSet<>(set)), type); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void set(Object value) { |
| try { |
| Set<Object> newSet = merge(set, type.converter, value); |
| set.clear(); |
| set.addAll(newSet); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void apply(Object values) { |
| set.addAll((Set<?>)values); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void add(String arg, Object o) { |
| if (arg != null) |
| throw new ConfigException("Cannot use argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type); |
| try { |
| set.addAll(normalize(type.converter, o)); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void remove(Object o) { |
| try { |
| set.removeAll(normalize(type.converter, o)); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized boolean isEmpty() { |
| return set.isEmpty(); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Object peek() { |
| return set; |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableListProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static class MutableListProperty extends MutableProperty { |
| private final List<Object> list = synchronizedList(new LinkedList<>()); |
| |
| MutableListProperty(String name, PropertyType type, Object value) { |
| super(name, type); |
| set(value); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Property build() { |
| return new Property(name, unmodifiableList(new ArrayList<>(list)), type); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void set(Object value) { |
| try { |
| List<Object> newList = merge(list, type.converter, value); |
| list.clear(); |
| list.addAll(newList); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot set value {0} ({1}) on property ''{2}'' ({3}).", string(value), className(value), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void apply(Object values) { |
| list.addAll((List<?>)values); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void add(String arg, Object o) { |
| if (arg != null && ! StringUtils.isNumeric(arg)) |
| throw new ConfigException("Invalid argument ''{0}'' on add command for property ''{1}'' ({2})", arg, name, type); |
| |
| int index = arg == null ? 0 : Integer.parseInt(arg); |
| if (index < 0) |
| index = 0; |
| else if (index > list.size()) |
| index = list.size(); |
| |
| try { |
| List<Object> l = normalize(type.converter, o); |
| list.removeAll(l); |
| list.addAll(index, l); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot add value {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void remove(Object o) { |
| try { |
| list.removeAll(normalize(type.converter, o)); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot remove value {0} ({1}) from property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized boolean isEmpty() { |
| return list.isEmpty(); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Object peek() { |
| return list; |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableMapProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| @SuppressWarnings("rawtypes") |
| static class MutableMapProperty extends MutableProperty { |
| protected Map<String,Object> map; |
| |
| MutableMapProperty(String name, PropertyType type, Object value) { |
| super(name, type); |
| this.map = createMap(); |
| set(value); |
| } |
| |
| protected Map<String,Object> createMap() { |
| return new ConcurrentHashMap<>(); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Property build() { |
| return new Property(name, unmodifiableMap(new TreeMap<>(map)), type); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void set(Object value) { |
| this.map.clear(); |
| add(null, value); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override /* MutableProperty */ |
| synchronized void apply(Object values) { |
| for (Map.Entry<String,Object> e : ((Map<String,Object>)values).entrySet()) |
| add(e.getKey(), e.getValue()); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized void add(String arg, Object o) { |
| if (arg != null) { |
| o = convert(o); |
| if (o == null) |
| this.map.remove(arg); |
| else |
| this.map.put(arg, o); |
| |
| } else if (o != null) { |
| if (o instanceof Map) { |
| Map m = (Map)o; |
| for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) |
| if (e.getKey() != null) |
| add(e.getKey().toString(), e.getValue()); |
| } else if (isObjectMap(o)) { |
| try { |
| add(null, new ObjectMap(o.toString())); |
| } catch (Exception e) { |
| throw new ConfigException(e, "Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } else { |
| throw new ConfigException("Cannot add {0} ({1}) to property ''{2}'' ({3}).", string(o), className(o), name, type); |
| } |
| } |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized boolean isEmpty() { |
| return this.map.isEmpty(); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Object peek() { |
| return map; |
| } |
| } |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // MutableLinkedMapProperty |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static class MutableLinkedMapProperty extends MutableMapProperty { |
| |
| MutableLinkedMapProperty(String name, PropertyType type, Object value) { |
| super(name, type, value); |
| set(value); |
| } |
| |
| @Override |
| protected Map<String,Object> createMap() { |
| return synchronizedMap(new LinkedHashMap<String,Object>()); |
| } |
| |
| @Override /* MutableProperty */ |
| synchronized Property build() { |
| return new Property(name, unmodifiableMap(new LinkedHashMap<>(map)), type); |
| } |
| } |
| |
| |
| //------------------------------------------------------------------------------------------------------------------- |
| // Utility methods |
| //------------------------------------------------------------------------------------------------------------------- |
| |
| static Set<Object> merge(Set<Object> oldSet, PropertyConverter<?> pc, Object o) throws Exception { |
| return merge(oldSet, new LinkedHashSet<>(), normalize(pc, o)); |
| } |
| |
| private static Set<Object> merge(Set<Object> oldSet, Set<Object> newSet, List<Object> l) { |
| for (Object o : l) { |
| if (isNone(o)) |
| newSet.clear(); |
| else if (isInherit(o)) |
| newSet.addAll(oldSet); |
| else |
| newSet.add(o); |
| } |
| return newSet; |
| } |
| |
| static List<Object> merge(List<Object> oldList, PropertyConverter<?> pc, Object o) throws Exception { |
| return merge(oldList, new ArrayList<>(), normalize(pc, o)); |
| } |
| |
| private static List<Object> merge(List<Object> oldList, List<Object> newList, List<Object> l) { |
| for (Object o : l) { |
| if (isIndexed(o)) { |
| Matcher lm = INDEXED_LINK_PATTERN.matcher(o.toString()); |
| lm.matches(); |
| String key = lm.group(1); |
| int i2 = Math.min(newList.size(), Integer.parseInt(lm.group(2))); |
| String remainder = lm.group(3); |
| newList.add(i2, key.isEmpty() ? remainder : key + ":" + remainder); |
| } else if (isNone(o)) { |
| newList.clear(); |
| } else if (isInherit(o)) { |
| if (oldList != null) |
| for (Object o2 : oldList) |
| newList.add(o2); |
| } else { |
| newList.remove(o); |
| newList.add(o); |
| } |
| } |
| |
| return newList; |
| } |
| |
| static List<Object> normalize(PropertyConverter<?> pc, Object o) throws Exception { |
| return normalize(new ArrayList<>(), pc, o); |
| } |
| |
| @SuppressWarnings("unchecked") |
| static List<Object> normalize(List<Object> l, PropertyConverter<?> pc, Object o) throws Exception { |
| if (o != null) { |
| if (o.getClass().isArray()) { |
| for (int i = 0; i < Array.getLength(o); i++) |
| normalize(l, pc, Array.get(o, i)); |
| } else if (o instanceof Collection) { |
| for (Object o2 : (Collection<Object>)o) |
| normalize(l, pc, o2); |
| } else if (isObjectList(o)) { |
| normalize(l, pc, new ObjectList(o.toString())); |
| } else { |
| l.add(pc == null ? o : pc.convert(o)); |
| } |
| } |
| return l; |
| } |
| |
| static String string(Object value) { |
| return SimpleJsonSerializer.DEFAULT.toString(value); |
| } |
| |
| static String className(Object value) { |
| return value.getClass().getSimpleName(); |
| } |
| |
| static boolean isObjectMap(Object o) { |
| if (o instanceof CharSequence) { |
| String s = o.toString(); |
| return (s.startsWith("{") && s.endsWith("}") && BeanContext.DEFAULT != null); |
| } |
| return false; |
| } |
| |
| private static String group(String key) { |
| if (key == null || key.indexOf('.') == -1) |
| return ""; |
| return key.substring(0, key.indexOf('.')); |
| } |
| |
| static boolean isObjectList(Object o) { |
| if (o instanceof CharSequence) { |
| String s = o.toString(); |
| return (s.startsWith("[") && s.endsWith("]") && BeanContext.DEFAULT != null); |
| } |
| return false; |
| } |
| |
| private static boolean isNone(Object o) { |
| if (o instanceof CharSequence) { |
| String s = o.toString(); |
| return "NONE".equals(s); |
| } |
| return false; |
| } |
| |
| private static boolean isIndexed(Object o) { |
| if (o instanceof CharSequence) { |
| String s = o.toString(); |
| return s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches(); |
| } |
| return false; |
| } |
| |
| private static final Pattern INDEXED_LINK_PATTERN = Pattern.compile("(?s)(\\S*)\\[(\\d+)\\]\\:(.*)"); |
| |
| private static boolean isInherit(Object o) { |
| if (o instanceof CharSequence) { |
| String s = o.toString(); |
| return "INHERIT".equals(s); |
| } |
| return false; |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return SimpleJson.DEFAULT_READABLE.toString(groups); |
| } |
| } |