Further improved implementation for
https://bugs.eclipse.org/bugs/show_bug.cgi?id=569910

Added EDEFProperties class and methods for both loading and storing EDEF
properties

Change-Id: Icc16f3f1e489d4000d59da61b85a852d865b6bc3
diff --git a/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/default.properties b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/default.properties
new file mode 100644
index 0000000..a11b757
--- /dev/null
+++ b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/default.properties
@@ -0,0 +1,4 @@
+service.imported=true
+endpoint.framework.uuid:uuid= 
+endpoint.id:uuid= 
+endpoint.service.id:Long=0
diff --git a/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/default.properties b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/default.properties
index 9c2e569..5ac3080 100644
--- a/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/default.properties
+++ b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/default.properties
@@ -1,5 +1,4 @@
 # other properties required by RSA specification (chap 122 in compendium spec)
-service.imported=true
-endpoint.framework.uuid:uuid= 
-endpoint.id:uuid= 
-endpoint.service.id:Long=0
+service.imported.configs:array=ecf.generic.server
+remote.configs.supported:array=ecf.generic.server
+remote.intents.supported:array=passByValue,exactlyOnce,ordered
diff --git a/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/timeserviceendpointdescription.xml b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/timeserviceendpointdescription.xml
index d2fda97..0e5e75b 100644
--- a/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/timeserviceendpointdescription.xml
+++ b/examples/bundles/com.mycorp.examples.timeservice.consumer.filediscovery/edef/timeserviceendpointdescription.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <endpoint-descriptions xmlns="http://www.osgi.org/xmlns/rsa/v1.0.0">
   <endpoint-description>
+    <property value-type="String" name="foo" value="bar"/>
   </endpoint-description>
 </endpoint-descriptions>
