/* Copyright 2013 Red Hat, Inc. and/or its affiliates. This file is part of lightblue. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.redhat.lightblue.hooks; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Iterator; import java.util.function.Consumer; import javax.management.RuntimeErrorException; import java.util.HashMap; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.redhat.lightblue.metadata.HookConfiguration; import com.redhat.lightblue.metadata.EntityMetadata; import com.redhat.lightblue.metadata.TypeResolver; import com.redhat.lightblue.metadata.PredefinedFields; import com.redhat.lightblue.metadata.Hook; import com.redhat.lightblue.metadata.parser.Extensions; import com.redhat.lightblue.metadata.parser.JSONMetadataParser; import com.redhat.lightblue.metadata.types.DefaultTypes; import com.redhat.lightblue.TestDataStoreParser; import com.redhat.lightblue.query.Projection; import com.redhat.lightblue.query.FieldProjection; import com.redhat.lightblue.crud.CRUDOperationContext; import com.redhat.lightblue.crud.CRUDOperation; import com.redhat.lightblue.crud.Factory; import com.redhat.lightblue.crud.DocCtx; import com.redhat.lightblue.crud.ListDocumentStream; import com.redhat.lightblue.crud.DocumentStream; import com.redhat.lightblue.util.test.AbstractJsonNodeTest; import com.redhat.lightblue.util.JsonDoc; import com.redhat.lightblue.util.Path; public class HookManagerTest extends AbstractJsonNodeTest { private static final JsonNodeFactory nodeFactory = JsonNodeFactory.withExactBigDecimals(false); public static class TestOperationContext extends CRUDOperationContext { EntityMetadata md; public TestOperationContext(EntityMetadata md, CRUDOperation op, Factory f, List<JsonDoc> docs) { super(op, "test", f, docs, null); this.md = md; if (CRUDOperation.UPDATE.equals(op) || CRUDOperation.DELETE.equals(op)) { // for update and delete setup the original document so pre isn't null in hooks for (DocCtx dc : getInputDocuments()) { dc.startModifications(); } } } @Override public EntityMetadata getEntityMetadata(String name) { return md; } } public static abstract class AbstractHook implements CRUDHook { private final String name; EntityMetadata md; HookConfiguration cfg; List<HookDoc> processed; public AbstractHook(String n) { name = n; } @Override public String getName() { return name; } @Override public void processHook(EntityMetadata md, HookConfiguration cfg, List<HookDoc> processedDocuments) { this.md = md; this.cfg = cfg; this.processed = processedDocuments; } } public static class TestHook1Config implements HookConfiguration { } public static class TestHook1 extends AbstractHook { public TestHook1() { super("hook1"); } } public static class TestHook2Config implements HookConfiguration { } public static class TestHook2 extends AbstractHook { public TestHook2() { super("hook2"); } } public static class TestMediatorHookConfig implements HookConfiguration { } public static class TestMediatorHook extends AbstractHook implements MediatorHook { public TestMediatorHook() { super("MH"); } } public static class TestErrorHook extends AbstractHook { public TestErrorHook() { super("EH"); } @Override public void processHook(EntityMetadata md, HookConfiguration cfg, List<HookDoc> processedDocuments) { throw new RuntimeErrorException(null, "Processing of other hooks should continue."); } } public static class TestErrorHookConfig implements HookConfiguration { } private TestHook1 hook1; private TestHook2 hook2; private TestMediatorHook mediatorHook; private TestErrorHook errorHook; private HookResolver resolver; public static class TestHookResolver implements HookResolver { Map<String, CRUDHook> map = new HashMap<>(); public TestHookResolver(CRUDHook... h) { for (CRUDHook x : h) { map.put(x.getName(), x); } } @Override public CRUDHook getHook(String name) { return map.get(name); } } @Before public void setup() { hook1 = new TestHook1(); hook2 = new TestHook2(); mediatorHook = new TestMediatorHook(); errorHook = new TestErrorHook(); resolver = new TestHookResolver(hook1, hook2, mediatorHook, errorHook); } private EntityMetadata getMD(String fname) throws Exception { JsonNode node = loadJsonNode(fname); Extensions<JsonNode> extensions = new Extensions<>(); extensions.addDefaultExtensions(); extensions.registerDataStoreParser("mongo", new TestDataStoreParser<JsonNode>()); TypeResolver typeResolver = new DefaultTypes(); JSONMetadataParser parser = new JSONMetadataParser(extensions, typeResolver, nodeFactory); EntityMetadata md = parser.parseEntityMetadata(node); PredefinedFields.ensurePredefinedFields(md); return md; } private void addHook(EntityMetadata md, String name, Projection projection, HookConfiguration config, String... actions) { Hook hook = new Hook(name); hook.setProjection(projection); hook.setConfiguration(config); for (String a : actions) { switch (a) { case "insert": hook.setInsert(true); break; case "update": hook.setUpdate(true); break; case "find": hook.setFind(true); break; case "delete": hook.setDelete(true); break; } } List<Hook> l = md.getHooks().getHooks(); l.add(hook); md.getHooks().setHooks(l); } private List<JsonDoc> getSomeDocs(int n) throws Exception { List<JsonDoc> ret = new ArrayList<>(); for (int i = 0; i < n; i++) { JsonNode node = loadJsonNode("./sample1.json"); JsonDoc doc = new JsonDoc(node); doc.modify(new Path("field1"), nodeFactory.textNode("field" + i), false); ret.add(doc); } return ret; } private TestOperationContext setupContext(CRUDOperation op) throws Exception { EntityMetadata md = getMD("./testMetadata.json"); // error hook gets processed first so we can be sure the rest continue addHook(md, "EH", null, new TestErrorHookConfig(), "update"); addHook(md, "hook1", null, new TestHook1Config(), "insert", "update"); addHook(md, "hook2", null, new TestHook2Config(), "find", "delete", "update"); addHook(md, "MH", null, new TestMediatorHookConfig(), "insert", "update", "delete"); TestOperationContext ctx = new TestOperationContext(md, op, new Factory(), getSomeDocs(10)); for (DocCtx doc : ctx.getInputDocuments()) { doc.setCRUDOperationPerformed(op); } return ctx; } @Test public void crudInsertQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.INSERT); // Only hook1 should be called hooks.queueHooks(ctx); hooks.callQueuedHooks(); Assert.assertEquals(ctx.md, hook1.md); Assert.assertTrue(hook1.cfg instanceof TestHook1Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook1.processed.size()); Assert.assertNull(hook2.md); Assert.assertNull(mediatorHook.md); } @Test public void crudUpdateQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.UPDATE); // hook1 and hook2 should be called hooks.queueHooks(ctx); hooks.callQueuedHooks(); Assert.assertEquals(ctx.md, hook1.md); Assert.assertTrue(hook1.cfg instanceof TestHook1Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook1.processed.size()); for (HookDoc doc : hook1.processed) { Assert.assertNotNull(doc.getPreDoc()); Assert.assertNotNull(doc.getPostDoc()); Assert.assertEquals(CRUDOperation.UPDATE, doc.getCRUDOperation()); } Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook2.processed.size()); Assert.assertNull(mediatorHook.md); for (HookDoc doc : hook2.processed) { Assert.assertNotNull(doc.getPreDoc()); Assert.assertNotNull(doc.getPostDoc()); Assert.assertEquals(CRUDOperation.UPDATE, doc.getCRUDOperation()); } } @Test public void crudDeleteQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.DELETE); // hook2 should be called hooks.queueHooks(ctx); hooks.callQueuedHooks(); Assert.assertNull(hook1.md); Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook2.processed.size()); Assert.assertNull(mediatorHook.md); } @Test public void crudFindQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.FIND); // hook2 should be called hooks.queueHooks(ctx); hooks.callQueuedHooks(); Assert.assertNull(hook1.md); Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook2.processed.size()); Assert.assertNull(mediatorHook.md); } private static class NonRewindableDocumentStream<T> implements DocumentStream<T> { private final List<T> documents; private Iterator<T> itr; private final ArrayList<Consumer<T>> listeners=new ArrayList<>(); public NonRewindableDocumentStream(List<T> list) { this.documents=list; } @Override public boolean hasNext() { if(itr==null) itr=documents.iterator(); return itr.hasNext(); } @Override public T next() { if(itr==null) itr=documents.iterator(); T doc=itr.next(); for(Consumer<T> c:listeners) c.accept(doc); return doc; } @Override public void close() {} @Override public void addListener(Consumer<T> listener) { listeners.add(listener); } } @Test public void crudFindQueueTest_deferredProcessing() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.FIND); ctx.setDocumentStream(new NonRewindableDocumentStream<DocCtx>(ctx.getInputDocuments())); // hook2 should be called hooks.queueHooks(ctx); // Suck docs while(ctx.getDocumentStream().hasNext()) ctx.getDocumentStream().next(); hooks.callQueuedHooks(); Assert.assertNull(hook1.md); Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook2.processed.size()); Assert.assertNull(mediatorHook.md); } @Test public void crudMixedQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.FIND); ctx.getInputDocuments().get(0).setCRUDOperationPerformed(CRUDOperation.INSERT); ctx.getInputDocuments().get(1).setCRUDOperationPerformed(CRUDOperation.UPDATE); ctx.getInputDocuments().get(2).setCRUDOperationPerformed(CRUDOperation.DELETE); hooks.queueHooks(ctx); hooks.callQueuedHooks(); Assert.assertEquals(ctx.md, hook1.md); Assert.assertTrue(hook1.cfg instanceof TestHook1Config); Assert.assertEquals(2, hook1.processed.size()); Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(9, hook2.processed.size()); Assert.assertNull(mediatorHook.md); } @Test public void mediatorMixedQueueTest() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.FIND); ctx.getInputDocuments().get(0).setCRUDOperationPerformed(CRUDOperation.INSERT); ctx.getInputDocuments().get(1).setCRUDOperationPerformed(CRUDOperation.UPDATE); ctx.getInputDocuments().get(2).setCRUDOperationPerformed(CRUDOperation.DELETE); hooks.queueMediatorHooks(ctx); hooks.callQueuedHooks(); Assert.assertNull(hook1.md); Assert.assertNull(hook2.md); Assert.assertEquals(ctx.md, mediatorHook.md); Assert.assertTrue(mediatorHook.cfg instanceof TestMediatorHookConfig); Assert.assertEquals(3, mediatorHook.processed.size()); } @Test public void projectionTestInsert() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.INSERT); // Add projection to one of the hooks for (Hook h : ctx.md.getHooks().getHooks()) { if (h.getName().equals("hook1")) { h.setProjection(new FieldProjection(new Path("field1"), true, false)); } } hooks.queueHooks(ctx); hooks.callQueuedHooks(); // hook1 only should have field1 projected Assert.assertEquals(ctx.md, hook1.md); Assert.assertTrue(hook1.cfg instanceof TestHook1Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook1.processed.size()); for (HookDoc h : hook1.processed) { Assert.assertNull(h.getPreDoc()); Assert.assertTrue(h.getPostDoc().get(new Path("field1")) != null); Assert.assertTrue(h.getPostDoc().get(new Path("field2")) == null); } } @Test public void projectionTestUpdate() throws Exception { HookManager hooks = new HookManager(resolver, nodeFactory); TestOperationContext ctx = setupContext(CRUDOperation.UPDATE); // Add projection to one of the hooks for (Hook h : ctx.md.getHooks().getHooks()) { if (h.getName().equals("hook1")) { h.setProjection(new FieldProjection(new Path("field1"), true, false)); } } hooks.queueHooks(ctx); hooks.callQueuedHooks(); // hook1 only should have field1 projected Assert.assertEquals(ctx.md, hook1.md); Assert.assertTrue(hook1.cfg instanceof TestHook1Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook1.processed.size()); for (HookDoc h : hook1.processed) { Assert.assertNotNull(h.getPreDoc()); Assert.assertTrue(h.getPostDoc().get(new Path("field1")) != null); Assert.assertTrue(h.getPostDoc().get(new Path("field2")) == null); } // hook2 should have field1 and others Assert.assertEquals(ctx.md, hook2.md); Assert.assertTrue(hook2.cfg instanceof TestHook2Config); Assert.assertEquals(ctx.getInputDocuments().size(), hook2.processed.size()); for (HookDoc h : hook2.processed) { Assert.assertNotNull(h.getPreDoc()); Assert.assertTrue(h.getPostDoc().get(new Path("field1")) != null); Assert.assertTrue(h.getPostDoc().get(new Path("field2")) != null); } } }