All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.juneau.PropertyStoreBuilder Maven / Gradle / Ivy

There is a newer version: 9.0.1
Show newest version
// ***************************************************************************************************************************
// * 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.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;

import org.apache.juneau.PropertyStore.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.json.*;

/**
 * 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 CACHE = new ConcurrentHashMap<>();

	// Maps property suffixes (e.g. "lc") to PropertyType (e.g. LIST_CLASS)
	static final Map SUFFIX_MAP = new ConcurrentHashMap<>();
	static {
		for (PropertyType pt : PropertyType.values())
			SUFFIX_MAP.put(pt.getSuffix(), pt);
	}

	private final Map 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.");
		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 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;
	}

	/**
	 * Sets a configuration property value on this object.
	 *
	 * @param key
	 * 	The configuration property key.
	 * 	
(e.g "BeanContext.foo.ss/add.1") *
If name ends with /add, then the specified value is added to the existing property value as an entry * in a SET or LIST property. *
If name ends with /add.{key}, then the specified value is added to the existing property value as a * key/value pair in a MAP property. *
If name ends with /remove, then the specified value is removed from the existing property property * value in a SET or LIST property. * @param value * The new value. * If null, 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. * *

* This is equivalent to calling set(key, null); * * @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. * *

* 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 newProperties) { propertyStore = null; clear(); add(newProperties); return this; } /** * Convenience method for setting multiple properties in one call. * *

* 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 newProperties) { propertyStore = null; if (newProperties != null) for (Map.Entry 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. *
For SETs, this must always be null. *
For LISTs, this can be null 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 "-100" will always just cause the entry to be added to the beginning * of the list. *
For MAPs, this can be null 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. *
For SETs and LISTs, this can be a single value, Collection, array, or JSON array string. *
For MAPs, this can be a single value, Map, or JSON object string. *
null 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 a value to a SET, LIST, or MAP property. * *

* Shortcut for calling addTo(key, null, value);. * * @param key The property key. * @param value * The new value to add to the property. *
For SETs and LISTs, this can be a single value, Collection, array, or JSON array string. *
for MAPs, this can be a single value, Map, or JSON object string. *
null 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. * *

* Used for debugging purposes. * * @param key The property key. * @return The property value, or null 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(); } /** * Clears all entries in this property store. */ public synchronized void clear() { propertyStore = null; this.groups.clear(); } //------------------------------------------------------------------------------------------------------------------- // PropertyGroupBuilder //------------------------------------------------------------------------------------------------------------------- static class PropertyGroupBuilder { final Map properties = new ConcurrentSkipListMap<>(); PropertyGroupBuilder() { this(EMPTY_MAP); } synchronized void apply(PropertyGroup copyFrom) { for (Map.Entry 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 properties) { for (Map.Entry 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); } } //------------------------------------------------------------------------------------------------------------------- // 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); } } //------------------------------------------------------------------------------------------------------------------- // 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 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 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 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 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 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 map; MutableMapProperty(String name, PropertyType type, Object value) { super(name, type); this.map = createMap(); set(value); } protected Map 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 e : ((Map)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)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 createMap() { return synchronizedMap(new LinkedHashMap()); } @Override /* MutableProperty */ synchronized Property build() { return new Property(name, unmodifiableMap(new LinkedHashMap<>(map)), type); } } //------------------------------------------------------------------------------------------------------------------- // Utility methods //------------------------------------------------------------------------------------------------------------------- static Set merge(Set oldSet, PropertyConverter pc, Object o) throws Exception { return merge(oldSet, new LinkedHashSet<>(), normalize(pc, o)); } private static Set merge(Set oldSet, Set newSet, List 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 merge(List oldList, PropertyConverter pc, Object o) throws Exception { return merge(oldList, new ArrayList<>(), normalize(pc, o)); } private static List merge(List oldList, List newList, List 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 normalize(PropertyConverter pc, Object o) throws Exception { return normalize(new ArrayList<>(), pc, o); } @SuppressWarnings("unchecked") static List normalize(List 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)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; } }