\ No newline at end of file
diff --git a/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EDEFProperties.java b/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EDEFProperties.java
new file mode 100644
index 0000000..e8299dc
--- /dev/null
+++ b/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EDEFProperties.java
@@ -0,0 +1,543 @@
+/****************************************************************************
+ * Copyright (c) 2020 Composent, Inc. and others.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Contributors:
+ *   Composent, Inc. - initial API and implementation
+ *   
+ * SPDX-License-Identifier: EPL-2.0
+ *****************************************************************************/
+package org.eclipse.ecf.osgi.services.remoteserviceadmin;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.UUID;
+
+import org.eclipse.ecf.internal.osgi.services.remoteserviceadmin.DebugOptions;
+import org.eclipse.ecf.internal.osgi.services.remoteserviceadmin.LogUtility;
+
+/**
+ * Class to represent EDEF properties for load from .properties file (via {@link #loadEDEFProperties(InputStream)} or
+ * {@link #loadEDEFProperties(Reader)}) or via store to .properties file (via {@link #storeEDEFProperties(BufferedWriter, String)}
+ * or {@link #storeEDEFProperties(Writer, String)}.  This class is used by the EndpointDescriptionLocator
+ * class to load from default.properties files as well as properties edeffile.properties to override
+ * the values from the edeffile.xml files specified by the Remote-Service header in manifest as per the
+ * RSA specification (chap 122 in compendium spec).
+ * @since 4.8
+ * 
+ */
+public class EDEFProperties extends Properties {
+
+	private static final long serialVersionUID = -7351470248095230347L;
+	private static int nextInt = 0;
+	private static long nextLong = 0;
+	private static short nextShort = 0;
+	private static byte nextByte = 0;
+
+	public class EDEFPropertiesValue {
+		private String type1 = "string"; //$NON-NLS-1$
+		private String type2 = "string"; //$NON-NLS-1$
+		private String valueString;
+		private Object valueObject;
+
+		boolean isArray() {
+			return this.type1.equalsIgnoreCase("array"); //$NON-NLS-1$
+		}
+
+		boolean isSet() {
+			return this.type1.equalsIgnoreCase("set"); //$NON-NLS-1$
+		}
+
+		boolean isList() {
+			return this.type1.equalsIgnoreCase("list"); //$NON-NLS-1$
+		}
+
+		boolean isCollection() {
+			return isSet() || isList();
+		}
+
+		boolean isSimpleType() {
+			return !isCollection() && !isArray();
+		}
+
+		public boolean hasTypeAgreement(EDEFPropertiesValue otherValue) {
+			String otherType = (otherValue.isArray() || otherValue.isCollection()) ? otherValue.type2
+					: otherValue.type1;
+			return (isArray() || isCollection()) ? this.type2.equalsIgnoreCase(otherType)
+					: this.type1.equalsIgnoreCase(otherType);
+		}
+
+		EDEFPropertiesValue addPropertyValue(EDEFPropertiesValue newValue) {
+			if (!hasTypeAgreement(newValue)) {
+				LogUtility.logError("addEDEFPropertyValue", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
+						"type disagreement between property values for old type1=" + this.type1 + ",type2=" //$NON-NLS-1$ //$NON-NLS-2$
+								+ this.type2 + ", and new type1=" + newValue.type1 + ",type2=" + newValue.type2); //$NON-NLS-1$ //$NON-NLS-2$
+				return this;
+			}
+			// get old and new values
+			Object ov = this.getValueObject();
+			Object nv = newValue.getValueObject();
+			// If we've got a collection already
+			if (isCollection()) {
+				Collection oldVs = (Collection) ov;
+				// If newValue is also collection
+				if (newValue.isCollection()) {
+					// Then addAll
+					oldVs.addAll((Collection) nv);
+				} else if (newValue.isSimpleType()) {
+					// Add single element
+					oldVs.add(nv);
+				}
+			} else if (isArray()) {
+				// Get old length
+				int oldLength = Array.getLength(ov);
+				Object newResultValue = null;
+				if (newValue.isArray()) {
+					// new value is also array...get length
+					int newLength = Array.getLength(nv);
+					// Check to make sure that the type of elements is same (type2 for arrays)
+					newResultValue = Array.newInstance(ov.getClass().getComponentType(), oldLength + newLength);
+					// copy ov contents to newResultValue
+					System.arraycopy(ov, 0, newResultValue, 0, oldLength);
+					// append nv contents to newResultValue after ov contents
+					System.arraycopy(nv, 0, newResultValue, oldLength, newLength);
+				} else if (newValue.isSimpleType()) {
+					newResultValue = Array.newInstance(ov.getClass().getComponentType(), oldLength + 1);
+					System.arraycopy(ov, 0, newResultValue, 0, oldLength);
+					Array.set(newResultValue, oldLength, nv);
+				}
+				if (newResultValue != null) {
+					this.valueObject = newResultValue;
+				}
+			}
+			return this;
+		}
+
+		EDEFPropertiesValue(String value) {
+			// Split value with =
+			String[] valueArr = value.split("="); //$NON-NLS-1$
+			// Split first one
+			if (valueArr.length > 1) {
+				// split second element in valueArr by :
+				String[] firstSplit = valueArr[0].split(":"); //$NON-NLS-1$
+				// If more than one then type2 is second element in firstSplit
+				if (firstSplit.length > 1) {
+					this.type2 = firstSplit[1];
+				}
+				// In either case type1 is firstSplit[0]
+				this.type1 = firstSplit[0];
+			}
+			// Now set value to the last elemtn in the valueArr
+			this.valueString = valueArr[valueArr.length - 1];
+		}
+
+		private void setType2(Collection coll) {
+			Class<?> c = coll.iterator().next().getClass();
+			if (!c.equals(String.class)) {
+				this.type2 = c.getSimpleName().toLowerCase();
+			}
+		}
+
+		EDEFPropertiesValue(Object value) {
+			Class<?> clazz = value.getClass();
+			if (clazz.isArray()) {
+				this.type1 = "array"; //$NON-NLS-1$
+				Class<?> compType = clazz.getComponentType();
+				if (!compType.equals(String.class)) {
+					this.type2 = compType.getSimpleName().toLowerCase();
+				}
+			} else if (List.class.isInstance(value)) {
+				this.type1 = "list"; //$NON-NLS-1$
+				setType2((List) value);
+			} else if (Set.class.isInstance(value)) {
+				this.type1 = "set"; //$NON-NLS-1$
+				setType2((Set) value);
+			} else {
+				String type = clazz.getSimpleName().toLowerCase();
+				if (!type.equalsIgnoreCase("string")) { //$NON-NLS-1$
+					this.type1 = type;
+				}
+			}
+			this.valueObject = value;
+
+		}
+
+		private Object getSimpleValue(Class<?> simpleType, Object value) {
+			try {
+				return simpleType.getDeclaredMethod("valueOf", new Class[] { String.class }).invoke(null, value); //$NON-NLS-1$
+			} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
+					| NoSuchMethodException | SecurityException e) {
+				LogUtility.logWarning("getSimpleValue", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, this.getClass(), //$NON-NLS-1$
+						"Cannot create instance of simpleType=" + simpleType + ", value=" + value); //$NON-NLS-1$ //$NON-NLS-2$
+				return null;
+			}
+		}
+
+		boolean isUnique() {
+			return "unique".equals(this.type2); //$NON-NLS-1$
+		}
+
+		Object readSimpleValue(String simpleType, String value) {
+			switch (simpleType) {
+			case "long": //$NON-NLS-1$
+			case "Long": //$NON-NLS-1$
+				if ("unique".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
+					return getNextLong();
+				} else if ("nanoTime".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
+					return System.nanoTime();
+				} else if ("milliTime".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
+					return System.currentTimeMillis();
+				}
+				return getSimpleValue(Long.class, value);
+			case "double": //$NON-NLS-1$
+			case "Double": //$NON-NLS-1$
+				return getSimpleValue(Double.class, value);
+			case "float": //$NON-NLS-1$
+			case "Float": //$NON-NLS-1$
+				return getSimpleValue(Float.class, value);
+			case "int": //$NON-NLS-1$
+			case "integer": //$NON-NLS-1$
+			case "Integer": //$NON-NLS-1$
+				if ("unique".equals(this.type2)) { //$NON-NLS-1$
+					return getNextInteger();
+				}
+				return getSimpleValue(Integer.class, value);
+			case "Byte": //$NON-NLS-1$
+			case "byte": //$NON-NLS-1$
+				if ("unique".equals(this.type2)) { //$NON-NLS-1$
+					return getNextByte();
+				}
+				return getSimpleValue(Byte.class, value);
+			case "char": //$NON-NLS-1$
+			case "Character": //$NON-NLS-1$
+				return getSimpleValue(Character.class, value.toCharArray()[0]);
+			case "boolean": //$NON-NLS-1$
+			case "Boolean": //$NON-NLS-1$
+				return getSimpleValue(Boolean.class, value);
+			case "short": //$NON-NLS-1$
+			case "Short": //$NON-NLS-1$
+				if ("unique".equals(this.type2)) { //$NON-NLS-1$
+					return getNextShort();
+				}
+				return getSimpleValue(Short.class, value);
+			case "uuid": //$NON-NLS-1$
+			case "Uuid": //$NON-NLS-1$
+			case "UUID": //$NON-NLS-1$
+				// we don't care whether 'unique' is given or not
+				return UUID.randomUUID().toString();
+			case "String": //$NON-NLS-1$
+			case "string": //$NON-NLS-1$
+				return value;
+			default:
+				return null;
+			}
+		}
+
+		Object readArrayValues(String collectionValue) {
+			String[] elements = this.valueString.split("\\s*,\\s*"); //$NON-NLS-1$
+			Object result = null;
+			for (int i = 0; i < elements.length; i++) {
+				Object elementValue = readSimpleValue(this.type2, elements[i]);
+				if (elementValue == null) {
+					LogUtility.logWarning("getArrayValues", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
+							"array element=" + elements[i] + " could not be created"); //$NON-NLS-1$//$NON-NLS-2$
+					continue;
+				} else {
+					if (i == 0) {
+						result = Array.newInstance(elementValue.getClass(), elements.length);
+					}
+					Array.set(result, i, elementValue);
+				}
+			}
+			return result;
+		}
+
+		Collection<Object> readCollectionValues(Collection<Object> c, String collectionValue) {
+			String[] elements = this.valueString.split("\\s*,\\s*"); //$NON-NLS-1$
+			for (String element : elements) {
+				Object elementValue = readSimpleValue(this.type2, element);
+				if (elementValue == null) {
+					LogUtility.logWarning("getCollectionValues", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, //$NON-NLS-1$
+							getClass(), "array element=" + element + " could not be created"); //$NON-NLS-1$//$NON-NLS-2$
+					continue;
+				} else {
+					c.add(elementValue);
+				}
+			}
+			return c;
+		}
+
+		public synchronized Object getValueObject() {
+			if (this.valueObject == null) {
+				switch (this.type1) {
+				case "array": //$NON-NLS-1$
+					this.valueObject = readArrayValues(this.valueString);
+					break;
+				case "list": //$NON-NLS-1$
+					this.valueObject = readCollectionValues(new ArrayList(), this.valueString);
+					break;
+				case "set": //$NON-NLS-1$
+					this.valueObject = readCollectionValues(new HashSet(), this.valueString);
+					break;
+				default:
+					this.valueObject = readSimpleValue(this.type1, this.valueString);
+					break;
+				}
+			}
+			return this.valueObject;
+		}
+
+		public synchronized String getEDEFPropertyValueString() {
+			if (this.valueString == null) {
+				StringBuffer buf = new StringBuffer();
+				if (isSimpleType()) {
+					if (!this.type1.equalsIgnoreCase("string")) { //$NON-NLS-1$
+						buf.append(":").append(this.type1); //$NON-NLS-1$
+					}
+					buf.append("=").append(this.valueObject.toString()); //$NON-NLS-1$
+				} else {
+					buf.append(":").append(this.type1); //$NON-NLS-1$
+					if (!this.type2.equalsIgnoreCase("string")) { //$NON-NLS-1$
+						buf.append(":").append(this.type2); //$NON-NLS-1$
+					}
+					buf.append("="); //$NON-NLS-1$
+					Object[] arr = (Object[]) (isArray() ? valueObject : ((Collection) valueObject).toArray());
+					for (int i = 0; i < arr.length; i++) {
+						buf.append(arr[i].toString());
+						if (i != (arr.length - 1)) {
+							buf.append(","); //$NON-NLS-1$
+						}
+					}
+				}
+				this.valueString = buf.toString();
+			}
+			return valueString;
+		}
+	}
+
+	synchronized static int getNextInteger() {
+		if (nextInt == Integer.MAX_VALUE) {
+			nextInt = 0;
+		}
+		return ++nextInt;
+	}
+
+	synchronized static long getNextLong() {
+		if (nextLong == Long.MAX_VALUE) {
+			nextLong = 0;
+		}
+		return ++nextLong;
+	}
+
+	synchronized static short getNextShort() {
+		if (nextShort == Short.MAX_VALUE) {
+			nextShort = 0;
+		}
+		return ++nextShort;
+	}
+
+	synchronized static byte getNextByte() {
+		if (nextByte == Byte.MAX_VALUE) {
+			nextByte = 0;
+		}
+		return ++nextByte;
+	}
+
+	/**
+	 * Create empty EDEFProperties instance.
+	 */
+	public EDEFProperties() {
+	}
+
+	/**
+	 * Create EDEFProperties instance initialized with all the given properties
+	 * @param properties must not be <code>null</code>
+	 */
+	public EDEFProperties(Map<String, Object> properties) {
+		putAllEDEFProperties(properties);
+	}
+
+	@Override
+	public Object put(Object key, Object value) {
+		if (key instanceof String && value instanceof String) {
+			EDEFPropertiesValue oldValue = (EDEFPropertiesValue) this.get(key);
+			EDEFPropertiesValue newValue = new EDEFPropertiesValue((String) value);
+			return super.put(key, (oldValue == null) ? newValue : oldValue.addPropertyValue(newValue));
+		}
+		return super.put(key, value);
+	}
+
+	/**
+	 * Put String->Object relation in as an EDEF property. Both the key and value
+	 * must not be <code>null</code>. The value must be either a Set, List, Array
+	 * type or a primitive type: Long/long, Byte/byte, Short/short, Int/Integer,
+	 * char/Character, double/Double, float/Float. If array,set, or list, the
+	 * elements must be one of the primitive types.
+	 * 
+	 * @param key   unique name/key for given value. Must not be <code>null</code>.
+	 * @param value array,list,set of primitive type or instance of primitive type.
+	 * @return existing Object with name/key. Null if no existing object exists.
+	 */
+	public Object putEDEFProperty(String key, Object value) {
+		if (key != null && value != null) {
+			return super.put(key, new EDEFPropertiesValue(value));
+		}
+		return null;
+	}
+
+	/**
+	 * Get EDEF properties as String -> Object map
+	 * 
+	 * @return Map<String,Object> containing all name->EDEFPropertiesValue contents
+	 *         of this EDEFProperties
+	 */
+	public Map<String, Object> getEDEFPropertiesAsMap() {
+		Map<String, Object> result = new TreeMap<String, Object>();
+		this.forEach((k, v) -> {
+			result.put((String) k, ((EDEFPropertiesValue) v).getValueObject());
+		});
+		return result;
+	}
+
+	private static void writeComments0(BufferedWriter bw, String comments) throws IOException {
+		bw.write("#"); //$NON-NLS-1$
+		int len = comments.length();
+		int current = 0;
+		int last = 0;
+		char[] uu = new char[6];
+		uu[0] = '\\';
+		uu[1] = 'u';
+		while (current < len) {
+			char c = comments.charAt(current);
+			if (c > '\u00ff' || c == '\n' || c == '\r') {
+				if (last != current)
+					bw.write(comments.substring(last, current));
+				if (c > '\u00ff') {
+					uu[2] = toHex((c >> 12) & 0xf);
+					uu[3] = toHex((c >> 8) & 0xf);
+					uu[4] = toHex((c >> 4) & 0xf);
+					uu[5] = toHex(c & 0xf);
+					bw.write(new String(uu));
+				} else {
+					bw.newLine();
+					if (c == '\r' && current != len - 1 && comments.charAt(current + 1) == '\n') {
+						current++;
+					}
+					if (current == len - 1
+							|| (comments.charAt(current + 1) != '#' && comments.charAt(current + 1) != '!'))
+						bw.write("#"); //$NON-NLS-1$
+				}
+				last = current + 1;
+			}
+			current++;
+		}
+		if (last != current)
+			bw.write(comments.substring(last, current));
+		bw.newLine();
+	}
+
+	private static final char[] hexDigit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
+			'F' };
+
+	private static char toHex(int nibble) {
+		return hexDigit[(nibble & 0xF)];
+	}
+
+	/**
+	 * Load EDEF properties from the given input stream
+	 * 
+	 * @param ins InputStream to read the edef properties from. Must not be
+	 *            <code>null</code>
+	 * @throws IOException if properties cannot be read from given InputStream
+	 */
+	public synchronized void loadEDEFProperties(InputStream ins) throws IOException {
+		load(ins);
+	}
+
+	/**
+	 * Load EDEF properties from the given reader
+	 * 
+	 * @param reader Reader to read the edef properties from. Must not be
+	 *               <code>null</code>
+	 * @throws IOException if properties cannot be read from given Reader
+	 */
+	public void loadEDEFProperties(Reader reader) throws IOException {
+		load(reader);
+	}
+
+	/**
+	 * Store EDEF properties to given Writer
+	 * 
+	 * @param writer   the Writer to write output to. Must not be <code>null</code>.
+	 * @param comments
+	 * @throws IOException
+	 */
+	public void storeEDEFProperties(Writer writer, String comments) throws IOException {
+		storeEDEFProperties((writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer),
+				comments);
+	}
+
+	/**
+	 * Store EDEF properties to the given buffered writer.
+	 * 
+	 * @param bufferedWriter the BufferedWriter to write to. Must not be
+	 *                       <code>null</code>
+	 * @param comments       Comment line prepended to properties file output. May
+	 *                       be <code>null</code>.
+	 * @throws IOException if properties cannot be written to bufferedWriter
+	 */
+	public void storeEDEFProperties(BufferedWriter bufferedWriter, String comments) throws IOException {
+		if (comments != null) {
+			writeComments0(bufferedWriter, comments);
+		}
+		bufferedWriter.write("#" + new Date().toString()); //$NON-NLS-1$
+		bufferedWriter.newLine();
+		synchronized (this) {
+			for (Enumeration<?> e = keys(); e.hasMoreElements();) {
+				Object k = e.nextElement();
+				if (k instanceof String) {
+					String key = (String) k;
+					Object elemValue = get(key);
+					if (elemValue instanceof EDEFPropertiesValue) {
+						bufferedWriter.write(key + ((EDEFPropertiesValue) elemValue).getEDEFPropertyValueString());
+						bufferedWriter.newLine();
+					}
+				}
+			}
+			bufferedWriter.flush();
+		}
+	}
+
+	/**
+	 * Put all the given properties into this map as EDEF properties, suitable for
+	 * storing via {@link #storeEDEFProperties(Writer, String)}
+	 * 
+	 * @param properties the properties to put. May not be <code>null</code>
+	 */
+	public synchronized void putAllEDEFProperties(Map<String, Object> properties) {
+		properties.forEach((k, v) -> {
+			if (k instanceof String) {
+				putEDEFProperty((String) k, v);
+			}
+		});
+	}
+
+}
\ No newline at end of file
diff --git a/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EndpointDescriptionLocator.java b/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EndpointDescriptionLocator.java
index 8cc2ea3..0b0b6b6 100644
--- a/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EndpointDescriptionLocator.java
+++ b/osgi/bundles/org.eclipse.ecf.osgi.services.remoteserviceadmin/src/org/eclipse/ecf/osgi/services/remoteserviceadmin/EndpointDescriptionLocator.java
@@ -15,10 +15,10 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.reflect.Array;
-import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
@@ -29,12 +29,12 @@
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.UUID;
 
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
@@ -1082,32 +1082,6 @@
 	/**
 	 * @since 4.8
 	 */
