diff options
5 files changed, 465 insertions, 12 deletions
diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java index 195a7eec..31e670a0 100644 --- a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java +++ b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/MessageTypeDispatcher.java @@ -36,7 +36,7 @@ public class MessageTypeDispatcher extends JsonListener { /** * Name of the message field, which may be hierarchical. */ - private final String[] messageFieldNames; + private final Object[] messageFieldNames; /** * Name of the message field, joined with "." - for logging. diff --git a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/RequestIdDispatcher.java b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/RequestIdDispatcher.java index fcf9c9a9..e4686c1e 100644 --- a/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/RequestIdDispatcher.java +++ b/policy-endpoints/src/main/java/org/onap/policy/common/endpoints/listeners/RequestIdDispatcher.java @@ -43,7 +43,7 @@ public class RequestIdDispatcher<T> extends ScoListener<T> { /** * Name of the request id field, which may be hierarchical. */ - private final String[] requestIdFieldNames; + private final Object[] requestIdFieldNames; /** * Listeners for autonomous messages. diff --git a/utils/src/main/java/org/onap/policy/common/utils/properties/PropertyObjectUtils.java b/utils/src/main/java/org/onap/policy/common/utils/properties/PropertyObjectUtils.java new file mode 100644 index 00000000..996f1b87 --- /dev/null +++ b/utils/src/main/java/org/onap/policy/common/utils/properties/PropertyObjectUtils.java @@ -0,0 +1,243 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.properties; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utilities for generating POJOs from Properties. + */ +public class PropertyObjectUtils { + + public static final Logger logger = LoggerFactory.getLogger(PropertyObjectUtils.class); + private static final Pattern NAME_PAT = Pattern.compile("\\[(\\d{1,3})\\]$"); + + private PropertyObjectUtils() { + // do nothing + } + + /** + * Converts a set of properties to a Map. Supports json-path style property names with + * "." separating components, where components may have an optional subscript. + * + * @param properties properties to be converted + * @param prefix properties whose names begin with this prefix are included. The + * prefix is stripped from the name before adding the value to the map + * @return a hierarchical map representing the properties + */ + public static Map<String, Object> toObject(Properties properties, String prefix) { + String dottedPrefix = prefix + (prefix.isEmpty() || prefix.endsWith(".") ? "" : "."); + int pfxlen = dottedPrefix.length(); + + Map<String, Object> map = new LinkedHashMap<>(); + + for (String name : properties.stringPropertyNames()) { + if (name.startsWith(dottedPrefix)) { + String[] components = name.substring(pfxlen).split("[.]"); + setProperty(map, components, properties.getProperty(name)); + } + } + + return map; + } + + /** + * Sets a property within a hierarchical map. + * + * @param map map into which the value should be placed + * @param names property name components + * @param value value to be placed into the map + */ + private static void setProperty(Map<String, Object> map, String[] names, String value) { + Map<String, Object> node = map; + + final int lastComp = names.length - 1; + + // process all but the final component + for (int comp = 0; comp < lastComp; ++comp) { + node = getNode(node, names[comp]); + } + + // process the final component + String name = names[lastComp]; + Matcher matcher = NAME_PAT.matcher(name); + + if (!matcher.find()) { + // no subscript + node.put(name, value); + return; + } + + // subscripted + List<Object> array = getArray(node, name.substring(0, matcher.start())); + int index = Integer.parseInt(matcher.group(1)); + expand(array, index); + array.set(index, value); + } + + /** + * Gets a node. + * + * @param map map from which to get the object + * @param name name of the element to get from the map, with an optional subscript + * @return a Map + */ + @SuppressWarnings("unchecked") + private static Map<String, Object> getNode(Map<String, Object> map, String name) { + Matcher matcher = NAME_PAT.matcher(name); + + if (!matcher.find()) { + // no subscript + return getObject(map, name); + } + + // subscripted + List<Object> array = getArray(map, name.substring(0, matcher.start())); + int index = Integer.parseInt(matcher.group(1)); + expand(array, index); + + Object item = array.get(index); + if (item instanceof Map) { + return (Map<String, Object>) item; + + } else { + LinkedHashMap<String, Object> result = new LinkedHashMap<>(); + array.set(index, result); + return result; + } + } + + /** + * Ensures that an array's size is large enough to hold the specified element. + * + * @param array array to be expanded + * @param index index of the desired element + */ + private static void expand(List<Object> array, int index) { + while (array.size() <= index) { + array.add(null); + } + } + + /** + * Gets an object (i.e., Map) from a map. If the particular element is not a Map, then + * it is replaced with an empty Map. + * + * @param map map from which to get the object + * @param name name of the element to get from the map, without any subscript + * @return a Map + */ + private static Map<String, Object> getObject(Map<String, Object> map, String name) { + @SuppressWarnings("unchecked") + Map<String, Object> result = (Map<String, Object>) map.compute(name, (key, value) -> { + if (value instanceof Map) { + return value; + } else { + return new LinkedHashMap<>(); + } + }); + + return result; + } + + /** + * Gets an array from a map. If the particular element is not an array, then it is + * replaced with an empty array. + * + * @param map map from which to get the array + * @param name name of the element to get from the map, without any subscript + * @return an array + */ + private static List<Object> getArray(Map<String, Object> map, String name) { + @SuppressWarnings("unchecked") + List<Object> result = (List<Object>) map.compute(name, (key, value) -> { + if (value instanceof List) { + return value; + } else { + return new ArrayList<>(); + } + }); + + return result; + } + + /** + * Compresses lists contained within a generic object, removing all {@code null} + * items. + * + * @param object object to be compressed + * @return the original object, modified in place + */ + public static Object compressLists(Object object) { + if (object instanceof Map) { + @SuppressWarnings("unchecked") + Map<String, Object> asMap = (Map<String, Object>) object; + compressMapValues(asMap); + + } else if (object instanceof List) { + @SuppressWarnings("unchecked") + List<Object> asList = (List<Object>) object; + compressListItems(asList); + } + + // else: ignore anything else + + return object; + } + + /** + * Walks a hierarchical map and removes {@code null} items found in any Lists. + * + * @param map map whose lists are to be compressed + */ + private static void compressMapValues(Map<String, Object> map) { + for (Object value : map.values()) { + compressLists(value); + } + } + + /** + * Removes {@code null} items from the list. In addition, it walks the items within + * the list, compressing them, as well. + * + * @param list the list to be compressed + */ + private static void compressListItems(List<Object> list) { + Iterator<Object> iter = list.iterator(); + while (iter.hasNext()) { + Object item = iter.next(); + if (item == null) { + // null item - remove it + iter.remove(); + + } else { + compressLists(item); + } + } + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/properties/PropertyObjectUtilsTest.java b/utils/src/test/java/org/onap/policy/common/utils/properties/PropertyObjectUtilsTest.java new file mode 100644 index 00000000..83b264f5 --- /dev/null +++ b/utils/src/test/java/org/onap/policy/common/utils/properties/PropertyObjectUtilsTest.java @@ -0,0 +1,212 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2019-2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.common.utils.properties; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import org.junit.Test; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.common.utils.coder.StandardCoder; + +public class PropertyObjectUtilsTest { + + @Test + public void testToObject() { + Map<String, String> map = Map.of("abc", "def", "ghi", "jkl"); + Properties props = new Properties(); + props.putAll(map); + + // with empty prefix + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + assertEquals(map, result); + + // with dotted prefix - other items skipped + map = Map.of("pfx.abc", "def", "ghi", "jkl", "pfx.mno", "pqr", "differentpfx.stu", "vwx"); + props.clear(); + props.putAll(Map.of("pfx.abc", "def", "ghi", "jkl", "pfx.mno", "pqr", "differentpfx.stu", "vwx")); + result = PropertyObjectUtils.toObject(props, "pfx."); + map = Map.of("abc", "def", "mno", "pqr"); + assertEquals(map, result); + + // undotted prefix - still skips other items + result = PropertyObjectUtils.toObject(props, "pfx"); + assertEquals(map, result); + } + + @Test + public void testSetProperty() { + // one, two, and three components in the name, the last two with subscripts + Map<String, Object> map = Map.of("one", "one.abc", "two.def", "two.ghi", "three.jkl.mno[0]", "three.pqr", + "three.jkl.mno[1]", "three.stu"); + Properties props = new Properties(); + props.putAll(map); + + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + // @formatter:off + map = Map.of( + "one", "one.abc", + "two", Map.of("def", "two.ghi"), + "three", Map.of("jkl", + Map.of("mno", + List.of("three.pqr", "three.stu")))); + // @formatter:on + assertEquals(map, result); + } + + @Test + public void testGetNode() { + Map<String, Object> map = Map.of("abc[0].def", "node.ghi", "abc[0].jkl", "node.mno", "abc[1].def", "node.pqr"); + Properties props = new Properties(); + props.putAll(map); + + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + // @formatter:off + map = Map.of( + "abc", + List.of( + Map.of("def", "node.ghi", "jkl", "node.mno"), + Map.of("def", "node.pqr") + )); + // @formatter:on + assertEquals(map, result); + + } + + @Test + public void testExpand() { + // add subscripts out of order + Properties props = makeProperties("abc[2]", "expand.def", "abc[1]", "expand.ghi"); + + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + // @formatter:off + Map<String,Object> map = + Map.of("abc", + Arrays.asList(null, "expand.ghi", "expand.def")); + // @formatter:on + assertEquals(map, result); + + } + + @Test + public void testGetObject() { + // first value is primitive, while second is a map + Properties props = makeProperties("object.abc", "object.def", "object.abc.ghi", "object.jkl"); + + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + // @formatter:off + Map<String,Object> map = + Map.of("object", + Map.of("abc", + Map.of("ghi", "object.jkl"))); + // @formatter:on + assertEquals(map, result); + } + + @Test + public void testGetArray() { + // first value is primitive, while second is an array + Properties props = makeProperties("array.abc", "array.def", "array.abc[0].ghi", "array.jkl"); + + Map<String, Object> result = PropertyObjectUtils.toObject(props, ""); + // @formatter:off + Map<String,Object> map = + Map.of("array", + Map.of("abc", + List.of( + Map.of("ghi", "array.jkl")))); + // @formatter:on + assertEquals(map, result); + } + + @Test + @SuppressWarnings("unchecked") + public void testCompressLists() throws IOException, CoderException { + assertEquals("plain-string", PropertyObjectUtils.compressLists("plain-string").toString()); + + // @formatter:off + Map<String, Object> map = + Map.of( + "cmp.abc", "cmp.def", + "cmp.ghi", + Arrays.asList(null, "cmp.list1", null, "cmp.list2", + Map.of("cmp.map", Arrays.asList("cmp.map.list1", "cmp.map1.list2", null)))); + // @formatter:on + + // the data structure needs to be modifiable, so we'll encode/decode it + StandardCoder coder = new StandardCoder(); + map = coder.decode(coder.encode(map), LinkedHashMap.class); + + PropertyObjectUtils.compressLists(map); + + // @formatter:off + Map<String, Object> expected = + Map.of( + "cmp.abc", "cmp.def", + "cmp.ghi", + Arrays.asList("cmp.list1", "cmp.list2", + Map.of("cmp.map", Arrays.asList("cmp.map.list1", "cmp.map1.list2")))); + // @formatter:on + assertEquals(expected, map); + } + + /** + * Makes properties containing the specified key/value pairs. The property set returns + * names in the order listed. + * + * @return a new properties containing the specified key/value pairs + */ + private Properties makeProperties(String key1, String value1, String key2, String value2) { + // control the order in which the names are returned + List<String> keyList = List.of(key1, key2); + + Set<String> keySet = new AbstractSet<>() { + @Override + public Iterator<String> iterator() { + return keyList.iterator(); + } + + @Override + public int size() { + return 2; + } + }; + + Properties props = new Properties() { + private static final long serialVersionUID = 1L; + + @Override + public Set<String> stringPropertyNames() { + return keySet; + } + }; + + props.putAll(Map.of(key1, value1, key2, value2)); + + return props; + } +} diff --git a/utils/src/test/java/org/onap/policy/common/utils/properties/exception/SupportBasicPropertyExceptionTester.java b/utils/src/test/java/org/onap/policy/common/utils/properties/exception/SupportBasicPropertyExceptionTester.java index 97503f8c..f5842923 100644 --- a/utils/src/test/java/org/onap/policy/common/utils/properties/exception/SupportBasicPropertyExceptionTester.java +++ b/utils/src/test/java/org/onap/policy/common/utils/properties/exception/SupportBasicPropertyExceptionTester.java @@ -7,9 +7,9 @@ * Licensed 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. @@ -20,11 +20,9 @@ package org.onap.policy.common.utils.properties.exception; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; - -import org.hamcrest.CoreMatchers; /** * Superclass used to test subclasses of {@link PropertyException}. @@ -52,7 +50,7 @@ public class SupportBasicPropertyExceptionTester { protected static final String FIELD = "PROPERTY"; /* - * Methods to perform various tests on the except subclass. + * Methods to perform various tests on the except subclass. */ protected void doTestPropertyExceptionStringField_AllPopulated(PropertyException ex) { @@ -98,7 +96,7 @@ public class SupportBasicPropertyExceptionTester { /** * Performs standard tests that should apply to all subclasses. - * + * * @param ex exception to test */ protected void standardTests(PropertyException ex) { @@ -111,17 +109,17 @@ public class SupportBasicPropertyExceptionTester { /** * Performs standard tests for exceptions that were provided a message in their * constructor. - * + * * @param ex exception to test */ protected void standardMessageTests(PropertyException ex) { - assertThat(ex.getMessage(), CoreMatchers.endsWith(MESSAGE)); + assertThat(ex.getMessage()).endsWith(MESSAGE); } /** * Performs standard tests for exceptions that were provided a throwable in their * constructor. - * + * * @param ex exception to test */ protected void standardThrowableTests(PropertyException ex) { |