diff --git a/api/pom.xml b/api/pom.xml
index 3011f5222a..3583772cd7 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -85,6 +85,7 @@
fluent-hc
4.5.6
+
org.vivoweb
vitro-dependencies
@@ -111,9 +112,15 @@
test
- org.easymock
- easymock
- 3.5
+ org.mockito
+ mockito-core
+ 4.3.1
+ test
+
+
+ org.mockito
+ mockito-inline
+ 4.3.1
test
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/AbstractPool.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/AbstractPool.java
new file mode 100644
index 0000000000..8a23a4e0b5
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/AbstractPool.java
@@ -0,0 +1,203 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import static edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary.RDF_TYPE;
+import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.FULL_UNION;
+import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri;
+import static java.lang.String.format;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import javax.servlet.ServletContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jena.ontology.OntModel;
+import org.apache.jena.rdf.model.NodeIterator;
+import org.apache.jena.rdf.model.Property;
+import org.apache.jena.rdf.model.RDFNode;
+import org.apache.jena.rdf.model.Resource;
+
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Poolable;
+import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess;
+import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader;
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoaderException;
+
+public abstract class AbstractPool, P extends Pool> implements Pool {
+
+ private final Log log = LogFactory.getLog(this.getClass());
+
+ private static final Object mutex = new Object();
+
+ private ConcurrentNavigableMap components;
+ private ServletContext ctx;
+ private ConfigurationBeanLoader loader;
+ private ContextModelAccess modelAccess;
+ private OntModel dynamicAPIModel;
+ private ConcurrentLinkedQueue obsoleteComponents;
+
+ protected AbstractPool() {
+ components = new ConcurrentSkipListMap<>();
+ obsoleteComponents = new ConcurrentLinkedQueue<>();
+ }
+
+ protected ConcurrentNavigableMap getComponents() {
+ return components;
+ }
+
+ public abstract P getPool();
+
+ public abstract C getDefault();
+
+ public abstract Class getType();
+
+ public C get(K key) {
+ C component = components.get(key);
+
+ if (component == null) {
+ return getDefault();
+ }
+
+ component.addClient();
+ return component;
+ }
+
+ public void printKeys() {
+ for (Map.Entry entry : components.entrySet()) {
+ log.debug(format("%s in pool: '%s'", getType().getName(), entry.getKey()));
+ }
+ }
+
+ public void add(String uri, C component) {
+ K key = component.getKey();
+ log.info(format("Adding component %s with URI %s", key, uri));
+ if (isInModel(uri)) {
+ synchronized (mutex) {
+ C oldComponent = components.put(key, component);
+ if (oldComponent != null) {
+ obsoleteComponents.add(oldComponent);
+ unloadObsoleteComponents();
+ }
+ }
+ } else {
+ throw new RuntimeException(format("%s %s with URI %s not found in model. Not adding to pool.",
+ getType().getName(), key, uri));
+ }
+ }
+
+ public void remove(String uri, K key) {
+ log.info(format("Removing component %s with URI %s", key, uri));
+ if (!isInModel(uri)) {
+ synchronized (mutex) {
+ C oldComponent = components.remove(key);
+ if (oldComponent != null) {
+ obsoleteComponents.add(oldComponent);
+ unloadObsoleteComponents();
+ }
+ }
+ } else {
+ throw new RuntimeException(format("%s %s with URI %s still exists in model. Not removing from pool.",
+ getType().getName(), key, uri));
+ }
+ }
+
+ private boolean isInModel(String uri) {
+ Resource s = dynamicAPIModel.getResource(uri);
+ Property p = dynamicAPIModel.getProperty(RDF_TYPE);
+
+ String javaUri = toJavaUri(getType());
+
+ NodeIterator nit = dynamicAPIModel.listObjectsOfProperty(s, p);
+ while (nit.hasNext()) {
+ RDFNode node = nit.next();
+ if (node.isResource() && node.toString().replace("#", ".").equals(javaUri)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void reload(String uri) {
+ try {
+ add(uri, loader.loadInstance(uri, getType()));
+ } catch (ConfigurationBeanLoaderException e) {
+ throw new RuntimeException(format("Failed to reload %s with URI %s.", getType().getName(), uri));
+ }
+ }
+
+ public synchronized void reload() {
+ if (ctx == null) {
+ log.error(format("Context is null. Can't reload %s.", this.getClass().getName()));
+ return;
+ }
+ if (loader == null) {
+ log.error(format("Loader is null. Can't reload %s.", this.getClass().getName()));
+ return;
+ }
+ ConcurrentNavigableMap newActions = new ConcurrentSkipListMap<>();
+ loadComponents(newActions);
+ ConcurrentNavigableMap oldActions = this.components;
+ components = newActions;
+ for (Map.Entry component : oldActions.entrySet()) {
+ obsoleteComponents.add(component.getValue());
+ oldActions.remove(component.getKey());
+ }
+ unloadObsoleteComponents();
+ }
+
+ public void init(ServletContext ctx) {
+ this.ctx = ctx;
+ modelAccess = ModelAccess.on(ctx);
+ dynamicAPIModel = modelAccess.getOntModel(FULL_UNION);
+ loader = new ConfigurationBeanLoader(dynamicAPIModel, ctx);
+ log.debug("Context Initialization ...");
+ loadComponents(components);
+ }
+
+ private void loadComponents(ConcurrentNavigableMap components) {
+ Set newActions = loader.loadEach(getType());
+ log.debug(format("Context Initialization. %s %s(s) currently loaded.", components.size(), getType().getName()));
+ for (C component : newActions) {
+ if (component.isValid()) {
+ components.put(component.getKey(), component);
+ } else {
+ log.error(format("%s with rpcName %s is invalid.", getType().getName(), component.getKey()));
+ }
+ }
+ log.debug(format("Context Initialization finished. %s %s(s) loaded.", components.size(), getType().getName()));
+ }
+
+ private void unloadObsoleteComponents() {
+ for (C component : obsoleteComponents) {
+ if (!isComponentInUse(component)) {
+ component.dereference();
+ obsoleteComponents.remove(component);
+ }
+ }
+ }
+
+ private boolean isComponentInUse(C component) {
+ if (!component.hasClients()) {
+ return false;
+ }
+ component.removeDeadClients();
+ if (!component.hasClients()) {
+ return false;
+ }
+ return true;
+ }
+
+ public long obsoleteCount() {
+ return obsoleteComponents.size();
+ }
+
+ public long count() {
+ return components.size();
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPool.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPool.java
index 66472ce959..633fcff567 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPool.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPool.java
@@ -1,99 +1,29 @@
package edu.cornell.mannlib.vitro.webapp.dynapi;
-import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.FULL_UNION;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import javax.servlet.ServletContext;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.jena.ontology.OntModel;
-
import edu.cornell.mannlib.vitro.webapp.dynapi.components.Action;
import edu.cornell.mannlib.vitro.webapp.dynapi.components.DefaultAction;
-import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess;
-import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
-import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader;
-public class ActionPool {
+public class ActionPool extends AbstractPool {
- private static ActionPool INSTANCE = null;
- private static final Log log = LogFactory.getLog(ActionPool.class);
-
- private Map actions;
- private ServletContext ctx;
- private ConfigurationBeanLoader loader;
- private ContextModelAccess modelAccess;
- private OntModel dynamicAPIModel;
+ private static ActionPool INSTANCE = new ActionPool();
- private ActionPool(){
- this.actions = new HashMap();
- INSTANCE = this;
- }
-
public static ActionPool getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new ActionPool();
- }
return INSTANCE;
}
-
- public Action getByName(String name) {
- Action action = actions.get(name);
- if (action == null) {
- action = new DefaultAction();
- }
- return action;
- }
-
- public void printActionNames() {
- for (Map.Entry entry : actions.entrySet()) {
- log.debug("Action in pool: '" + entry.getKey() + "'");
- }
- }
-
- public void reload() {
- if (ctx == null ) {
- log.error("Context is null. Can't reload action pool.");
- return;
- }
- if (loader == null ) {
- log.error("Loader is null. Can't reload action pool.");
- return;
- }
- for (Map.Entry entry : actions.entrySet()) {
- entry.getValue().dereference();
- }
- actions.clear();
- loadActions();
- }
- public void init(ServletContext ctx) {
- this.ctx = ctx;
- modelAccess = ModelAccess.on(ctx);
- dynamicAPIModel = modelAccess.getOntModel(FULL_UNION);
- loader = new ConfigurationBeanLoader( dynamicAPIModel, ctx);
- log.debug("Context Initialization ...");
- loadActions();
+ @Override
+ public ActionPool getPool() {
+ return getInstance();
}
- private void loadActions() {
- Set actions = loader.loadEach(Action.class);
- log.debug("Context Initialization. actions loaded: " + actions.size());
- for (Action action : actions) {
- if (action.isValid()) {
- add(action);
- } else {
- log.error("Action with rpcName " + action.getName() + " is invalid.");
- }
- }
- log.debug("Context Initialization finished. " + actions.size() + " actions loaded.");
+ @Override
+ public Action getDefault() {
+ return new DefaultAction();
}
- private void add(Action action) {
- actions.put(action.getName(), action);
+ @Override
+ public Class getType() {
+ return Action.class;
}
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/OperationData.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/OperationData.java
index 911ab55a11..5313ad1c3d 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/OperationData.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/OperationData.java
@@ -7,8 +7,8 @@
public class OperationData {
- private Map params;
- private ServletContext context;
+ private final Map params;
+ private final ServletContext context;
public OperationData(HttpServletRequest request) {
params = request.getParameterMap();
@@ -20,10 +20,7 @@ public ServletContext getContext() {
}
public boolean has(String paramName) {
- if (params.containsKey(paramName)) {
- return true;
- }
- return false;
+ return params.containsKey(paramName);
}
public String[] get(String paramName) {
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/Pool.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/Pool.java
new file mode 100644
index 0000000000..e07d27c031
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/Pool.java
@@ -0,0 +1,27 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import javax.servlet.ServletContext;
+
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Poolable;
+
+public interface Pool> {
+
+ public C get(K key);
+
+ public void printKeys();
+
+ public void add(String uri, C component);
+
+ public void remove(String uri, K key);
+
+ public void reload();
+
+ public void reload(String uri);
+
+ public void init(ServletContext ctx);
+
+ public long obsoleteCount();
+
+ public long count();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpoint.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpoint.java
index ebd1b4ab4b..53ceddfb44 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpoint.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpoint.java
@@ -1,5 +1,8 @@
package edu.cornell.mannlib.vitro.webapp.dynapi;
+import static edu.cornell.mannlib.vitro.webapp.dynapi.request.RequestPath.REST_BASE_PATH;
+import static java.lang.String.format;
+
import java.io.IOException;
import javax.servlet.ServletException;
@@ -11,53 +14,112 @@
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Action;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.OperationResult;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Resource;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.ResourceKey;
+import edu.cornell.mannlib.vitro.webapp.dynapi.request.RequestPath;
+
+@WebServlet(name = "RESTEndpoint", urlPatterns = { REST_BASE_PATH + "/*" })
+public class RESTEndpoint extends VitroHttpServlet {
-@WebServlet(name = "RESTEndpoint", urlPatterns = { "/api/rest/*" })
-public class RESTEndpoint extends VitroHttpServlet {
+ private static final Log log = LogFactory.getLog(RESTEndpoint.class);
- private static final long serialVersionUID = 1L;
- private static final Log log = LogFactory.getLog(RESTEndpoint.class);
private ResourcePool resourcePool = ResourcePool.getInstance();
-
-
+ private ActionPool actionPool = ActionPool.getInstance();
+
@Override
- public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
- if (request.getMethod().equalsIgnoreCase("PATCH")){
- doPatch(request, response);
- } else {
- super.service(request, response);
- }
+ public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ if (request.getMethod().equalsIgnoreCase("PATCH")) {
+ doPatch(request, response);
+ } else {
+ super.service(request, response);
+ }
}
-
+
@Override
- public void doPost( HttpServletRequest request, HttpServletResponse response ) {
+ public void doPost(HttpServletRequest request, HttpServletResponse response) {
process(request, response);
}
-
+
@Override
- public void doGet( HttpServletRequest request, HttpServletResponse response ) {
+ public void doGet(HttpServletRequest request, HttpServletResponse response) {
process(request, response);
}
-
+
@Override
- public void doDelete( HttpServletRequest request, HttpServletResponse response ) {
+ public void doDelete(HttpServletRequest request, HttpServletResponse response) {
process(request, response);
}
-
+
@Override
- public void doPut( HttpServletRequest request, HttpServletResponse response ) {
+ public void doPut(HttpServletRequest request, HttpServletResponse response) {
process(request, response);
}
-
- public void doPatch( HttpServletRequest request, HttpServletResponse response ) {
+
+ public void doPatch(HttpServletRequest request, HttpServletResponse response) {
process(request, response);
}
-
+
private void process(HttpServletRequest request, HttpServletResponse response) {
- String requestURL = request.getRequestURI();
- //String resourceName = requestURL.substring(requestURL.lastIndexOf("/") + 1 );
- //Resource resource = resourcePool.getByName(resourceName);
- log.debug(request.getMethod());
+ RequestPath requestPath = RequestPath.from(request);
+
+ if (requestPath.isValid()) {
+ ResourceKey resourceKey = ResourceKey.of(requestPath.getResourceName(), requestPath.getResourceVersion());
+
+ if (log.isDebugEnabled()) {
+ resourcePool.printKeys();
+ }
+ Resource resource = resourcePool.get(resourceKey);
+ String method = request.getMethod();
+
+ String actionName = null;
+
+ if (requestPath.isCustomRestAction()) {
+ if (method.toUpperCase().equals("POST")) {
+ String customRestActionName = requestPath.getActionName();
+
+ try {
+ actionName = resource.getCustomRestActionByName(customRestActionName);
+ } catch (UnsupportedOperationException e) {
+ log.error(format("Custom REST action %s not implemented for resource %s", customRestActionName,
+ resource.getKey()), e);
+ OperationResult.notImplemented().prepareResponse(response);
+ return;
+ } finally {
+ resource.removeClient();
+ }
+ } else {
+ OperationResult.notImplemented().prepareResponse(response);
+ resource.removeClient();
+ return;
+ }
+ } else {
+ try {
+ actionName = resource.getActionNameByMethod(method);
+ } catch (UnsupportedOperationException e) {
+ log.error(format("Method %s not implemented for resource %s", method, resource.getKey()), e);
+ OperationResult.notImplemented().prepareResponse(response);
+ return;
+ } finally {
+ resource.removeClient();
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ actionPool.printKeys();
+ }
+ Action action = actionPool.get(actionName);
+ OperationData input = new OperationData(request);
+ try {
+ OperationResult result = action.run(input);
+ result.prepareResponse(response);
+ } finally {
+ action.removeClient();
+ }
+ } else {
+ OperationResult.notFound().prepareResponse(response);
+ }
}
-
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpoint.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpoint.java
index ee31f35be3..f3ca8b7f31 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpoint.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpoint.java
@@ -1,5 +1,7 @@
package edu.cornell.mannlib.vitro.webapp.dynapi;
+import static edu.cornell.mannlib.vitro.webapp.dynapi.request.RequestPath.RPC_BASE_PATH;
+
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -10,42 +12,48 @@
import edu.cornell.mannlib.vitro.webapp.controller.VitroHttpServlet;
import edu.cornell.mannlib.vitro.webapp.dynapi.components.Action;
import edu.cornell.mannlib.vitro.webapp.dynapi.components.OperationResult;
+import edu.cornell.mannlib.vitro.webapp.dynapi.request.RequestPath;
+
+@WebServlet(name = "RPCEndpoint", urlPatterns = { RPC_BASE_PATH + "/*" })
+public class RPCEndpoint extends VitroHttpServlet {
-@WebServlet(name = "RPCEndpoint", urlPatterns = { "/api/rpc/*" })
-public class RPCEndpoint extends VitroHttpServlet {
+ private static final Log log = LogFactory.getLog(RESTEndpoint.class);
- private static final long serialVersionUID = 1L;
- private static final Log log = LogFactory.getLog(RPCEndpoint.class);
private ActionPool actionPool = ActionPool.getInstance();
-
+
@Override
- public void doGet( HttpServletRequest request, HttpServletResponse response ) {
- process(request, response);
+ public void doGet(HttpServletRequest request, HttpServletResponse response) {
+ OperationResult.notImplemented().prepareResponse(response);
}
-
+
@Override
- public void doPost( HttpServletRequest request, HttpServletResponse response ) {
- process(request, response);
+ public void doPost(HttpServletRequest request, HttpServletResponse response) {
+ RequestPath requestPath = RequestPath.from(request);
+ if (requestPath.isValid()) {
+ if (log.isDebugEnabled()) {
+ actionPool.printKeys();
+ }
+ Action action = actionPool.get(requestPath.getActionName());
+ OperationData input = new OperationData(request);
+ try {
+ OperationResult result = action.run(input);
+ result.prepareResponse(response);
+ } finally {
+ action.removeClient();
+ }
+ } else {
+ OperationResult.notFound().prepareResponse(response);
+ }
}
-
+
@Override
- public void doDelete( HttpServletRequest request, HttpServletResponse response ) {
- process(request, response);
+ public void doDelete(HttpServletRequest request, HttpServletResponse response) {
+ OperationResult.notImplemented().prepareResponse(response);
}
-
+
@Override
- public void doPut( HttpServletRequest request, HttpServletResponse response ) {
- process(request, response);
- }
-
- private void process(HttpServletRequest request, HttpServletResponse response) {
- String requestURL = request.getRequestURI();
- String actionName = requestURL.substring(requestURL.lastIndexOf("/") + 1 );
- log.debug(actionName);
- actionPool.printActionNames();
- Action action = actionPool.getByName(actionName);
- OperationData input = new OperationData(request);
- OperationResult result = action.run(input);
- result.prepareResponse(response);
+ public void doPut(HttpServletRequest request, HttpServletResponse response) {
+ OperationResult.notImplemented().prepareResponse(response);
}
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ResourcePool.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ResourcePool.java
index 2b7863ae14..ea118b6f78 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ResourcePool.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/ResourcePool.java
@@ -1,60 +1,30 @@
package edu.cornell.mannlib.vitro.webapp.dynapi;
-import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.FULL_UNION;
-
-import java.util.HashMap;
-import java.util.Set;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Resource;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.ResourceKey;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.DefaultResource;
-import javax.servlet.ServletContext;
+public class ResourcePool extends VersionableAbstractPool {
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-import org.apache.jena.ontology.OntModel;
+ private static ResourcePool INSTANCE = new ResourcePool();
-import edu.cornell.mannlib.vitro.webapp.dynapi.components.Resource;
-import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess;
-import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
-import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader;
-
-public class ResourcePool {
-
- private static final Log log = LogFactory.getLog(ResourcePool.class);
- private static ResourcePool INSTANCE;
- private ContextModelAccess modelAccess;
- private OntModel dynamicAPIModel;
- private ConfigurationBeanLoader loader;
- private HashMap resources;
- private ServletContext ctx;
-
- private ResourcePool() {
- this.resources = new HashMap();
- INSTANCE = this;
- }
public static ResourcePool getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new ResourcePool();
- }
return INSTANCE;
}
- public void init(ServletContext ctx) {
- this.ctx = ctx;
- modelAccess = ModelAccess.on(ctx);
- dynamicAPIModel = modelAccess.getOntModel(FULL_UNION);
- loader = new ConfigurationBeanLoader( dynamicAPIModel, ctx);
- loadResources();
+ @Override
+ public ResourcePool getPool() {
+ return getInstance();
}
-
- private void loadResources() {
- Set resources = loader.loadEach(Resource.class);
- log.debug("Context Initialization. resources created: " + resources.size());
- for (Resource resource : resources) {
- add(resource);
- }
- log.debug("Context Initialization finished. " + resources.size() + " resources loaded.");
+
+ @Override
+ public Resource getDefault() {
+ return new DefaultResource();
}
- private void add(Resource resource) {
- resources.put(resource.getName(), resource);
+ @Override
+ public Class getType() {
+ return Resource.class;
}
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/VersionableAbstractPool.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/VersionableAbstractPool.java
new file mode 100644
index 0000000000..f3afd49bdc
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/VersionableAbstractPool.java
@@ -0,0 +1,80 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import java.util.Map.Entry;
+
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Version;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Versionable;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Versioned;
+
+public abstract class VersionableAbstractPool, P extends Pool>
+ extends AbstractPool {
+
+ public C get(K key) {
+ C component = null;
+ Entry entry = getComponents().floorEntry(key);
+ if (entry != null) {
+ component = entry.getValue();
+ String keyName = key.getName();
+ Version keyVersion = key.getVersion();
+ // ensure key name matches component key name
+ if (keyName.equals(component.getKey().getName())) {
+ boolean hasVersionMax = component.getVersionMax() != null;
+ boolean hasVersionMin = component.getVersionMin() != null;
+ if (hasVersionMax) {
+ // ensure key version is not greater than component version max
+ if (greaterThanVersionMax(component, keyVersion)) {
+ component = null;
+ }
+ } else if (hasVersionMin) {
+ // ensure key version specific values are respected
+ if (mismatchSpecificVersion(component, keyVersion)) {
+ component = null;
+ }
+ }
+ } else {
+ component = null;
+ }
+ }
+
+ if (component == null) {
+ return getDefault();
+ }
+
+ component.addClient();
+ return component;
+ }
+
+ private boolean greaterThanVersionMax(C component, Version keyVersion) {
+ Version componentVersionMax = Version.of(component.getVersionMax());
+ boolean majorVersionGreater = keyVersion.getMajor().compareTo(componentVersionMax.getMajor()) > 0;
+
+ boolean minorVersionGreater = minorVersionSpecific(keyVersion)
+ && keyVersion.getMinor().compareTo(componentVersionMax.getMinor()) > 0;
+
+ boolean patchVersionGreater = patchVersionSpecific(keyVersion)
+ && keyVersion.getPatch().compareTo(componentVersionMax.getPatch()) > 0;
+
+ return majorVersionGreater || minorVersionGreater || patchVersionGreater;
+ }
+
+ private boolean mismatchSpecificVersion(C component, Version keyVersion) {
+ Version componentVersionMin = Version.of(component.getVersionMin());
+
+ boolean mismatchMinorVersion = minorVersionSpecific(keyVersion)
+ && !keyVersion.getMinor().equals(componentVersionMin.getMinor());
+
+ boolean mismatchPatchVersion = patchVersionSpecific(keyVersion)
+ && !keyVersion.getPatch().equals(componentVersionMin.getPatch());
+
+ return mismatchMinorVersion || mismatchPatchVersion;
+ }
+
+ private boolean minorVersionSpecific(Version keyVersion) {
+ return !keyVersion.getMinor().equals(Integer.MAX_VALUE);
+ }
+
+ private boolean patchVersionSpecific(Version keyVersion) {
+ return !keyVersion.getPatch().equals(Integer.MAX_VALUE);
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Action.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Action.java
index 28a408e6ad..90673de29b 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Action.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Action.java
@@ -1,21 +1,26 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
import edu.cornell.mannlib.vitro.webapp.dynapi.OperationData;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
-public class Action implements RunnableComponent{
-
- private static final Log log = LogFactory.getLog(Action.class);
+public class Action implements Poolable, Operation, Link {
private Step firstStep = null;
private RPC rpc;
+ private Set clients = ConcurrentHashMap.newKeySet();
+
+ private Parameters providedParams = new Parameters();
+ private Parameters requiredParams;
+
@Override
public void dereference() {
if (firstStep != null) {
@@ -25,29 +30,90 @@ public void dereference() {
rpc.dereference();
rpc = null;
}
-
+
public OperationResult run(OperationData input) {
if (firstStep == null) {
return new OperationResult(HttpServletResponse.SC_NOT_IMPLEMENTED);
}
return firstStep.run(input);
}
-
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#firstStep", minOccurs = 1, maxOccurs = 1)
public void setStep(OperationalStep step) {
this.firstStep = step;
- }
-
+ }
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#assignedRPC", minOccurs = 1, maxOccurs = 1)
public void setRPC(RPC rpc) {
this.rpc = rpc;
}
-
- public String getName() {
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#providesParameter")
+ public void addProvidedParameter(Parameter param) {
+ providedParams.add(param);
+ }
+
+ @Override
+ public String getKey() {
return rpc.getName();
}
+ @Override
public boolean isValid() {
return true;
}
+
+ @Override
+ public void addClient() {
+ clients.add(Thread.currentThread().getId());
+ }
+
+ @Override
+ public void removeClient() {
+ clients.remove(Thread.currentThread().getId());
+ }
+
+ @Override
+ public void removeDeadClients() {
+ Map currentThreadIds = Thread
+ .getAllStackTraces()
+ .keySet()
+ .stream()
+ .collect(Collectors.toMap(Thread::getId, Thread::isAlive));
+ for (Long client : clients) {
+ if (!currentThreadIds.containsKey(client) || currentThreadIds.get(client) == false) {
+ clients.remove(client);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasClients() {
+ return !clients.isEmpty();
+ }
+
+ @Override
+ public Set getNextLinks() {
+ return Collections.singleton(firstStep);
+ }
+
+ @Override
+ public Parameters getRequiredParams() {
+ return requiredParams;
+ }
+
+ public void computeScopes() {
+ requiredParams = Scopes.computeInitialRequirements(this);
+ }
+
+ @Override
+ public Parameters getProvidedParams() {
+ return providedParams;
+ }
+
+ @Override
+ public boolean isRoot() {
+ return true;
+ }
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Condition.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Condition.java
new file mode 100644
index 0000000000..395fb013d2
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Condition.java
@@ -0,0 +1,5 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+public interface Condition extends ParameterInfo{
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ConditionalStep.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ConditionalStep.java
index f9d918a8ad..69d76ea91e 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ConditionalStep.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ConditionalStep.java
@@ -1,10 +1,14 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+import java.util.Set;
+
import edu.cornell.mannlib.vitro.webapp.dynapi.OperationData;
public class ConditionalStep implements Step {
- @Override
+ private Condition condition;
+
+ @Override
public void dereference() {
// TODO Auto-generated method stub
@@ -15,4 +19,29 @@ public OperationResult run(OperationData input) {
return null;
}
+ @Override
+ public Set getNextLinks() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Parameters getRequiredParams() {
+ return condition.getRequiredParams();
+ }
+
+ @Override
+ public Parameters getProvidedParams() {
+ return new Parameters();
+ }
+
+ @Override
+ public boolean isRoot() {
+ return false;
+ }
+
+ public void setCondition(Condition condition) {
+ this.condition = condition;
+ }
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomAction.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomRESTAction.java
similarity index 86%
rename from api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomAction.java
rename to api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomRESTAction.java
index 11d0d2a21c..6a62b14789 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomAction.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/CustomRESTAction.java
@@ -2,7 +2,7 @@
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
-public class CustomAction implements Removable {
+public class CustomRESTAction implements Removable {
private String name;
private RPC targetRPC;
@@ -17,7 +17,7 @@ public String getName() {
return name;
}
- @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#customActionName", minOccurs = 1, maxOccurs = 1)
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#customRESTActionName", minOccurs = 1, maxOccurs = 1)
public void setName(String name) {
this.name = name;
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/DefaultResource.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/DefaultResource.java
new file mode 100644
index 0000000000..0601d19cf5
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/DefaultResource.java
@@ -0,0 +1,10 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class DefaultResource extends Resource {
+
+ private static final Log log = LogFactory.getLog(DefaultResource.class);
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Link.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Link.java
new file mode 100644
index 0000000000..2622c30a35
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Link.java
@@ -0,0 +1,11 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import java.util.Set;
+
+public interface Link extends ParameterInfo {
+
+ public Set getNextLinks();
+
+ public boolean isRoot();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/N3Template.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/N3Template.java
index f6a209c93c..062407eaad 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/N3Template.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/N3Template.java
@@ -6,10 +6,13 @@
public class N3Template implements Template{
private String n3Text;
+ private Parameters requiredParams = new Parameters();
- @Override
- public void dereference() {
-
+ //region @Property Setters
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#requiresParameter")
+ public void addRequiredParameter(Parameter param) {
+ requiredParams.add(param);
}
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#hasN3Text", minOccurs = 1, maxOccurs = 1)
@@ -17,34 +20,29 @@ public void setN3Text(String n3Text) {
this.n3Text = n3Text;
}
- @Override
- public OperationResult run(OperationData input) {
-
- return null;
- }
+ //endregion
+
+ //region Getters
@Override
- public void addRequiredParameter(Parameter param) {
- // TODO Auto-generated method stub
-
+ public Parameters getRequiredParams() {
+ return requiredParams;
}
@Override
- public void addProvidedParameter(Parameter param) {
- // TODO Auto-generated method stub
-
+ public Parameters getProvidedParams() {
+ return new Parameters();
}
+ //endregion
+
@Override
- public Parameters getRequiredParams() {
- // TODO Auto-generated method stub
+ public OperationResult run(OperationData input) {
return null;
}
@Override
- public Parameters getProvidedParams() {
- // TODO Auto-generated method stub
- return null;
- }
+ public void dereference() {
+ }
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Operation.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Operation.java
index 50153b094a..6682cf62b4 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Operation.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Operation.java
@@ -1,12 +1,6 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components;
-public interface Operation extends RunnableComponent {
-
- public void addRequiredParameter(Parameter param);
-
- public void addProvidedParameter(Parameter param);
+public interface Operation extends RunnableComponent, ParameterInfo {
- public Parameters getRequiredParams();
- public Parameters getProvidedParams();
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationResult.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationResult.java
index b635386de5..f48f88f8d7 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationResult.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationResult.java
@@ -27,5 +27,9 @@ public void prepareResponse(HttpServletResponse response) {
public static OperationResult notImplemented() {
return new OperationResult(HttpServletResponse.SC_NOT_IMPLEMENTED);
}
+
+ public static OperationResult notFound() {
+ return new OperationResult(HttpServletResponse.SC_NOT_FOUND);
+ }
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationalStep.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationalStep.java
index bcdc808cd1..5cb437cc1d 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationalStep.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/OperationalStep.java
@@ -1,5 +1,8 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+import java.util.Collections;
+import java.util.Set;
+
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -59,4 +62,24 @@ public OperationResult run(OperationData data) {
}
return result;
}
+
+ @Override
+ public Set getNextLinks() {
+ return Collections.singleton(nextStep);
+ }
+
+ @Override
+ public Parameters getRequiredParams() {
+ return operation.getRequiredParams();
+ }
+
+ @Override
+ public Parameters getProvidedParams() {
+ return operation.getProvidedParams();
+ }
+
+ @Override
+ public boolean isRoot() {
+ return false;
+ }
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ParameterInfo.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ParameterInfo.java
new file mode 100644
index 0000000000..343d8456a6
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ParameterInfo.java
@@ -0,0 +1,9 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+public interface ParameterInfo {
+
+ public Parameters getRequiredParams();
+
+ public Parameters getProvidedParams();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Poolable.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Poolable.java
new file mode 100644
index 0000000000..0ab579dca5
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Poolable.java
@@ -0,0 +1,17 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+public interface Poolable extends Removable {
+
+ public K getKey();
+
+ public boolean isValid();
+
+ public void addClient();
+
+ public void removeClient();
+
+ public void removeDeadClients();
+
+ public boolean hasClients();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Resource.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Resource.java
index 076abdc22d..f2829ac399 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Resource.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Resource.java
@@ -2,15 +2,15 @@
import java.util.LinkedList;
import java.util.List;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
-public class Resource implements Removable {
+public class Resource implements Versionable {
- private static final Log log = LogFactory.getLog(Resource.class);
private String name;
private String versionMin;
private String versionMax;
@@ -19,17 +19,21 @@ public class Resource implements Removable {
private RPC rpcOnDelete;
private RPC rpcOnPut;
private RPC rpcOnPatch;
- private List customActions = new LinkedList();
+ private List customRESTActions = new LinkedList();
+
+ private Set clients = ConcurrentHashMap.newKeySet();
+ @Override
public String getVersionMin() {
return versionMin;
}
-
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#restAPIVersionMin", minOccurs = 0, maxOccurs = 1)
public void setVersionMin(String versionMin) {
this.versionMin = versionMin;
}
+ @Override
public String getVersionMax() {
return versionMax;
}
@@ -39,13 +43,23 @@ public void setVersionMax(String versionMax) {
this.versionMax = versionMax;
}
+ public String getName() {
+ return name;
+ }
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#resourceName", minOccurs = 1, maxOccurs = 1)
public void setName(String name) {
this.name = name;
}
-
- public String getName() {
- return name;
+
+ @Override
+ public ResourceKey getKey() {
+ return ResourceKey.of(name, versionMin);
+ }
+
+ @Override
+ public boolean isValid() {
+ return true;
}
public RPC getRpcOnGet() {
@@ -92,16 +106,80 @@ public RPC getRpcOnPatch() {
public void setRpcOnPatch(RPC rpcOnPatch) {
this.rpcOnPatch = rpcOnPatch;
}
-
- @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#hasCustomAction")
- public void addCustomAction(CustomAction customAction) {
- customActions.add(customAction);
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#hasCustomRESTAction")
+ public void addCustomRESTAction(CustomRESTAction customRESTAction) {
+ customRESTActions.add(customRESTAction);
}
@Override
public void dereference() {
// TODO Auto-generated method stub
-
+
+ }
+
+ @Override
+ public void addClient() {
+ clients.add(Thread.currentThread().getId());
+ }
+
+ @Override
+ public void removeClient() {
+ clients.remove(Thread.currentThread().getId());
}
-
+
+ @Override
+ public void removeDeadClients() {
+ Map currentThreadIds = Thread
+ .getAllStackTraces()
+ .keySet()
+ .stream()
+ .collect(Collectors.toMap(Thread::getId, Thread::isAlive));
+ for (Long client : clients) {
+ if (!currentThreadIds.containsKey(client) || currentThreadIds.get(client) == false) {
+ clients.remove(client);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasClients() {
+ return !clients.isEmpty();
+ }
+
+ public String getCustomRestActionByName(String name) {
+ for (CustomRESTAction customRestAction : customRESTActions) {
+ if (customRestAction.getName().equals(name)) {
+ return customRestAction.getTargetRPC().getName();
+ }
+ }
+ throw new UnsupportedOperationException("Unsupported custom action");
+ }
+
+ public String getActionNameByMethod(String method) {
+ System.out.println("\nget action name: " + method + "\n");
+ switch (method.toUpperCase()) {
+ case "POST":
+ return getNameOfRpc(rpcOnPost);
+ case "GET":
+ return getNameOfRpc(rpcOnGet);
+ case "DELETE":
+ return getNameOfRpc(rpcOnDelete);
+ case "PUT":
+ return getNameOfRpc(rpcOnPut);
+ case "PATCH":
+ return getNameOfRpc(rpcOnPatch);
+ default:
+ throw new UnsupportedOperationException("Unsupported method");
+ }
+ }
+
+ private String getNameOfRpc(RPC rpc) {
+ System.out.println("\nget name of rpc: " + rpc + "\n");
+ if (rpc != null) {
+ return rpc.getName();
+ }
+ throw new UnsupportedOperationException("Unable to determine action");
+ }
+
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ResourceKey.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ResourceKey.java
new file mode 100644
index 0000000000..b86d940e33
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/ResourceKey.java
@@ -0,0 +1,82 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import static java.lang.String.format;
+
+public class ResourceKey implements Comparable, Versioned {
+
+ private final String name;
+
+ private final Version version;
+
+ private ResourceKey(String name, String version) {
+ this.name = name;
+ this.version = Version.of(version);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((version == null) ? 0 : version.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ResourceKey other = (ResourceKey) obj;
+ if (name == null) {
+ if (other.name != null) {
+ return false;
+ }
+ } else if (!name.equals(other.name)) {
+ return false;
+ }
+ if (version == null) {
+ if (other.version != null) {
+ return false;
+ }
+ } else if (!version.equals(other.version)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(ResourceKey o) {
+ int nameCompare = this.name.compareTo(o.name);
+ if (nameCompare == 0) {
+ return this.version.compareTo(o.version);
+ }
+ return nameCompare;
+ }
+
+ @Override
+ public String toString() {
+ return format("%s (%s)", name, version.toString());
+ }
+
+ public static ResourceKey of(String name, String version) {
+ return new ResourceKey(name, version);
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SPARQLQuery.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SPARQLQuery.java
index f5411c3b17..2d311d4ec0 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SPARQLQuery.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SPARQLQuery.java
@@ -17,7 +17,7 @@
import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess;
import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
-public class SPARQLQuery implements Operation{
+public class SPARQLQuery implements Operation {
private static final Log log = LogFactory.getLog(SPARQLQuery.class);
@@ -30,12 +30,12 @@ public class SPARQLQuery implements Operation{
public void dereference() {
//TODO
}
-
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#requiresParameter")
public void addRequiredParameter(Parameter param) {
- requiredParams.add(param);
+ requiredParams.add(param);
}
-
+
@Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#providesParameter")
public void addProvidedParameter(Parameter param) {
providedParams.add(param);
@@ -50,11 +50,13 @@ public void setQueryText(String queryText) {
public void setQueryModel(ModelComponent model) {
this.modelComponent = model;
}
-
+
+ @Override
public Parameters getRequiredParams() {
return requiredParams;
}
+ @Override
public Parameters getProvidedParams() {
return providedParams;
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Scopes.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Scopes.java
new file mode 100644
index 0000000000..8e30a707a5
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Scopes.java
@@ -0,0 +1,25 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class Scopes {
+
+ public static Parameters computeInitialRequirements(Action action) {
+ Parameters actionResult = action.getProvidedParams();
+ Set links = action.getNextLinks();
+ Set requirements = new HashSet();
+
+ for (Link link : links) {
+ computeRequirements(link, actionResult);
+ }
+
+ return new Parameters();
+ }
+
+ private static void computeRequirements(Link link, Parameters actionResult) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SolrQuery.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SolrQuery.java
new file mode 100644
index 0000000000..1f6dd4f12c
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/SolrQuery.java
@@ -0,0 +1,217 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils;
+import edu.cornell.mannlib.vitro.webapp.dynapi.OperationData;
+import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngine;
+import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngineException;
+import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchQuery;
+import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchResponse;
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.*;
+
+public class SolrQuery implements Operation{
+
+ private static final Log log = LogFactory.getLog(SolrQuery.class);
+
+ private Parameters requiredParams = new Parameters();
+ private Parameters providedParams = new Parameters();
+
+ private String queryText;
+ private String offset;
+ private String limit;
+ private ArrayList fields = new ArrayList<>();
+ private ArrayList filters = new ArrayList<>();
+ private ArrayList facets = new ArrayList<>();
+ private ArrayList sorts = new ArrayList<>();
+
+ //region @Property Setters
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#requiresParameter")
+ public void addRequiredParameter(Parameter param) {
+ requiredParams.add(param);
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#providesParameter")
+ public void addProvidedParameter(Parameter param) {
+ providedParams.add(param);
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrQueryText", maxOccurs = 1)
+ public void setQueryText(String queryText){
+ this.queryText=queryText;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrFilter")
+ public void addFilter(String filter){
+ this.filters.add(filter);
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrField")
+ public void addField(String field){
+ this.fields.add(field);
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrLimit", maxOccurs = 1)
+ public void setLimit(String limit){
+ this.limit=limit;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrOffset", maxOccurs = 1)
+ public void setOffset(String offset){
+ this.offset=offset;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrSort")
+ public void addSort(String sort){
+ this.sorts.add(sort);
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#solrFacet")
+ public void addFacet(String facet){
+ this.facets.add(facet);
+ }
+
+ //endregion
+
+ //region Getters
+
+ @Override
+ public Parameters getRequiredParams() {
+ return requiredParams;
+ }
+
+ @Override
+ public Parameters getProvidedParams() {
+ return providedParams;
+ }
+
+ public String getQueryText() {
+ return queryText;
+ }
+
+ public ArrayList getFields() {
+ return fields;
+ }
+
+ public ArrayList getFilters() {
+ return filters;
+ }
+
+ public String getOffset() {
+ return offset;
+ }
+
+ public String getLimit() {
+ return limit;
+ }
+
+ public ArrayList getFacets() {
+ return facets;
+ }
+
+ public ArrayList getSorts() {
+ return sorts;
+ }
+
+ //endregion
+
+ @Override
+ public OperationResult run(OperationData input) {
+ if (!isInputValid(input)) {
+ return new OperationResult(400);
+ }
+
+ SearchQuery searchQuery;
+ try {
+ searchQuery = createSearchQuery(input);
+ } catch (Exception e) {
+ log.error("Error while parsing input data for query");
+ log.error(e);
+ return new OperationResult(400);
+ }
+
+ SearchEngine searchEngine = ApplicationUtils.instance().getSearchEngine();
+ SearchResponse response;
+ try {
+ response = searchEngine.query(searchQuery);
+ } catch (SearchEngineException e) {
+ log.error("Error while executing Solr Query:");
+ log.error(e.getMessage());
+ return new OperationResult(500);
+ }
+
+ return new OperationResult(200);
+ }
+
+ private SearchQuery createSearchQuery(OperationData input)
+ throws InputMismatchException, IllegalArgumentException{
+ SearchQuery searchQuery = ApplicationUtils.instance().getSearchEngine().createQuery();
+
+ if(queryText!=null){
+ searchQuery = searchQuery.setQuery(replaceVariablesWithInput(queryText, input));
+ }
+ if(offset!=null){
+ searchQuery = searchQuery.setStart(Integer.parseInt(replaceVariablesWithInput(offset, input)));
+ }
+ if(limit!=null){
+ searchQuery = searchQuery.setRows(Integer.parseInt(replaceVariablesWithInput(limit,input)));
+ }
+ for(String field:fields){
+ searchQuery = searchQuery.addFields(replaceVariablesWithInput(field, input));
+ }
+ for(String filter:filters){
+ searchQuery = searchQuery.addFilterQuery(replaceVariablesWithInput(filter, input));
+ }
+ for(String sort: sorts){
+ sort = replaceVariablesWithInput(sort, input);
+ String[] sortTokens = sort.trim().split(" ");
+ searchQuery = searchQuery.addSortField(
+ sortTokens[0],
+ SearchQuery.Order.valueOf(sortTokens[sortTokens.length-1].toUpperCase())
+ );
+ }
+ return searchQuery;
+ }
+
+ private String replaceVariablesWithInput(String property, OperationData input)
+ throws InputMismatchException{
+
+ String[] propertyVariables = Arrays.stream(property.split(":| |,"))
+ .filter(propertySegment -> propertySegment.startsWith("?"))
+ .map(propertyVariable -> propertyVariable.substring(1))
+ .toArray(String[]::new);
+
+ for(String propertyVar:propertyVariables){
+ if(!input.has(propertyVar) || input.get(propertyVar).length>1){
+ throw new InputMismatchException();
+ }
+ property=property.replace("?"+propertyVar, input.get(propertyVar)[0]);
+ }
+
+ return property;
+ }
+
+ private boolean isInputValid(OperationData input) {
+ for (String name : requiredParams.getNames()) {
+ if (!input.has(name)) {
+ log.error("Parameter " + name + " not found");
+ return false;
+ }
+ Parameter param = requiredParams.get(name);
+ String[] inputValues = input.get(name);
+ if (!param.isValid(name, inputValues)){
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void dereference() {
+
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Step.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Step.java
index c43a30b502..9498ea3118 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Step.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Step.java
@@ -1,5 +1,5 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components;
-public interface Step extends RunnableComponent{
+public interface Step extends RunnableComponent, Link{
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Version.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Version.java
new file mode 100644
index 0000000000..a13921c841
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Version.java
@@ -0,0 +1,103 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+import static java.lang.String.format;
+import static java.util.regex.Pattern.quote;
+
+public class Version implements Comparable {
+
+ private final static String PERIOD_PATTERN = quote(".");
+
+ private final Integer major;
+
+ private final Integer minor;
+
+ private final Integer patch;
+
+ private Version(String version) {
+ String[] parts = version.split(PERIOD_PATTERN);
+ major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0;
+ minor = parts.length > 1 ? Integer.parseInt(parts[1]) : Integer.MAX_VALUE;
+ patch = parts.length > 2 ? Integer.parseInt(parts[2]) : Integer.MAX_VALUE;
+ }
+
+ public Integer getMajor() {
+ return major;
+ }
+
+ public Integer getMinor() {
+ return minor;
+ }
+
+ public Integer getPatch() {
+ return patch;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((major == null) ? 0 : major.hashCode());
+ result = prime * result + ((minor == null) ? 0 : minor.hashCode());
+ result = prime * result + ((patch == null) ? 0 : patch.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Version other = (Version) obj;
+ if (major == null) {
+ if (other.major != null) {
+ return false;
+ }
+ } else if (!major.equals(other.major)) {
+ return false;
+ }
+ if (minor == null) {
+ if (other.minor != null) {
+ return false;
+ }
+ } else if (!minor.equals(other.minor)) {
+ return false;
+ }
+ if (patch == null) {
+ if (other.patch != null) {
+ return false;
+ }
+ } else if (!patch.equals(other.patch)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int compareTo(Version o) {
+ int majorCompare = this.major.compareTo(o.major);
+ if (majorCompare == 0) {
+ int minorCompare = this.minor.compareTo(o.minor);
+ if (minorCompare == 0) {
+ return this.patch.compareTo(o.patch);
+ }
+ return minorCompare;
+ }
+ return majorCompare;
+ }
+
+ @Override
+ public String toString() {
+ return format("%s.%s.%s", major, minor, patch);
+ }
+
+ public static Version of(String version) {
+ return new Version(version);
+ }
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versionable.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versionable.java
new file mode 100644
index 0000000000..14212bded9
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versionable.java
@@ -0,0 +1,9 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+public interface Versionable extends Poolable {
+
+ public String getVersionMin();
+
+ public String getVersionMax();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versioned.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versioned.java
new file mode 100644
index 0000000000..3824185f40
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/Versioned.java
@@ -0,0 +1,9 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components;
+
+public interface Versioned {
+
+ public String getName();
+
+ public Version getVersion();
+
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/AbstractValidator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/AbstractValidator.java
index 6cbbf7cb15..5c11c67c7a 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/AbstractValidator.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/AbstractValidator.java
@@ -1,22 +1,11 @@
package edu.cornell.mannlib.vitro.webapp.dynapi.components.validators;
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
+
public abstract class AbstractValidator implements Validator {
-
- private String name;
-
@Override
public void dereference() {
}
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public void setName(String name) {
- this.name = name;
- }
-
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/IsInteger.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/IsInteger.java
index c3f2bce74a..c39b92efaf 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/IsInteger.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/IsInteger.java
@@ -13,18 +13,20 @@ public boolean isValid(String name, String[] values) {
if (!super.isValid(name, values)) {
return false;
}
-
- if (!isInteger(values[0])) {
- return false;
+
+ for (String value : values) {
+ if (!isInteger(values[0])) {
+ return false;
+ }
}
return true;
}
private boolean isInteger(String string) {
- //TODO: Do the real check
- if (NumberUtils.isParsable(string)) {
+ if (NumberUtils.isDigits(string)) {
return true;
}
+ log.debug("Value is not a number. Validation failed.");
return false;
}
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/NumericRangeValidator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/NumericRangeValidator.java
new file mode 100644
index 0000000000..9b0f10d1bb
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/NumericRangeValidator.java
@@ -0,0 +1,62 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components.validators;
+
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class NumericRangeValidator extends IsNotBlank {
+
+ private static final Log log = LogFactory.getLog(NumericRangeValidator.class);
+
+ private Float minValue;
+
+ private Float maxValue;
+
+ public Float getMinValue() {
+ return minValue;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#validatorMinNumericValue", maxOccurs = 1)
+ public void setMinValue(float minValue) {
+ this.minValue = minValue;
+ }
+
+ public Float getMaxValue() {
+ return maxValue;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#validatorMaxNumericValue", maxOccurs = 1)
+ public void setMaxValue(float maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ @Override
+ public boolean isValid(String name, String[] values) {
+ if (!super.isValid(name, values)) {
+ return false;
+ }
+ for (String value : values) {
+ if (!isInRange(value)) {
+ log.debug("Value of " + name + " is not in range [" + ((minValue != null)?minValue:" ") + "-" + ((maxValue != null)?maxValue:" ")+"].");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isInRange(String string) {
+ if (NumberUtils.isParsable(string)) {
+ double value = NumberUtils.toDouble(string);
+
+ if ((minValue != null) && (value < minValue))
+ return false;
+
+ if ((maxValue != null) && (value > maxValue))
+ return false;
+
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/RegularExpressionValidator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/RegularExpressionValidator.java
new file mode 100644
index 0000000000..5401c8eae9
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/RegularExpressionValidator.java
@@ -0,0 +1,44 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components.validators;
+
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.regex.Pattern;
+
+public class RegularExpressionValidator extends AbstractValidator {
+
+ private static final Log log = LogFactory.getLog(RegularExpressionValidator.class);
+
+ private String regularExpression;
+
+ public String getRegularExpression() {
+ return regularExpression;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#validatorRegularExpressionValue", minOccurs = 1, maxOccurs = 1)
+ public void setRegularExpression (String regularExpression) {
+ this.regularExpression = regularExpression;
+ }
+
+ @Override
+ public boolean isValid(String name, String[] values) {
+ if (values.length == 0) {
+ log.debug("No values of " + name + " found. Validation failed.");
+ return false;
+ }
+
+ for (String value : values) {
+ if (!isLengthInRange(value)) {
+ log.debug("Value of " + name + " is not in accordance with the pattern \"" + regularExpression + "\"");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean isLengthInRange(String string) {
+ return Pattern.matches(regularExpression, string);
+ }
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/StringLengthRangeValidator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/StringLengthRangeValidator.java
new file mode 100644
index 0000000000..4b671d30c3
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/StringLengthRangeValidator.java
@@ -0,0 +1,59 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.components.validators;
+
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class StringLengthRangeValidator extends IsNotBlank {
+
+ private static final Log log = LogFactory.getLog(StringLengthRangeValidator.class);
+
+ private Integer minLength;
+
+ private Integer maxLength;
+
+ public Integer getMinLength() {
+ return minLength;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#validatorMinLengthValue", maxOccurs = 1)
+ public void setMinLength(int minLength) {
+ this.minLength = minLength;
+ }
+
+ public Integer getMaxLength() {
+ return maxLength;
+ }
+
+ @Property(uri = "https://vivoweb.org/ontology/vitro-dynamic-api#validatorMaxLengthValue", maxOccurs = 1)
+ public void setMaxLength(int maxValue) {
+ this.maxLength = maxValue;
+ }
+
+ @Override
+ public boolean isValid(String name, String[] values) {
+ if (!super.isValid(name, values)) {
+ return false;
+ }
+
+ for (String value : values) {
+ if (!isLengthInRange(value)) {
+ log.debug("Length of " + name + " is not in range [" + ((minLength != null)?minLength:" ") + "-" + ((maxLength != null)?maxLength:" ")+"].");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isLengthInRange(String string) {
+ int length = string.length();
+
+ if ((minLength != null) && (length < minLength))
+ return false;
+
+ if ((maxLength != null) && (length > maxLength))
+ return false;
+
+ return true;
+ }
+}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/Validator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/Validator.java
index 039f23aa79..261a7242d2 100644
--- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/Validator.java
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/components/validators/Validator.java
@@ -4,10 +4,6 @@
public interface Validator extends Removable {
- public boolean isValid(String name, String[] values);
-
- public String getName();
-
- public void setName(String getName);
+ boolean isValid(String name, String[] values);
}
diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/request/RequestPath.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/request/RequestPath.java
new file mode 100644
index 0000000000..a8b4ef7af8
--- /dev/null
+++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dynapi/request/RequestPath.java
@@ -0,0 +1,139 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi.request;
+
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.apache.commons.lang3.StringUtils.isNotEmpty;
+
+import java.util.Base64;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RequestPath {
+
+ public static final String RESOURCE_ID_PARAM = "RESOURCE_ID";
+
+ public static final String API_BASE_PATH = "/api";
+ public static final String RPC_BASE_PATH = API_BASE_PATH + "/rpc";
+ public static final String REST_BASE_PATH = API_BASE_PATH + "/rest";
+
+ private final RequestType type;
+
+ private final String[] pathParts;
+
+ private final String resourceVersion;
+
+ private final String resourceName;
+
+ private final String resourceId;
+
+ private final String actionName;
+
+ private RequestPath(HttpServletRequest request) {
+ String contextPath = request != null && request.getContextPath() != null
+ ? request.getContextPath()
+ : EMPTY;
+ String pathInfo = request != null && request.getPathInfo() != null
+ ? request.getPathInfo()
+ : EMPTY;
+
+ pathParts = pathInfo.split("/");
+
+ if (contextPath.toLowerCase().contains(RPC_BASE_PATH)) {
+ type = RequestType.RPC;
+ actionName = pathParts.length > 1 ? pathParts[1] : null;
+ resourceVersion = null;
+ resourceName = null;
+ resourceId = null;
+ } else if (contextPath.toLowerCase().contains(REST_BASE_PATH)) {
+ type = RequestType.REST;
+ resourceVersion = pathParts.length > 1 ? pathParts[1] : null;
+ resourceName = pathParts.length > 2 ? pathParts[2] : null;
+
+ if (pathParts.length > 3) {
+ if (pathParts[3].toLowerCase().startsWith("resource:")) {
+ resourceId = decode(pathParts[3]);
+ actionName = pathParts.length > 4 ? pathParts[4] : null;
+ request.getParameterMap().put(RESOURCE_ID_PARAM, new String[] { resourceId });
+ } else {
+ resourceId = null;
+ actionName = pathParts[3];
+ }
+ } else {
+ resourceId = null;
+ actionName = null;
+ }
+ } else {
+ type = RequestType.UNKNOWN;
+ resourceVersion = null;
+ resourceName = null;
+ resourceId = null;
+ actionName = null;
+ }
+ }
+
+ private String decode(String pathParameter) {
+ return new String(Base64.getDecoder().decode(pathParameter.split(":")[1]));
+ }
+
+ public RequestType getType() {
+ return type;
+ }
+
+ public String getResourceVersion() {
+ return resourceVersion;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public String getActionName() {
+ return actionName;
+ }
+
+ public boolean isValid() {
+ boolean isValid = false;
+ switch (type) {
+ case REST:
+ boolean hasVersionAndName = isNotEmpty(resourceVersion) && isNotEmpty(resourceName);
+ if (pathParts.length == 3) {
+ isValid = hasVersionAndName;
+ } else if (pathParts.length > 3) {
+ boolean hasActionName = isNotEmpty(actionName);
+ boolean hasResourceId = isNotEmpty(resourceId);
+
+ if (pathParts.length == 4) {
+ isValid = hasVersionAndName && (hasActionName || hasResourceId);
+ } else if (pathParts.length == 5) {
+ isValid = hasVersionAndName && hasResourceId && hasActionName;
+ }
+ }
+ break;
+ case RPC:
+ boolean hasActionName = isNotEmpty(actionName);
+
+ isValid = hasActionName;
+ break;
+ case UNKNOWN:
+ default:
+ }
+
+ return isValid;
+ }
+
+ public boolean isCustomRestAction() {
+ return isNotEmpty(resourceVersion) && isNotEmpty(resourceName) && isNotEmpty(actionName);
+ }
+
+ public static RequestPath from(HttpServletRequest request) {
+ return new RequestPath(request);
+ }
+
+ public enum RequestType {
+ RPC, REST, UNKNOWN
+ }
+
+}
diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilderTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilderTest.java
index e7872e6d14..4c81db0572 100644
--- a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilderTest.java
+++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/UrlBuilderTest.java
@@ -3,21 +3,17 @@
package edu.cornell.mannlib.vitro.webapp.controller.freemarker;
-import static org.easymock.EasyMock.*;
-
-import javax.servlet.http.HttpServletRequest;
-
import org.junit.Assert;
-
import org.junit.Test;
-import stubs.edu.cornell.mannlib.vitro.webapp.dao.ApplicationDaoStub;
-import stubs.edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactoryStub;
import edu.cornell.mannlib.vitro.testing.AbstractTestClass;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder.ParamMap;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.web.URLEncoder;
+import stubs.edu.cornell.mannlib.vitro.webapp.dao.ApplicationDaoStub;
+import stubs.edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactoryStub;
+import stubs.javax.servlet.http.HttpServletRequestStub;
public class UrlBuilderTest extends AbstractTestClass {
@@ -114,7 +110,7 @@ public void testGetIndividualProfileURI(){
}
protected VitroRequest makeMockVitroRequest( final String defaultNS){
- HttpServletRequest req = createMock( HttpServletRequest.class );
+ HttpServletRequestStub req = new HttpServletRequestStub();
return new VitroRequest(req){
@Override
diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPoolTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPoolTest.java
new file mode 100644
index 0000000000..5bc491e5dc
--- /dev/null
+++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/ActionPoolTest.java
@@ -0,0 +1,315 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.After;
+import org.junit.Test;
+
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Action;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.DefaultAction;
+import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoaderException;
+
+public class ActionPoolTest extends ServletContextTest {
+
+ protected final static String TEST_PERSON_ACTION_URI = "https://vivoweb.org/ontology/vitro-dynamic-api/action/testPersonAction1";
+
+ @After
+ public void reset() {
+ setup();
+
+ ActionPool actionPool = ActionPool.getInstance();
+ actionPool.init(servletContext);
+ actionPool.reload();
+
+ assertEquals(0, actionPool.count());
+ assertEquals(0, actionPool.obsoleteCount());
+ }
+
+ @Test
+ public void testGetInstance() {
+ ActionPool actionPool = ActionPool.getInstance();
+ assertNotNull(actionPool);
+ assertEquals(actionPool, ActionPool.getInstance());
+ }
+
+ @Test
+ public void testGetBeforeInit() {
+ ActionPool actionPool = ActionPool.getInstance();
+ Action action = actionPool.get(TEST_ACTION_NAME);
+ assertNotNull(action);
+ assertTrue(action instanceof DefaultAction);
+ }
+
+ @Test
+ public void testPrintKeysBeforeInit() {
+ ActionPool actionPool = ActionPool.getInstance();
+ actionPool.printKeys();
+ // nothing to assert
+ }
+
+ @Test
+ public void testReloadBeforeInit() throws IOException {
+ ActionPool actionPool = ActionPool.getInstance();
+ actionPool.reload();
+ // not sure what to assert
+ }
+
+ @Test
+ public void testInit() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+ assertEquals(1, actionPool.count());
+ assertEquals(0, actionPool.obsoleteCount());
+
+ assertAction(TEST_ACTION_NAME, actionPool.get(TEST_ACTION_NAME));
+ }
+
+ @Test
+ public void testPrintKeys() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ actionPool.printKeys();
+ // nothing to assert
+ }
+
+ @Test
+ public void testAdd() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ Action action = loader.loadInstance(TEST_PERSON_ACTION_URI, Action.class);
+
+ actionPool.add(TEST_PERSON_ACTION_URI, action);
+
+ assertEquals(0, actionPool.obsoleteCount());
+
+ assertAction(TEST_PERSON_ACTION_NAME, actionPool.get(TEST_PERSON_ACTION_NAME));
+ }
+
+ @Test
+ public void testAddHasClient() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ actionPool.reload();
+
+ Action action = loader.loadInstance(TEST_PERSON_ACTION_URI, Action.class);
+
+ assertEquals(0, actionPool.obsoleteCount());
+
+ Action actionHasClient = actionPool.get(TEST_PERSON_ACTION_NAME);
+
+ actionPool.add(TEST_PERSON_ACTION_URI, action);
+
+ assertEquals(1, actionPool.obsoleteCount());
+
+ actionHasClient.removeClient();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testAddWithoutModelLoaded() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ Action action = loader.loadInstance(TEST_PERSON_ACTION_URI, Action.class);
+
+ reset();
+
+ assertTrue(actionPool.get(TEST_PERSON_ACTION_NAME) instanceof DefaultAction);
+
+ actionPool.add(TEST_PERSON_ACTION_URI, action);
+ }
+
+ @Test
+ public void testRemove() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ actionPool.reload();
+
+ Action action = actionPool.get(TEST_PERSON_ACTION_NAME);
+
+ assertFalse(action instanceof DefaultAction);
+
+ action.removeClient();
+
+ reset();
+
+ actionPool.remove(TEST_PERSON_ACTION_URI, TEST_PERSON_ACTION_NAME);
+
+ assertEquals(0, actionPool.obsoleteCount());
+
+ assertTrue(actionPool.get(TEST_PERSON_ACTION_NAME) instanceof DefaultAction);
+ }
+
+ @Test
+ public void testRemoveHasClient() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ actionPool.reload();
+
+ Action actionHasClient = actionPool.get(TEST_PERSON_ACTION_NAME);
+
+ assertFalse(actionHasClient instanceof DefaultAction);
+
+ setup();
+
+ actionPool.init(servletContext);
+
+ actionPool.remove(TEST_PERSON_ACTION_URI, TEST_PERSON_ACTION_NAME);
+
+ assertEquals(1, actionPool.obsoleteCount());
+
+ assertTrue(actionPool.get(TEST_PERSON_ACTION_NAME) instanceof DefaultAction);
+
+ actionHasClient.removeClient();
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testRemoveWithModelLoaded() throws IOException, ConfigurationBeanLoaderException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ actionPool.reload();
+
+ actionPool.remove(TEST_PERSON_ACTION_URI, TEST_PERSON_ACTION_NAME);
+ }
+
+ @Test
+ public void testReloadSingle() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ Action action = actionPool.get(TEST_PERSON_ACTION_NAME);
+
+ assertTrue(action instanceof DefaultAction);
+
+ actionPool.reload(TEST_PERSON_ACTION_URI);
+
+ assertAction(TEST_PERSON_ACTION_NAME, actionPool.get(TEST_PERSON_ACTION_NAME));
+ }
+
+ @Test
+ public void testReload() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ assertAction(TEST_ACTION_NAME, actionPool.get(TEST_ACTION_NAME));
+
+ loadTestModel();
+
+ actionPool.reload();
+
+ assertEquals(8, actionPool.count());
+ assertEquals(0, actionPool.obsoleteCount());
+
+ assertAction(TEST_PERSON_ACTION_NAME, actionPool.get(TEST_PERSON_ACTION_NAME));
+ }
+
+ @Test
+ public void testReloadThreadSafety() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ assertAction(TEST_ACTION_NAME, actionPool.get(TEST_ACTION_NAME));
+
+ loadTestModel();
+
+ CompletableFuture reloadFuture = CompletableFuture.runAsync(() -> actionPool.reload());
+
+ while (!reloadFuture.isDone()) {
+ assertAction(TEST_ACTION_NAME, actionPool.get(TEST_ACTION_NAME));
+ }
+
+ assertAction(TEST_ACTION_NAME, actionPool.get(TEST_ACTION_NAME));
+
+ assertAction(TEST_PERSON_ACTION_NAME, actionPool.get(TEST_PERSON_ACTION_NAME));
+ }
+
+ @Test
+ public void testRealodOfActionHasClient() throws IOException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ loadTestModel();
+
+ Action action = actionPool.get(TEST_ACTION_NAME);
+
+ CompletableFuture reloadFuture = CompletableFuture.runAsync(() -> actionPool.reload());
+
+ while (!reloadFuture.isDone()) {
+ assertEquals(TEST_ACTION_NAME, action.getKey());
+ }
+
+ action.removeClient();
+ }
+
+ @Test
+ public void testClientsManagement() throws IOException, InterruptedException {
+ ActionPool actionPool = initWithDefaultModel();
+
+ actionPool.reload();
+
+ long initalCount = actionPool.obsoleteCount();
+ Action action = actionPool.get(TEST_ACTION_NAME);
+
+ action.removeClient();
+
+ assertFalse(action.hasClients());
+
+ Thread t1 = getActionInThread(actionPool, TEST_ACTION_NAME);
+
+ t1.join();
+
+ assertTrue(action.hasClients());
+
+ actionPool.reload();
+
+ assertEquals(initalCount, actionPool.obsoleteCount());
+ }
+
+ private Thread getActionInThread(ActionPool actionPool, String name) {
+ Runnable client = new Runnable() {
+ @Override
+ public void run() {
+ Action action = actionPool.get(name);
+ assertEquals(name, action.getKey());
+ assertTrue(action.hasClients());
+ }
+ };
+ Thread thread = new Thread(client);
+ thread.start();
+ return thread;
+ }
+
+ private ActionPool initWithDefaultModel() throws IOException {
+ loadDefaultModel();
+
+ ActionPool actionPool = ActionPool.getInstance();
+
+ actionPool.init(servletContext);
+
+ return actionPool;
+ }
+
+ private void assertAction(String expectedName, Action actualAction) {
+ assertNotNull(actualAction);
+ assertFalse(format("%s not loaded!", expectedName), actualAction instanceof DefaultAction);
+ assertTrue(actualAction.isValid());
+ assertEquals(expectedName, actualAction.getKey());
+ assertTrue(actualAction.hasClients());
+ actualAction.removeClient();
+ }
+
+}
diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpointTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpointTest.java
new file mode 100644
index 0000000000..d332ab8c6f
--- /dev/null
+++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RESTEndpointTest.java
@@ -0,0 +1,204 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import static edu.cornell.mannlib.vitro.webapp.dynapi.request.RequestPath.REST_BASE_PATH;
+import static org.apache.commons.lang3.StringUtils.EMPTY;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Action;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.OperationResult;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.Resource;
+import edu.cornell.mannlib.vitro.webapp.dynapi.components.ResourceKey;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RESTEndpointTest {
+
+ private final static String PATH_INFO = "/1/test";
+ private final static String CONTEXT_PATH = REST_BASE_PATH + PATH_INFO;
+
+ private Map params;
+
+ private ServletContext context;
+
+ private MockedStatic resourcePoolStatic;
+
+ private MockedStatic actionPoolStatic;
+
+ private RESTEndpoint restEndpoint;
+
+ @Mock
+ private ResourcePool resourcePool;
+
+ @Mock
+ private ActionPool actionPool;
+
+ @Mock
+ private Resource resource;
+
+ @Spy
+ private Action action;
+
+ @Mock
+ private HttpServletRequest request;
+
+ @Mock
+ private HttpServletResponse response;
+
+ @Before
+ public void beforeEach() {
+ resourcePoolStatic = mockStatic(ResourcePool.class);
+ actionPoolStatic = mockStatic(ActionPool.class);
+
+ when(ResourcePool.getInstance()).thenReturn(resourcePool);
+ when(resourcePool.get(any(ResourceKey.class))).thenReturn(resource);
+
+ when(ActionPool.getInstance()).thenReturn(actionPool);
+ when(actionPool.get(any(String.class))).thenReturn(action);
+
+ when(request.getParameterMap()).thenReturn(params);
+ when(request.getServletContext()).thenReturn(context);
+
+ restEndpoint = new RESTEndpoint();
+ }
+
+ @After
+ public void afterEach() {
+ resourcePoolStatic.close();
+ actionPoolStatic.close();
+ }
+
+ @Test
+ public void doPostTest() {
+ prepareMocks("POST", "test");
+ restEndpoint.doPost(request, response);
+ verifyMocks(1, HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doPostCustomRestActionTest() {
+ OperationResult result = new OperationResult(HttpServletResponse.SC_OK);
+
+ when(request.getMethod()).thenReturn("POST");
+ when(request.getContextPath()).thenReturn(CONTEXT_PATH + "/dedupe");
+ when(request.getPathInfo()).thenReturn(PATH_INFO + "/dedupe");
+
+ when(action.run(any(OperationData.class))).thenReturn(result);
+
+ when(resource.getCustomRestActionByName("dedupe")).thenReturn("dedupe");
+ doNothing().when(resource).removeClient();
+
+ restEndpoint.doPost(request, response);
+
+ verify(resource, times(0)).getActionNameByMethod(any());
+ verify(resource, times(1)).getCustomRestActionByName(any());
+ verify(resource, times(1)).removeClient();
+ verify(action, times(1)).run(any());
+ verify(action, times(1)).removeClient();
+ verify(response, times(1)).setStatus(HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doPostTestNotFound() {
+ when(request.getPathInfo()).thenReturn(EMPTY);
+ restEndpoint.doPost(request, response);
+ verifyMocks(0, HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void doGetTest() {
+ prepareMocks("GET", "test");
+ restEndpoint.doGet(request, response);
+ verifyMocks(1, HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doGetTestNotFound() {
+ when(request.getPathInfo()).thenReturn(EMPTY);
+ restEndpoint.doGet(request, response);
+ verifyMocks(0, HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void doPutTest() {
+ prepareMocks("PUT", "test");
+ restEndpoint.doPut(request, response);
+ verifyMocks(1, HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doPutTestNotFound() {
+ when(request.getPathInfo()).thenReturn(EMPTY);
+ restEndpoint.doPut(request, response);
+ verifyMocks(0, HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void doPatchTest() {
+ prepareMocks("PATCH", "test");
+ restEndpoint.doPatch(request, response);
+ verifyMocks(1, HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doPatchTestNotFound() {
+ when(request.getPathInfo()).thenReturn(EMPTY);
+ restEndpoint.doPatch(request, response);
+ verifyMocks(0, HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ @Test
+ public void doDeleteTest() {
+ prepareMocks("DELETE", "test");
+ restEndpoint.doDelete(request, response);
+ verifyMocks(1, HttpServletResponse.SC_OK);
+ }
+
+ @Test
+ public void doDeleteTestNotFound() {
+ when(request.getPathInfo()).thenReturn(EMPTY);
+ restEndpoint.doDelete(request, response);
+ verifyMocks(0, HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ private void prepareMocks(String method, String actionName) {
+ OperationResult result = new OperationResult(HttpServletResponse.SC_OK);
+
+ when(request.getMethod()).thenReturn(method);
+ when(request.getContextPath()).thenReturn(CONTEXT_PATH);
+ when(request.getPathInfo()).thenReturn(PATH_INFO);
+
+ when(action.run(any(OperationData.class))).thenReturn(result);
+
+ when(resource.getActionNameByMethod(method)).thenReturn(actionName);
+ doNothing().when(resource).removeClient();
+ }
+
+ private void verifyMocks(int count, int status) {
+ verify(resource, times(count)).getActionNameByMethod(any());
+ verify(resource, times(0)).getCustomRestActionByName(any());
+ verify(resource, times(count)).removeClient();
+ verify(action, times(count)).run(any());
+ verify(action, times(count)).removeClient();
+ verify(response, times(1)).setStatus(status);
+ }
+
+}
diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpointIT.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpointIT.java
new file mode 100644
index 0000000000..904af9a227
--- /dev/null
+++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/dynapi/RPCEndpointIT.java
@@ -0,0 +1,201 @@
+package edu.cornell.mannlib.vitro.webapp.dynapi;
+
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryExecutionFactory;
+import org.apache.jena.query.ResultSetFactory;
+import org.apache.jena.rdf.model.Model;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.mockito.MockedStatic;
+
+@RunWith(Parameterized.class)
+public class RPCEndpointIT extends ServletContextIT {
+
+ private final static String URI_CONTEXT = "/api/rpc/";
+ private final static String URI_BASE = "http://localhost" + URI_CONTEXT;
+
+ private RPCEndpoint rpcEndpoint;
+
+ private HttpServletRequest request;
+
+ private HttpServletResponse response;
+
+ private MockedStatic actionPoolStatic;
+
+ private MockedStatic queryExecutionFactoryStatic;
+
+ private ActionPool actionPool;
+
+ private QueryExecution queryExecution;
+
+ @Parameter(0)
+ public String testAction;
+
+ @Parameter(1)
+ public String testLimit;
+
+ @Parameter(2)
+ public String testEmail;
+
+ @Parameter(3)
+ public Integer testStatus;
+
+ @Parameter(4)
+ public Method testBefore;
+
+ @Parameter(5)
+ public Method testAfter;
+
+ @Parameter(6)
+ public String testMessage;
+
+ @Before
+ public void beforeEach() throws Exception {
+ queryExecution = mock(QueryExecution.class);
+ actionPool = new ActionPool();
+
+ actionPoolStatic = mockStatic(ActionPool.class);
+ when(ActionPool.getInstance()).thenReturn(actionPool);
+
+ queryExecutionFactoryStatic = mockStatic(QueryExecutionFactory.class);
+ when(QueryExecutionFactory.create(any(String.class), any(Model.class))).thenReturn(queryExecution);
+
+ request = mock(HttpServletRequest.class);
+ response = mock(HttpServletResponse.class);
+ rpcEndpoint = new RPCEndpoint();
+
+ loadDefaultModel();
+
+ when(request.getServletContext()).thenReturn(servletContext);
+ when(request.getContextPath()).thenReturn(URI_CONTEXT);
+
+ actionPool.init(request.getServletContext());
+ actionPool.reload();
+
+ if (testAction != null) {
+ StringBuffer buffer = new StringBuffer(URI_BASE + testAction);
+ when(request.getRequestURL()).thenReturn(buffer);
+ when(request.getPathInfo()).thenReturn("/" + testAction);
+ }
+
+ when(request.getParameterMap()).thenReturn(parameterMap);
+
+ mockParameterIntoMap("limit", testLimit);
+ mockParameterIntoMap("email", testEmail);
+ mockStatus(response);
+ runCallback(testBefore);
+ }
+
+ @After
+ public void afterEach() throws Exception {
+ runCallback(testAfter);
+
+ actionPoolStatic.close();
+ queryExecutionFactoryStatic.close();
+ }
+
+ @Test
+ public void doGetTest() {
+ rpcEndpoint.doGet(request, response);
+
+ // For all permutations, this should return HTTP 501.
+ assertResponseStatus(SC_NOT_IMPLEMENTED);
+ }
+
+ @Test
+ public void doPostTest() {
+ rpcEndpoint.doPost(request, response);
+ assertResponseStatus(testStatus);
+ }
+
+ @Test
+ public void doDeleteTest() {
+ rpcEndpoint.doDelete(request, response);
+
+ // For all permutations, this should return HTTP 501.
+ assertResponseStatus(SC_NOT_IMPLEMENTED);
+ }
+
+ @Test
+ public void doPutTest() {
+ rpcEndpoint.doPut(request, response);
+
+ // For all permutations, this should return HTTP 501.
+ assertResponseStatus(SC_NOT_IMPLEMENTED);
+ }
+
+ private void assertResponseStatus(int status) {
+ assertEquals("Invalid Status for test: " + testMessage, status, response.getStatus());
+ }
+
+ /**
+ * Prevent SPARQL from actually running by returning a mocked response.
+ * @throws IOException
+ */
+ protected void mockSparqlResponseEmptySuccess() throws IOException {
+ String json = readMockFile("sparql/response/json/sparql-empty-success.json");
+ InputStream stream = new ByteArrayInputStream(json.getBytes());
+ when(queryExecution.execSelect()).thenReturn(ResultSetFactory.fromJSON(stream));
+ }
+
+ @Parameterized.Parameters
+ public static Collection