-	protected static class EDEFProperties extends Properties {
-
-		private static final long serialVersionUID = -7351470248095230347L;
-
-		@Override
-		public synchronized Object put(Object key, Object value) {
-			String keyStr = (String) key;
-			String valueStr = (String) value;
-			EDEFPropertyValue propValue = null;
-			if (keyStr != null && valueStr != null) {
-				EDEFPropertyValue newValue = new EDEFPropertyValue(valueStr);
-				EDEFPropertyValue oldValue = (EDEFPropertyValue) this.get(keyStr);
-				if (oldValue == null) {
-					propValue = newValue;
-				} else {
-					propValue = oldValue.addPropertyValue(newValue);
-				}
-				super.put(keyStr, propValue);
-			}
-			return null;
-		}
-	}
-
-	/**
-	 * @since 4.8
-	 */
 	protected EDEFProperties loadProperties(URL url) throws IOException {
 		EDEFProperties result = new EDEFProperties();
 		try (InputStream ins = url.openStream()) {
@@ -1119,324 +1093,83 @@
 	/**
 	 * @since 4.8
 	 */
-	public static class EDEFPropertyValue {
-		private static int nextInt = 0;
-		private static long nextLong = 0;
-		private static short nextShort = 0;
-		private static byte nextByte = 0;
-		private String type1;
-		private String type2;
-		private String value;
-		private Object resultValue;
 
-		synchronized static int getNextInteger() {
-			if (nextInt == Integer.MAX_VALUE) {
-				nextInt = 0;
-			}
-			return ++nextInt;
-		}
-
-		boolean isArray() {
-			return this.type1.equalsIgnoreCase("array"); //$NON-NLS-1$
-		}
-
-		boolean isSet() {
-			return this.type1.equalsIgnoreCase("set"); //$NON-NLS-1$
-		}
-
-		boolean isList() {
-			return this.type1.equalsIgnoreCase("list"); //$NON-NLS-1$
-		}
-
-		boolean isCollection() {
-			return isSet() || isList();
-		}
-
-		boolean isSimpleType() {
-			return !isSet() && !isList() && !isArray();
-		}
-
-		boolean hasTypeAgreement(EDEFPropertyValue otherValue) {
-			String otherType = (otherValue.isArray() || otherValue.isCollection()) ? otherValue.type2
-					: otherValue.type1;
-			return (isArray() || isCollection()) ? this.type2.equalsIgnoreCase(otherType)
-					: this.type1.equalsIgnoreCase(otherType);
-		}
-
-		EDEFPropertyValue addPropertyValue(EDEFPropertyValue newValue) {
-			if (!hasTypeAgreement(newValue)) {
-				LogUtility.logError("addEDEFPropertyValue", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
-						"type disagreement between property values for old type1=" + this.type1 + ",type2=" + this.type2 //$NON-NLS-1$ //$NON-NLS-2$
-								+ ", and new type1=" + newValue.type1 + ",type2=" + newValue.type2); //$NON-NLS-1$ //$NON-NLS-2$
-				return this;
-			}
-			// get old and new values
-			Object ov = this.getValue();
-			Object nv = newValue.getValue();
-			// If we've got a collection already
-			if (isCollection()) {
-				Collection oldVs = (Collection) ov;
-				// If newValue is also collection
-				if (newValue.isCollection()) {
-					// Then addAll
-					oldVs.addAll((Collection) nv);
-				} else if (newValue.isSimpleType()) {
-					// Add single element
-					oldVs.add(nv);
-				}
-			} else if (isArray()) {
-				// Get old length
-				int oldLength = Array.getLength(ov);
-				Object newResultValue = null;
-				if (newValue.isArray()) {
-					// new value is also array...get length
-					int newLength = Array.getLength(nv);
-					// Check to make sure that the type of elements is same (type2 for arrays)
-					newResultValue = Array.newInstance(ov.getClass().getComponentType(), oldLength + newLength);
-					// copy ov contents to newResultValue
-					System.arraycopy(ov, 0, newResultValue, 0, oldLength);
-					// append nv contents to newResultValue after ov contents
-					System.arraycopy(nv, 0, newResultValue, oldLength, newLength);
-				} else if (newValue.isSimpleType()) {
-					newResultValue = Array.newInstance(ov.getClass().getComponentType(), oldLength + 1);
-					System.arraycopy(ov, 0, newResultValue, 0, oldLength);
-					Array.set(newResultValue, oldLength, nv);
-				}
-				if (newResultValue != null) {
-					this.resultValue = newResultValue;
-				}
-			}
-			return this;
-		}
-
-		synchronized static long getNextLong() {
-			if (nextLong == Long.MAX_VALUE) {
-				nextLong = 0;
-			}
-			return ++nextLong;
-		}
-
-		synchronized static short getNextShort() {
-			if (nextShort == Short.MAX_VALUE) {
-				nextShort = 0;
-			}
-			return ++nextShort;
-		}
-
-		synchronized static byte getNextByte() {
-			if (nextByte == Byte.MAX_VALUE) {
-				nextByte = 0;
-			}
-			return ++nextByte;
-		}
-
-		public EDEFPropertyValue(String value) {
-			// Default type1 is String
-			this.type1 = "String"; //$NON-NLS-1$
-			// Default type2 is String also
-			this.type2 = "String"; //$NON-NLS-1$
-			// Split value with =
-			String[] valueArr = value.split("="); //$NON-NLS-1$
-			// Split first one
-			if (valueArr.length > 1) {
-				// split second element in valueArr by :
-				String[] firstSplit = valueArr[0].split(":"); //$NON-NLS-1$
-				// If more than one then type2 is second element in firstSplit
-				if (firstSplit.length > 1) {
-					this.type2 = firstSplit[1];
-				}
-				// In either case type1 is firstSplit[0]
-				this.type1 = firstSplit[0];
-			}
-			// Now set value to the last elemtn in the valueArr
-			this.value = valueArr[valueArr.length - 1];
-		}
-
-		private Object getSimpleValue(Class<?> simpleType, Object value) {
-			try {
-				return simpleType.getDeclaredMethod("valueOf", new Class[] { String.class }).invoke(null, value); //$NON-NLS-1$
-			} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException
-					| NoSuchMethodException | SecurityException e) {
-				LogUtility.logWarning("getSimpleValue", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, this.getClass(), //$NON-NLS-1$
-						"Cannot create instance of simpleType=" + simpleType + ", value=" + value); //$NON-NLS-1$ //$NON-NLS-2$
-				return null;
-			}
-		}
-
-		boolean isUnique() {
-			return "unique".equals(this.type2); //$NON-NLS-1$
-		}
-
-		Object getSimpleValue(String simpleType, String value) {
-			switch (simpleType) {
-			case "long": //$NON-NLS-1$
-			case "Long": //$NON-NLS-1$
-				if ("unique".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
-					return getNextLong();
-				} else if ("nanoTime".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
-					return System.nanoTime();
-				} else if ("milliTime".equalsIgnoreCase(this.type2)) { //$NON-NLS-1$
-					return System.currentTimeMillis();
-				}
-				return getSimpleValue(Long.class, value);
-			case "double": //$NON-NLS-1$
-			case "Double": //$NON-NLS-1$
-				return getSimpleValue(Double.class, value);
-			case "float": //$NON-NLS-1$
-			case "Float": //$NON-NLS-1$
-				return getSimpleValue(Float.class, value);
-			case "int": //$NON-NLS-1$
-			case "Integer": //$NON-NLS-1$
-				if ("unique".equals(this.type2)) { //$NON-NLS-1$
-					return getNextInteger();
-				}
-				return getSimpleValue(Integer.class, value);
-			case "Byte": //$NON-NLS-1$
-			case "byte": //$NON-NLS-1$
-				if ("unique".equals(this.type2)) { //$NON-NLS-1$
-					return getNextByte();
-				}
-				return getSimpleValue(Byte.class, value);
-			case "char": //$NON-NLS-1$
-			case "Character": //$NON-NLS-1$
-				return getSimpleValue(Character.class, value.toCharArray()[0]);
-			case "boolean": //$NON-NLS-1$
-			case "Boolean": //$NON-NLS-1$
-				return getSimpleValue(Boolean.class, value);
-			case "short": //$NON-NLS-1$
-			case "Short": //$NON-NLS-1$
-				if ("unique".equals(this.type2)) { //$NON-NLS-1$
-					return getNextShort();
-				}
-				return getSimpleValue(Short.class, value);
-			case "uuid": //$NON-NLS-1$
-			case "Uuid": //$NON-NLS-1$
-			case "UUID": //$NON-NLS-1$
-				// we don't care whether 'unique' is given or not
-				return UUID.randomUUID().toString();
-			case "String": //$NON-NLS-1$
-			case "string": //$NON-NLS-1$
-				return value;
-			default:
-				return null;
-			}
-		}
-
-		Object getArrayValues(String collectionValue) {
-			String[] elements = this.value.split("\\s*,\\s*"); //$NON-NLS-1$
-			Object result = null;
-			for (int i = 0; i < elements.length; i++) {
-				Object elementValue = getSimpleValue(this.type2, elements[i]);
-				if (elementValue == null) {
-					LogUtility.logWarning("getArrayValues", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
-							"array element=" + elements[i] + " could not be created"); //$NON-NLS-1$//$NON-NLS-2$
-					continue;
-				} else {
-					if (i == 0) {
-						result = Array.newInstance(elementValue.getClass(), elements.length);
-					}
-					Array.set(result, i, elementValue);
-				}
-			}
-			return result;
-		}
-
-		Collection<Object> getCollectionValues(Collection<Object> c, String collectionValue) {
-			String[] elements = this.value.split("\\s*,\\s*"); //$NON-NLS-1$
-			for (String element : elements) {
-				Object elementValue = getSimpleValue(this.type2, element);
-				if (elementValue == null) {
-					LogUtility.logWarning("getCollectionValues", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
-							"array element=" + element + " could not be created"); //$NON-NLS-1$//$NON-NLS-2$
-					continue;
-				} else {
-					c.add(elementValue);
-				}
-			}
-			return c;
-		}
-
-		public synchronized Object getValue() {
-			if (this.resultValue == null) {
-				switch (this.type1) {
-				case "array": //$NON-NLS-1$
-					this.resultValue = getArrayValues(this.value);
-					break;
-				case "list": //$NON-NLS-1$
-					this.resultValue = getCollectionValues(new ArrayList(), this.value);
-					break;
-				case "set": //$NON-NLS-1$
-					this.resultValue = getCollectionValues(new HashSet(), this.value);
-					break;
-				default:
-					this.resultValue = getSimpleValue(this.type1, this.value);
-					break;
-				}
-			}
-			return this.resultValue;
-		}
+	/**
+	 * @since 4.8
+	 */
+	protected Map<String, Object> processProperties(EDEFProperties props) {
+		return props.getEDEFPropertiesAsMap();
 	}
 
 	/**
 	 * @since 4.8
 	 */
-	protected Map<String, Object> processProperties(Properties props) {
-		Map<String, Object> result = new HashMap<String, Object>();
-		if (props == null) {
-			return result;
+	protected Map<String, Object> loadDefaultProperties(Map<String, Object> props, URL url) {
+		try {
+			props = PropertiesUtil.mergePropertiesRaw(props, processProperties(loadProperties(url)));
+			trace("loadDefaultProperties", "loaded default.properties file=" + url.getFile() //$NON-NLS-1$ //$NON-NLS-2$
+					+ " loaded properties=" //$NON-NLS-1$
+					+ props);
+		} catch (IOException e) {
+			LogUtility.logWarning("findOverrideProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
+					"Could not load default properties=" + url); //$NON-NLS-1$
 		}
-		props.forEach((k, v) -> {
-			String name = (String) k;
-			Object value = ((EDEFPropertyValue) v).getValue();
-			if (value != null) {
-				result.put(name, value);
-			} else {
-				LogUtility.logWarning("processProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
-						"Invalid EDEFPropertyValue for name=" + name + ". Not added to processed properties"); //$NON-NLS-1$ //$NON-NLS-2$
+		return props;
+	}
+
+	/**
+	 * @since 4.8
+	 */
+	protected Map<String, Object> loadAllDefaultProperties(URL url) {
+		Map<String, Object> props = new TreeMap<String, Object>();
+		URL rootUrl = null;
+		try {
+			rootUrl = new URL(url.getProtocol(), url.getHost(), url.getPort(), "/"); //$NON-NLS-1$
+		} catch (MalformedURLException e) {
+			LogUtility.logError("loadAllDefaultProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
+					"MalformedUrlException creating rootUrl from url=" + url); //$NON-NLS-1$
+			return props;
+		}
+		String pathSegment = ""; //$NON-NLS-1$
+		Iterator<Path> pathIterator = Paths.get(url.getPath()).iterator();
+		do {
+			String newPath = pathSegment + "/" + DEFAULT_PROPERTIES_FILE; //$NON-NLS-1$
+			try {
+				props = loadDefaultProperties(props, new URL(rootUrl, newPath));
+			} catch (MalformedURLException e) {
+				LogUtility.logError("loadAllDefaultProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
+						"MalformedUrlException creating rootUrl from url=" + url); //$NON-NLS-1$
+				return props;
 			}
-		});
-		return result;
+			pathSegment = pathSegment + "/" + pathIterator.next(); //$NON-NLS-1$
+		} while (pathIterator.hasNext());
+		return props;
 	}
 
 	/**
 	 * @since 4.7
 	 */
 	protected Map<String, Object> findOverrideProperties(Bundle bundle, URL fileURL) {
-		Map<String, Object> mergedProps = new HashMap<String, Object>();
-		URL defaultPropsFileURL = getDefaultPropsURLFromEDFileURL(fileURL);
-		if (defaultPropsFileURL != null) {
-			trace("handleEndpointDescriptionFile", //$NON-NLS-1$
-					"Attempting to load default.properties.  BundleId=" + bundle.getBundleId() + " defaultPropsFileURL=" //$NON-NLS-1$ //$NON-NLS-2$
-							+ defaultPropsFileURL);
-			try {
-				mergedProps = PropertiesUtil.mergePropertiesRaw(mergedProps,
-						processProperties(loadProperties(defaultPropsFileURL)));
-				trace("findOverrideProperties", "loaded default.properties file=" + defaultPropsFileURL.getFile() //$NON-NLS-1$ //$NON-NLS-2$
-						+ " properties loaded=" //$NON-NLS-1$
-						+ mergedProps);
-			} catch (IOException e) {
-				LogUtility.logWarning("findOverrideProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
-						"Could not load default properties file=" + defaultPropsFileURL + ",edef fileUrl=" //$NON-NLS-1$ //$NON-NLS-2$
-								+ fileURL.getFile());
-			}
-		}
+		Map<String, Object> defaultProperties = loadAllDefaultProperties(fileURL);
+		trace("findOverrideProperties", "merged default.properties=" + defaultProperties); //$NON-NLS-1$ //$NON-NLS-2$
+		Map<String, Object> overrideProps = new HashMap<String, Object>();
 		URL propsFileURL = getPropsURLFromEDFileURL(fileURL);
 		if (propsFileURL != null) {
 			trace("handleEndpointDescriptionFile", //$NON-NLS-1$
 					"Attemping to load <file>.properties.  BundleId=" + bundle.getBundleId() + " propsFileURL=" //$NON-NLS-1$ //$NON-NLS-2$
 							+ propsFileURL);
 			try {
-				mergedProps = PropertiesUtil.mergePropertiesRaw(mergedProps,
+				overrideProps = PropertiesUtil.mergePropertiesRaw(defaultProperties,
 						processProperties(loadProperties(propsFileURL)));
 				trace("findOverrideProperties", //$NON-NLS-1$
 						"loaded override properties file=" + fileURL.getFile() + " merged Properties=" //$NON-NLS-1$ //$NON-NLS-2$
-								+ mergedProps);
+								+ overrideProps);
 			} catch (IOException e) {
 				LogUtility.logWarning("findOverrideProperties", DebugOptions.ENDPOINT_DESCRIPTION_LOCATOR, getClass(), //$NON-NLS-1$
 						"Could not load properties fileUrl=" + propsFileURL + ",fileUrl=" + fileURL.getFile()); //$NON-NLS-1$ //$NON-NLS-2$
 			}
 		}
-		return (!mergedProps.isEmpty()) ? mergedProps : null;
+		return (!overrideProps.isEmpty()) ? overrideProps : null;
 	}
 
 	EndpointDescription findED(IServiceID serviceID) {