This is an automated email from the ASF dual-hosted git repository. rmerriman pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/metron.git
The following commit(s) were added to refs/heads/master by this push: new 8792882 METRON-2061 Solr documents with date fields cannot be updated with Dao classes (merrimanr) closes apache/metron#1374 8792882 is described below commit 87928824509c5f6ab314375be720dd64d8361cf4 Author: merrimanr <merrim...@gmail.com> AuthorDate: Tue Jul 2 08:47:27 2019 -0500 METRON-2061 Solr documents with date fields cannot be updated with Dao classes (merrimanr) closes apache/metron#1374 --- .../metron/indexing/dao/update/PatchException.java | 27 +++ .../metron/indexing/dao/update/PatchOperation.java | 28 +++ .../metron/indexing/dao/update/PatchUtils.java | 105 ++++++++ .../metron/indexing/dao/update/UpdateDao.java | 3 +- .../metron/indexing/dao/update/PatchUtilsTest.java | 263 +++++++++++++++++++++ 5 files changed, 424 insertions(+), 2 deletions(-) diff --git a/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchException.java b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchException.java new file mode 100644 index 0000000..c621cfa --- /dev/null +++ b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchException.java @@ -0,0 +1,27 @@ +/** + * 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.metron.indexing.dao.update; + +public class PatchException extends RuntimeException { + + public PatchException(String message) { + super(message); + } +} diff --git a/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchOperation.java b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchOperation.java new file mode 100644 index 0000000..9b0e92d --- /dev/null +++ b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchOperation.java @@ -0,0 +1,28 @@ +/** + * 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.metron.indexing.dao.update; + +enum PatchOperation { + ADD, + REPLACE, + REMOVE, + COPY, + MOVE, + TEST; +} diff --git a/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchUtils.java b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchUtils.java new file mode 100644 index 0000000..e5c5117 --- /dev/null +++ b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/PatchUtils.java @@ -0,0 +1,105 @@ +/** + * 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.metron.indexing.dao.update; + +import java.util.*; + +import static org.apache.metron.indexing.dao.update.PatchOperation.*; + +public enum PatchUtils { + INSTANCE; + + public static final String OP = "op"; + public static final String VALUE = "value"; + public static final String PATH = "path"; + public static final String FROM = "from"; + private static final String PATH_SEPARATOR = "/"; + + public Map<String, Object> applyPatch(List<Map<String, Object>> patches, Map<String, Object> source) { + Map<String, Object> patchedObject = new HashMap<>(source); + for(Map<String, Object> patch: patches) { + + // parse patch request parameters + String operation = (String) patch.get(OP); + PatchOperation patchOperation; + try { + patchOperation = PatchOperation.valueOf(operation.toUpperCase()); + } catch(IllegalArgumentException e) { + throw new UnsupportedOperationException(String.format("The %s operation is not supported", operation)); + } + + Object value = patch.get(VALUE); + String path = (String) patch.get(PATH); + + // locate the nested object + List<String> fieldNames = getFieldNames(path); + String nestedFieldName = fieldNames.get(fieldNames.size() - 1); + Map<String, Object> nestedObject = getNestedObject(fieldNames, patchedObject); + + // apply the patch operation + if (ADD.equals(patchOperation) || REPLACE.equals(patchOperation)) { + nestedObject.put(nestedFieldName, value); + } else if (REMOVE.equals(patchOperation)) { + nestedObject.remove(nestedFieldName); + } else if (COPY.equals(patchOperation) || MOVE.equals(patchOperation)) { + + // locate the nested object to copy/move the value from + String from = (String) patch.get(FROM); + List<String> fromFieldNames = getFieldNames(from); + String fromNestedFieldName = fromFieldNames.get(fromFieldNames.size() - 1); + Map<String, Object> fromNestedObject = getNestedObject(fromFieldNames, patchedObject); + + // copy the value + Object copyValue = fromNestedObject.get(fromNestedFieldName); + nestedObject.put(nestedFieldName, copyValue); + if (MOVE.equals(patchOperation)) { + + // remove the from value in case of a move + nestedObject.remove(fromNestedFieldName); + } + } else if (TEST.equals(patchOperation)) { + + Object testValue = nestedObject.get(nestedFieldName); + if (!Objects.equals(value, testValue)) { + throw new PatchException(String.format("TEST operation failed: supplied value [%s] != target value [%s]", value, testValue)); + } + } + } + return patchedObject; + } + + private List<String> getFieldNames(String path) { + String[] parts = path.split(PATH_SEPARATOR); + return new ArrayList<>(Arrays.asList(parts).subList(1, parts.length)); + } + + @SuppressWarnings("unchecked") + private Map<String, Object> getNestedObject(List<String> fieldNames, Map<String, Object> patchedObject) { + Map<String, Object> nestedObject = patchedObject; + for(int i = 0; i < fieldNames.size() - 1; i++) { + Object object = nestedObject.get(fieldNames.get(i)); + if (object == null || !(object instanceof Map)) { + throw new IllegalArgumentException(String.format("Invalid path: /%s", String.join(PATH_SEPARATOR, fieldNames))); + } else { + nestedObject = (Map<String, Object>) object; + } + } + return nestedObject; + } +} diff --git a/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/UpdateDao.java b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/UpdateDao.java index ef1d298..efbbb04 100644 --- a/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/UpdateDao.java +++ b/metron-platform/metron-indexing/metron-indexing-common/src/main/java/org/apache/metron/indexing/dao/update/UpdateDao.java @@ -17,7 +17,6 @@ */ package org.apache.metron.indexing.dao.update; -import org.apache.metron.common.utils.JSONUtils; import org.apache.metron.indexing.dao.RetrieveLatestDao; import java.io.IOException; @@ -95,7 +94,7 @@ public interface UpdateDao { } } - Map<String, Object> patchedSource = JSONUtils.INSTANCE.applyPatch(request.getPatch(), originalSource); + Map<String, Object> patchedSource = PatchUtils.INSTANCE.applyPatch(request.getPatch(), originalSource); return new Document(patchedSource, guid, sensorType, timestamp, documentID); } } diff --git a/metron-platform/metron-indexing/metron-indexing-common/src/test/java/org/apache/metron/indexing/dao/update/PatchUtilsTest.java b/metron-platform/metron-indexing/metron-indexing-common/src/test/java/org/apache/metron/indexing/dao/update/PatchUtilsTest.java new file mode 100644 index 0000000..b2bb173 --- /dev/null +++ b/metron-platform/metron-indexing/metron-indexing-common/src/test/java/org/apache/metron/indexing/dao/update/PatchUtilsTest.java @@ -0,0 +1,263 @@ +/** + * 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.metron.indexing.dao.update; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PatchUtilsTest { + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void addOperationShouldAddValue() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.ADD.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, "value"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", "value"); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<>())); + } + + @Test + public void removeOperationShouldRemoveValue() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.REMOVE.name()); + put(PatchUtils.PATH, "/remove/path"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", "value"); + put("remove", new HashMap<>()); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", "value"); + put("remove", new HashMap<String, Object>() {{ + put("path", "removeValue"); + }}); + }})); + } + + @Test + public void copyOperationShouldCopyValue() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.COPY.name()); + put(PatchUtils.FROM, "/from"); + put(PatchUtils.PATH, "/path"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("from", "value"); + put("path", "value"); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("from", "value"); + }})); + } + + @Test + public void copyOperationShouldCopyNestedValue() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.COPY.name()); + put(PatchUtils.FROM, "/nested/from"); + put(PatchUtils.PATH, "/nested/path"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("nested", new HashMap<String, Object>() {{ + put("from", "value"); + put("path", "value"); + }}); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("nested", new HashMap<String, Object>() {{ + put("from", "value"); + }}); + }})); + } + + @Test + public void moveOperationShouldMoveValue() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.MOVE.name()); + put(PatchUtils.FROM, "/from"); + put(PatchUtils.PATH, "/path"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", "value"); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("from", "value"); + }})); + } + + @Test + public void testOperationShouldCompareStrings() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.TEST.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, "value"); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", "value"); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", "value"); + }})); + } + + @Test + public void testOperationShouldCompareNumbers() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.TEST.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, 100); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", 100); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", 100); + }})); + } + + @Test + public void testOperationShouldCompareArrays() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.TEST.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, Arrays.asList(1, 2, 3)); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", Arrays.asList(1, 2, 3)); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", Arrays.asList(1, 2, 3)); + }})); + } + + @Test + public void testOperationShouldCompareObjects() { + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.TEST.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, new HashMap<String, Object>() {{ + put("key", "value"); + }}); + }}); + + Map<String, Object> expected = new HashMap<String, Object>() {{ + put("path", new HashMap<String, Object>() {{ + put("key", "value"); + }}); + }}; + + Assert.assertEquals(expected, PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", new HashMap<String, Object>() {{ + put("key", "value"); + }}); + }})); + } + + @Test + public void testOperationShouldThrowExceptionOnFailedCompare() { + exception.expect(PatchException.class); + exception.expectMessage("TEST operation failed: supplied value [value1] != target value [value2]"); + + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.TEST.name()); + put(PatchUtils.PATH, "/path"); + put(PatchUtils.VALUE, "value1"); + }}); + + PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", "value2"); + }}); + } + + @Test + public void shouldThrowExceptionOnInvalidPath() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("Invalid path: /missing/path"); + + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, PatchOperation.REMOVE.name()); + put(PatchUtils.PATH, "/missing/path"); + }}); + + PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", "value"); + }}); + + } + + @Test + public void shouldThrowExceptionOnInvalidOperation() { + exception.expect(UnsupportedOperationException.class); + exception.expectMessage("The invalid operation is not supported"); + + List<Map<String, Object>> patches = new ArrayList<>(); + patches.add(new HashMap<String, Object>() {{ + put(PatchUtils.OP, "invalid"); + put(PatchUtils.PATH, "/path"); + }}); + + PatchUtils.INSTANCE.applyPatch(patches, new HashMap<String, Object>() {{ + put("path", "value"); + }}); + + } +}