From ea7a090b970965b7490f841f2f95249177db8332 Mon Sep 17 00:00:00 2001 From: Sandra Mierz Date: Wed, 13 Oct 2021 16:56:55 +0200 Subject: [PATCH 01/19] Reporting tool, VQT takeover from graham rm unimplemented or unnecessary classes rm unused distributors added us text for vqt use local server port to make new request instead of port used by the client fix self signed certificate issue Renamed german translation properties checkstyle fixes --- api/pom.xml | 5 + .../api/DistributeDataApiController.java | 158 +++ .../distribute/AbstractDataDistributor.java | 30 + .../api/distribute/DataDistributor.java | 171 +++ .../distribute/DataDistributorContext.java | 97 ++ .../DataDistributorContextImpl.java | 40 + .../rdf/AbstractSparqlBindingDistributor.java | 38 + .../distribute/rdf/RdfGraphDistributor.java | 41 + .../rdf/SelectFromContentDistributor.java | 97 ++ .../rdf/SelectFromGraphDistributor.java | 113 ++ .../graphbuilder/AbstractGraphBuilder.java | 20 + .../AbstractSparqlBindingGraphBuilder.java | 37 + .../ConstructQueryGraphBuilder.java | 112 ++ .../graphbuilder/DrillDownGraphBuilder.java | 221 ++++ .../rdf/graphbuilder/EmptyGraphBuilder.java | 20 + .../rdf/graphbuilder/GraphBuilder.java | 23 + .../graphbuilder/GraphBuilderUtilities.java | 97 ++ .../graphbuilder/IteratingGraphBuilder.java | 145 +++ .../distribute/rdf/util/VariableBinder.java | 74 ++ .../auth/permissions/SimplePermission.java | 3 + .../DataDistributorConfigController.java | 376 ++++++ .../controller/admin/ReportingController.java | 490 ++++++++ .../freemarker/BaseSiteAdminController.java | 2 + .../vitro/webapp/dao/DataDistributorDao.java | 89 ++ .../vitro/webapp/dao/ReportingDao.java | 51 + .../vitro/webapp/dao/WebappDaoFactory.java | 3 + .../filtering/WebappDaoFactoryFiltering.java | 25 +- .../dao/jena/DataDistributorDaoJena.java | 254 ++++ .../webapp/dao/jena/ReportingDaoJena.java | 271 +++++ .../webapp/dao/jena/WebappDaoFactoryJena.java | 27 +- .../rdfservice/filter/LangAwareOntModel.java | 38 + .../webapp/reporting/AbstractReport.java | 93 ++ .../reporting/AbstractTemplateReport.java | 50 + .../reporting/AbstractYARGTemplateReport.java | 94 ++ .../reporting/DataDistributorEndpoint.java | 46 + .../vitro/webapp/reporting/DataSource.java | 94 ++ .../webapp/reporting/OpenDopeWordReport.java | 135 +++ .../webapp/reporting/ReportGenerator.java | 55 + .../reporting/ReportGeneratorException.java | 24 + .../webapp/reporting/TemplateExcelReport.java | 23 + .../webapp/reporting/TemplateWordReport.java | 23 + .../vitro/webapp/reporting/XmlGenerator.java | 13 + .../utils/configuration/PropertyType.java | 544 +++++---- .../utils/configuration/WrappedInstance.java | 4 +- .../webapp/dao/WebappDaoFactoryStub.java | 517 ++++---- dependencies/pom.xml | 26 + .../rdf/auth/everytime/permission_config.n3 | 5 + pom.xml | 9 + webapp/src/main/webapp/css/reporting.css | 13 + webapp/src/main/webapp/images/run.png | Bin 0 -> 240 bytes .../src/main/webapp/local/i18n/all.properties | 5 - .../webapp/local/i18n/all_de_DE.properties | 101 ++ .../webapp/local/i18n/all_en_US.properties | 1058 +++++++++++++++++ ...n-dataDistributor-edit-DataDistributor.ftl | 176 +++ .../body/admin/admin-dataDistributor.ftl | 71 ++ .../admin/admin-reporting-edit-report.ftl | 126 ++ .../freemarker/body/admin/admin-reporting.ftl | 46 + .../siteAdmin/siteAdmin-siteConfiguration.ftl | 8 + 58 files changed, 5969 insertions(+), 558 deletions(-) create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/AbstractDataDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContext.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/AbstractSparqlBindingDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/RdfGraphDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractSparqlBindingGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrillDownGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilities.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinder.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/DataDistributorConfigController.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DataDistributorDao.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/ReportingDao.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/DataDistributorDaoJena.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ReportingDaoJena.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LangAwareOntModel.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractTemplateReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataDistributorEndpoint.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGeneratorException.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java create mode 100644 api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java create mode 100644 webapp/src/main/webapp/css/reporting.css create mode 100644 webapp/src/main/webapp/images/run.png delete mode 100644 webapp/src/main/webapp/local/i18n/all.properties create mode 100644 webapp/src/main/webapp/local/i18n/all_de_DE.properties create mode 100644 webapp/src/main/webapp/local/i18n/all_en_US.properties create mode 100644 webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor-edit-DataDistributor.ftl create mode 100644 webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor.ftl create mode 100644 webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting-edit-report.ftl create mode 100644 webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting.ftl diff --git a/api/pom.xml b/api/pom.xml index ae0bed6ff3..c031ab41d6 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -91,6 +91,11 @@ 1.15.1-SNAPSHOT pom + + org.reflections + reflections + 0.9.11 + javax.servlet javax.servlet-api diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java new file mode 100644 index 0000000000..dca406a8bd --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java @@ -0,0 +1,158 @@ +///* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api; + +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY; +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.List; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.ActionFailedException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.MissingParametersException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.NoSuchActionException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.NotAuthorizedException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextImpl; +import edu.cornell.mannlib.vitro.webapp.controller.api.VitroApiServlet; +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; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; + +/** + * Find a distributor description for the requested action. Create an instance + * of that distributor. Write its data to the HTTP response. + */ +@WebServlet(name = "DistributeDataApi", urlPatterns = { "/api/dataRequest/*" }) +public class DistributeDataApiController extends VitroApiServlet { + private static final Log log = LogFactory.getLog(DistributeDataApiController.class); + + private static final String DISTRIBUTOR_FOR_SPECIFIED_ACTION = "" + + "PREFIX : \n" + "SELECT ?distributor \n" // + + "WHERE { \n" // + + " ?distributor :actionName ?action . \n" // + + "} \n"; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + Model model = ModelAccess.on(req).getOntModel(DISPLAY); + + String uri = findDistributorForAction(req, model); + DataDistributor instance = instantiateDistributor(req, uri, model); + setCorsHeaders(req, resp); + runIt(req, resp, instance); + } catch (NoSuchActionException e) { + do400BadRequest(e.getMessage(), resp); + } catch (MissingParametersException e) { + do400BadRequest(e.getMessage(), resp); + } catch (NotAuthorizedException e) { + do403Forbidden(resp); + } catch (Exception e) { + do500InternalServerError(e.getMessage(), e, resp); + } + } + + private String findDistributorForAction(HttpServletRequest req, Model model) throws NoSuchActionException { + String action = req.getPathInfo(); + if (action == null || action.isEmpty()) { + throw new NoSuchActionException("'action' path was not provided."); + } + if (action.startsWith("/")) { + action = action.substring(1); + } + + List uris = createSelectQueryContext(model, DISTRIBUTOR_FOR_SPECIFIED_ACTION) + .bindVariableToPlainLiteral("action", action).execute().toStringFields("distributor").flatten(); + Collections.sort(uris); + log.debug("Found URIs for action '" + action + "': " + uris); + + if (uris.isEmpty()) { + throw new NoSuchActionException("Did not find a DataDistributor for '" + action + "'"); + } + if (uris.size() > 1) { + log.warn("Found more than one DataDistributor for '" + action + "': " + uris); + } + + return uris.get(0); + } + + private DataDistributor instantiateDistributor(HttpServletRequest req, String distributorUri, Model model) + throws ActionFailedException { + try { + return new ConfigurationBeanLoader(model, req).loadInstance(distributorUri, DataDistributor.class); + } catch (ConfigurationBeanLoaderException e) { + throw new ActionFailedException("Failed to instantiate the DataDistributor: " + distributorUri, e); + } + } + + private void runIt(HttpServletRequest req, HttpServletResponse resp, DataDistributor instance) + throws DataDistributorException { + try { + instance.init(new DataDistributorContextImpl(req)); + log.debug("Distributor is " + instance); + + resp.setContentType(instance.getContentType()); + resp.setCharacterEncoding("UTF-8"); + instance.writeOutput(resp.getOutputStream()); + } catch (Exception e) { + log.error("Failed to execute the DataDistributor", e); + instance.close(); + throw new ActionFailedException(e); + } + } + + private void setCorsHeaders(HttpServletRequest req, HttpServletResponse resp) { + log.debug("Setting CORS header for every request."); + resp.setHeader("Access-Control-Allow-Origin", "*"); + } + + private void do400BadRequest(String message, HttpServletResponse resp) throws IOException { + log.debug("400BadRequest: " + message); + resp.setStatus(400); + resp.getWriter().println(message); + } + + private void do403Forbidden(HttpServletResponse resp) throws IOException { + log.debug("403Forbidden"); + resp.setStatus(403); + resp.getWriter().println("Not authorized for this action."); + } + + private void do500InternalServerError(String message, Exception e, HttpServletResponse resp) throws IOException { + log.warn("500InternalServerError " + message, e); + resp.setStatus(500); + try { + PrintWriter w = resp.getWriter(); + w.println(message); + e.printStackTrace(w); + } catch (IllegalStateException e1) { + OutputStream os = resp.getOutputStream(); + os.write((message + "\n").getBytes()); + e.printStackTrace(new PrintStream(os)); + } + + } + + /** + * If you want to use a post form, go ahead. + */ + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + doGet(req, resp); + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/AbstractDataDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/AbstractDataDistributor.java new file mode 100644 index 0000000000..e9e9e737a2 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/AbstractDataDistributor.java @@ -0,0 +1,30 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute; + +import java.util.Map; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +/** + * TODO + */ +public abstract class AbstractDataDistributor implements DataDistributor { + private static final String ACTION_NAME_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName"; + + protected DataDistributorContext ddContext; + protected Map parameters; + protected String actionName; + + @Override + public void init(DataDistributorContext ddc) throws DataDistributorException { + this.ddContext = ddc; + this.parameters = ddc.getRequestParameters(); + } + + @Property(uri = ACTION_NAME_PROPERTY, minOccurs = 1, maxOccurs = 1) + public void setActionName(String aName) { + this.actionName = aName; + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributor.java new file mode 100644 index 0000000000..3313bf2940 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributor.java @@ -0,0 +1,171 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute; + +import java.io.OutputStream; + +/** + * Life-cycle of a DataDistributor: + *
    + *
  • Instantiated to service a single HTTP request.
  • + *
  • init() -- Called one time.
  • + *
  • getContentType() -- Called one time.
  • + *
  • writeOutput() -- Called one time.
  • + *
  • close() -- Called exactly once.
  • + *
  • Garbage-collected (probably)
  • + *
+ */ +public interface DataDistributor { + /** + * Called exactly once, after instantiation is complete. + * + * @param ddContext provides access to requrest parameters and triple-store + * content. + * @throws DataDistributorException + */ + void init(DataDistributorContext ddContext) throws DataDistributorException; + + /** + * States the MIME type of the output from this instance. The MIME type may be + * hardcoded, derived from the configuration, or derived from the request + * parameters. + * + * Called exactly once, after init(). + * + * @return the MIME type of the (expected) output. + * @throws DataDistributorException + */ + String getContentType() throws DataDistributorException; + + /** + * Writes to the output stream (does not close it). This output will become the + * body of the HTTP response. + * + * Called no more than once, after getContentType(). + * + * Might not be called if a previous method throws an exception, or if the + * content type is not acceptable to the requestor. + * + * @param output + * @throws DataDistributorException + */ + void writeOutput(OutputStream output) throws DataDistributorException; + + /** + * Called exactly once. + * + * @throws DataDistributorException + */ + void close() throws DataDistributorException; + + /** + * A problem occurred while creating or running the DataDistributor. + */ + public class DataDistributorException extends Exception { + public DataDistributorException() { + super(); + } + + public DataDistributorException(String message) { + super(message); + } + + public DataDistributorException(Throwable cause) { + super(cause); + } + + public DataDistributorException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * The controller could not find a DataDistributor configured for the requested + * action. + */ + public class NoSuchActionException extends DataDistributorException { + public NoSuchActionException() { + super(); + } + + public NoSuchActionException(String message) { + super(message); + } + + public NoSuchActionException(Throwable cause) { + super(cause); + } + + public NoSuchActionException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * The user registered to the current HTTP session may not run the + * DataDistributor for the requested action. + * + * @see DataDistributorContext#isAuthorized( + * edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest) + */ + public class NotAuthorizedException extends DataDistributorException { + public NotAuthorizedException() { + super(); + } + + public NotAuthorizedException(String message) { + super(message); + } + + public NotAuthorizedException(Throwable cause) { + super(cause); + } + + public NotAuthorizedException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * The HTTP request did not contain one or more required parameters. + */ + public class MissingParametersException extends DataDistributorException { + public MissingParametersException() { + super(); + } + + public MissingParametersException(String message) { + super(message); + } + + public MissingParametersException(Throwable cause) { + super(cause); + } + + public MissingParametersException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * A problem occurred while creating the output. + */ + public class ActionFailedException extends DataDistributorException { + public ActionFailedException() { + super(); + } + + public ActionFailedException(String message) { + super(message); + } + + public ActionFailedException(Throwable cause) { + super(cause); + } + + public ActionFailedException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContext.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContext.java new file mode 100644 index 0000000000..7bc5e515b3 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContext.java @@ -0,0 +1,97 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; + +/** + * Make this information available to each DataDistributor instance. + */ +public interface DataDistributorContext { + /** + * The parameter map on the HTTP request for which the instance was created. + * + * Each value is a non-empty array. Null values, or zero-length arrays are not + * allowed. Arrays may not contain nulls, but may contain empty strings. + */ + Map getRequestParameters(); + + /** + * The data structures that provide access to the content of the triple-store. + * + * These structures are associated with the current HTTP request. + */ + RequestModelAccess getRequestModels(); + + /** + * Permits the DataDistributor instance to check whether the current user is + * authorized for a given activity, such as accessing unfiltered data or + * accessing the configuration models. + * + * For example, before a DataDistributor releases information about user + * accounts, it should execute a check of this nature: + * + *
+     * if (!ddc.isAuthorized(SimplePermission.QUERY_USER_ACCOUNTS_MODEL.ACTION)) {
+     *     throw new DataDistributor.NotAuthorizedException();
+     * }
+     * 
+ * + * It is probably best to execute this code in the DataDistributor's init() or + * writeOutput() methods. + * + * @param actions The activities for which the user must be authorized, or the + * DataDistributor will not execute. + */ + boolean isAuthorized(AuthorizationRequest actions); + + // ---------------------------------------------------------------------- + // Utility methods + // ---------------------------------------------------------------------- + + /** + * Convert a parameter map to its list-based equivalent. + */ + public static Map> arraysToLists(Map arrays) { + Map> lists = new HashMap<>(); + for (String key : arrays.keySet()) { + lists.put(key, new ArrayList<>(Arrays.asList(arrays.get(key)))); + } + return lists; + } + + /** + * Convert a list-based parameter map back to its canonical form. + */ + public static Map listsToArrays(Map> lists) { + Map arrays = new HashMap<>(); + for (String key : lists.keySet()) { + List values = lists.get(key); + arrays.put(key, values.toArray(new String[values.size()])); + } + return arrays; + } + + /** + * Create a copy of the parameter map: changes to the copy don't affect the + * original. + */ + public static Map deepCopyParameters(Map map) { + return listsToArrays(arraysToLists(map)); + } + + /** + * Format the parameter map, suitable for logging. + */ + public static String formatParameters(DataDistributorContext ddContext) { + return arraysToLists(ddContext.getRequestParameters()).toString(); + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java new file mode 100644 index 0000000000..aa1c10f714 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java @@ -0,0 +1,40 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; + +/** + * Build a DataDistributorContext around the current HTTP request. + */ +public class DataDistributorContextImpl implements DataDistributorContext { + private final HttpServletRequest req; + + public DataDistributorContextImpl(HttpServletRequest req) { + this.req = req; + } + + @SuppressWarnings("unchecked") + @Override + public Map getRequestParameters() { + return req.getParameterMap(); + } + + @Override + public RequestModelAccess getRequestModels() { + return ModelAccess.on(req); + } + + @Override + public boolean isAuthorized(AuthorizationRequest actions) { + return PolicyHelper.isAuthorizedForActions(req, actions); + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/AbstractSparqlBindingDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/AbstractSparqlBindingDistributor.java new file mode 100644 index 0000000000..e8ba8760c2 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/AbstractSparqlBindingDistributor.java @@ -0,0 +1,38 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import java.util.HashSet; +import java.util.Set; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.util.VariableBinder; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +/** + * Keep track of the binding details for DataDistributors that bind request + * parameters into SPARQL queries. + */ +public abstract class AbstractSparqlBindingDistributor extends AbstractDataDistributor { + protected VariableBinder binder; + + protected Set uriBindingNames = new HashSet<>(); + protected Set literalBindingNames = new HashSet<>(); + + @Override + public void init(DataDistributorContext ddContext) throws DataDistributorException { + super.init(ddContext); + this.binder = new VariableBinder(ddContext.getRequestParameters()); + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#uriBinding") + public void addUriBindingName(String uriBindingName) { + this.uriBindingNames.add(uriBindingName); + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#literalBinding") + public void addLiteralBindingName(String literalBindingName) { + this.literalBindingNames.add(literalBindingName); + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/RdfGraphDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/RdfGraphDistributor.java new file mode 100644 index 0000000000..6dcc013f22 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/RdfGraphDistributor.java @@ -0,0 +1,41 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.GraphBuilders; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +/** + * Execute one or more GraphBuilders, merge the results, and write them out as + * Turtle RDF. + */ +public class RdfGraphDistributor extends AbstractDataDistributor { + private List graphBuilders = new ArrayList<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#graphBuilder", minOccurs = 1) + public void addGraphBuilder(GraphBuilder builder) { + graphBuilders.add(builder); + } + + @Override + public String getContentType() throws DataDistributorException { + return "text/turtle"; + } + + @Override + public void writeOutput(OutputStream output) throws DataDistributorException { + new GraphBuilders(ddContext, graphBuilders).run().write(output, "TTL"); + } + + @Override + public void close() throws DataDistributorException { + // Nothing to close. + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributor.java new file mode 100644 index 0000000000..6aa8ef9585 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributor.java @@ -0,0 +1,97 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.io.OutputStream; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; + +/** + * Issue a SPARQL SELECT query against VIVO's content models and return the + * results as JSON. You provide: + *
    + *
  • the action name
  • + *
  • the query string
  • + *
  • names of request parameters whose values will be bound as URIs in the + * query
  • + *
  • names of request parameters whose values will be bound as plain literals + * in the query
  • + *
+ * + * So if the configuration looks like this: + * + *
+ * :sample_select_from_content_distributor
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor> ;
+ *     :actionName "sampleAction" ;
+ *     :query """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       SELECT ?article
+ *       WHERE {
+ *         ?person foo:isAuthor ?article .
+ *         ?article foo:hasTopic ?topic .
+ *       }
+ *     """ ;
+ *     :uriBinding "person" ;
+ *     :literalBinding "topic" .
+ * 
+ * + * Then this request: + * + *
+ *    dataRequest/sampleAction?person=http%3A%2F%2Fmy.domain.edu%2Findividual%2Fn1234&topic=Oncology
+ * 
+ * + * Will execute this query: + * + *
+ *    PREFIX foo: <http://some.silly.domain/foo#>
+ *    SELECT ?article
+ *    WHERE {
+ *      <http://my.domain.edu/individual/n1234> foo:isAuthor ?article .
+ *      ?article foo:hasTopic "Oncology" .
+ *    }
+ * 
+ * + * Each specified binding name must have exactly one value in the request + * parameters. + */ +public class SelectFromContentDistributor extends AbstractSparqlBindingDistributor { + private RequestModelAccess models; + private String rawQuery; + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query", minOccurs = 1, maxOccurs = 1) + public void setRawQuery(String query) { + rawQuery = query; + } + + @Override + public void init(DataDistributorContext ddc) throws DataDistributorException { + super.init(ddc); + this.models = ddc.getRequestModels(); + } + + @Override + public String getContentType() throws DataDistributorException { + return "application/sparql-results+json"; + } + + @Override + public void writeOutput(OutputStream output) throws DataDistributorException { + QueryHolder boundQuery = binder.bindValuesToQuery(uriBindingNames, literalBindingNames, + new QueryHolder(rawQuery)); + createSelectQueryContext(this.models.getRDFService(), boundQuery).execute().writeToOutput(output); + } + + @Override + public void close() throws DataDistributorException { + // Nothing to do. + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributor.java new file mode 100644 index 0000000000..ee0a1fb2f0 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributor.java @@ -0,0 +1,113 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.GraphBuilders; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; +import org.apache.jena.rdf.model.Model; + +/** + * Issue a SPARQL SELECT query against an internal graph, and return the results + * as JSON. You provide: + *
    + *
  • the action name
  • + *
  • the query string
  • + *
  • names of request parameters whose values will be bound as URIs in the + * query
  • + *
  • names of request parameters whose values will be bound as plain literals + * in the query
  • + *
  • one or more graph builders; the SELECT query will run against the union + * of their output.
  • + *
+ * + * For details of the variable binding, see + * {@link SelectFromContentDistributor}. + *

+ * So if the configuration looks like this: + * + *

+ * :sample_select_from_graph_distributor
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor> ;
+ *     :actionName "sampleAction" ;
+ *     :query """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       SELECT ?article
+ *       WHERE {
+ *         ?person foo:isAuthor ?article .
+ *       }
+ *     """ ;
+ *     :uriBinding "person" ;
+ *     :graphBuilder :empty_graph_builder .
+ *
+ * :empty_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         EmptyGraphBuilder> .
+ * 
+ * + * Then this request: + * + *
+ *    dataRequest/sampleAction?person=http%3A%2F%2Fmy.domain.edu%2Findividual%2Fn1234
+ * 
+ * + * Will execute this query: + * + *
+ *    PREFIX foo: <http://some.silly.domain/foo#>
+ *    SELECT ?article
+ *    WHERE {
+ *      <http://my.domain.edu/individual/n1234> foo:isAuthor ?article .
+ *    }
+ * 
+ * + * against an empty graph, producing an empty result. + */ +public class SelectFromGraphDistributor extends AbstractSparqlBindingDistributor { + private String rawQuery; + private List graphBuilders = new ArrayList<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query", minOccurs = 1, maxOccurs = 1) + public void setRawQuery(String query) { + rawQuery = query; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#graphBuilder", minOccurs = 1) + public void addGraphBuilder(GraphBuilder builder) { + graphBuilders.add(builder); + } + + @Override + public void init(DataDistributorContext ddc) throws DataDistributorException { + super.init(ddc); + } + + @Override + public String getContentType() throws DataDistributorException { + return "application/sparql-results+json"; + } + + @Override + public void writeOutput(OutputStream output) throws DataDistributorException { + QueryHolder boundQuery = binder.bindValuesToQuery(uriBindingNames, literalBindingNames, + new QueryHolder(rawQuery)); + Model graph = new GraphBuilders(ddContext, graphBuilders).run(); + createSelectQueryContext(graph, boundQuery).execute().writeToOutput(output); + } + + @Override + public void close() throws DataDistributorException { + // Nothing to do. + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractGraphBuilder.java new file mode 100644 index 0000000000..de473df231 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractGraphBuilder.java @@ -0,0 +1,20 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +public abstract class AbstractGraphBuilder implements GraphBuilder { + private static final String BUILDER_NAME_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName"; + + protected String builderName; + + public String getBuilderName() { + return builderName; + } + + @Property(uri = BUILDER_NAME_PROPERTY, minOccurs = 1, maxOccurs = 1) + public void setBuilderName(String name) { + this.builderName = name; + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractSparqlBindingGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractSparqlBindingGraphBuilder.java new file mode 100644 index 0000000000..39cb1e82ff --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/AbstractSparqlBindingGraphBuilder.java @@ -0,0 +1,37 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import java.util.HashSet; +import java.util.Set; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.MissingParametersException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.util.VariableBinder; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; + +/** + * Keep track of the binding details for DataDistributors that bind request + * parameters into SPARQL queries. + */ +public abstract class AbstractSparqlBindingGraphBuilder extends AbstractGraphBuilder { + protected Set uriBindingNames = new HashSet<>(); + protected Set literalBindingNames = new HashSet<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#uriBinding") + public void addUriBindingName(String uriBindingName) { + this.uriBindingNames.add(uriBindingName); + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#literalBinding") + public void addLiteralBindingName(String literalBindingName) { + this.literalBindingNames.add(literalBindingName); + } + + protected QueryHolder bindParametersToQuery(DataDistributorContext ddContext, QueryHolder rawQuery) + throws MissingParametersException { + return new VariableBinder(ddContext.getRequestParameters()).bindValuesToQuery(uriBindingNames, + literalBindingNames, rawQuery); + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilder.java new file mode 100644 index 0000000000..be3cd7f20c --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilder.java @@ -0,0 +1,112 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.formatParameters; +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createConstructQueryContext; + +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor; +import edu.cornell.mannlib.vitro.webapp.rdfservice.RDFService; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Run one or more construct queries to build the graph. Bind parameters from + * the request, as needed. You provide: + * + *
    + *
  • the query string(s)
  • + *
  • names of request parameters whose values will be bound as URIs in the + * query
  • + *
  • names of request parameters whose values will be bound as plain literals + * in the query
  • + *
+ * + * For details of the variable binding, see + * {@link SelectFromContentDistributor}. + * + *

+ * So if the configuration looks like this: + * + *

+ * :sample_distributor
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor> ;
+ *     :actionName "sampleAction" ;
+ *     :graphBuilder :construct_graph_builder .
+ *
+ * :construct_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.
+ *         distribute.rdf.graphbuilder.ConstructQueryGraphBuilder> ;
+ *     :literalBinding "subjectArea" ;
+ *     :constructQuery """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword ?subjectArea .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword ?subjectArea .
+ *       }
+ *     """ .
+ * 
+ * + * Then this request: + * + *
+ *    dataRequest/sampleAction?subjectArea=cancer
+ * 
+ * + * Will execute this query: + * + *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword "cancer" .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword "cancer" .
+ *       }
+ * 
+ * + * against the VIVO content model, distributing the result as RDF in Turtle + * format. + * + * If the constructQuery property is repeated, the bindings will apply to all + * queries. The sequence of execution of the queries is indeterminate. + */ +public class ConstructQueryGraphBuilder extends AbstractSparqlBindingGraphBuilder { + private static final Log log = LogFactory.getLog(ConstructQueryGraphBuilder.class); + + private List rawQueries = new ArrayList<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#constructQuery", minOccurs = 1) + public void addRawQuery(String query) { + rawQueries.add(query); + } + + @Override + public Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException { + log.debug("Parameters: " + formatParameters(ddContext)); + + RDFService rdfService = ddContext.getRequestModels().getRDFService(); + Model m = ModelFactory.createDefaultModel(); + + for (String rawQuery : rawQueries) { + QueryHolder query = bindParametersToQuery(ddContext, new QueryHolder(rawQuery)); + log.debug("Query is: " + query); + + m.add(createConstructQueryContext(rdfService, query).execute().toModel()); + } + return m; + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrillDownGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrillDownGraphBuilder.java new file mode 100644 index 0000000000..76184263db --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrillDownGraphBuilder.java @@ -0,0 +1,221 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.EnhancedDataDistributionContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.GraphBuilders; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +/** + * A decorator that runs one or more subordinate graph builders zero or more + * times, based the results of a SPARQL SELECT query. The results are + * accumulated and returned as a single RDF graph. + *

+ * This is similar in concept to an {@link IteratingGraphBuilder}, but the + * iteration values are not configured; instead, they are discovered. + *

+ * To discover the iteration values, the builder run one or more "top-level" + * graph builders and runs a SELECT query against the resulting graph. The + * results of the SELECT query determine how many times the "child" graph + * builders are run, and what values will be added to their parameter maps on + * each iteration. + *

+ * The result is the union of the graphs from the top-level graph builders and + * the child graph builders. + *

+ * + * NOTE: if a named parameter from the SELECT results already appears with one + * or more values in the parameter map, then each value provided here will be + * added in turn to the array of values for that parameter. + * + * + *

+ * You provide: + * + *

    + *
  • one or more top-level graph builders, to be used in the SELECT query
  • + *
  • the SELECT query string
  • + *
  • names of request parameters whose values will be bound as URIs in the + * query
  • + *
  • names of request parameters whose values will be bound as plain literals + * in the query
  • + *
  • one or more child (subordinate) graph builders
  • + *
+ * + * For details of the variable binding, see + * {@link SelectFromContentDistributor}. + * + *

+ * Consider a configuration that looks like this: + * + *

+ * :sample_distributor
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor> ;
+ *     :actionName "sampleAction" ;
+ *     :graphBuilder :drilldown_graph_builder .
+ *
+ * :drilldown_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         DrillDownGraphBuilder> ;
+ *     :topLevelGraphBuilder :top_level_builder ;
+ *     :childGraphBuilder :child_graph_builder ;
+ *     :drillDownQuery """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       SELECT ?article ?subjectArea
+ *       WHERE {
+ *         foo:author_123 foo:writes ?article ;
+ *         ?article foo:pertainsTo ?subjectArea .
+ *       }
+ *     """ .
+ *
+ * :top_level_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         ConstructQueryGraphBuilder> .
+ *     :literalBinding "subjectArea" ;
+ *     :constructQuery """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?author foo:writes ?article ;
+ *         ?article foo:pertainsTo ?subjectArea .
+ *       }
+ *       WHERE {
+ *         ?author foo:writes ?article ;
+ *         ?article ?property ?subjectArea .
+ *       }
+ *     """ .
+ *
+ * :child_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         ConstructQueryGraphBuilder> .
+ *     :literalBinding "subjectArea" ;
+ *     :constructQuery """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article ?property ?subjectArea .
+ *       }
+ *       WHERE {
+ *         ?article ?property ?subjectArea .
+ *       }
+ *     """ .
+ * 
+ * + * When this request is received: + * + *
+ * dataRequest / sampleAction
+ * 
+ * + *
    + *
  • The top-level graph builder will extract all author/article/subjectArea + * triples into a local graph.
  • + *
  • The drillDownQuery will obtain the article/subjectArea + * information for a particular author. + *
  • The child graph builder will be run several times, once for each row in + * the result set of the drillDownQuery. + *
+ * + * So, if the drillDownQuery returns these results: + * + *
+ * article = http://some.silly.domain/foo#article_142 subjectArea = cancer
+ * article = http://some.silly.domain/foo#article_207 subjectArea = tissues
+ * 
+ * + * Then the child graph builder will behave as if it had received these two + * requests: + * + *
+ *    dataRequest/sampleAction?subjectArea=cancer&article=http://some.silly.domain/foo#article_142
+ *    dataRequest/sampleAction?subjectArea=tissues&article=http://some.silly.domain/foo#article_207
+ * 
+ * + * and will execute these two queries against the VIVO content model: + * + *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         <http://some.silly.domain/foo#article_142> ?property "cancer" .
+ *       }
+ *       WHERE {
+ *         <http://some.silly.domain/foo#article_142> ?property "cancer" .
+ *       }
+ *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         <http://some.silly.domain/foo#article_207> ?property "tissues" .
+ *       }
+ *       WHERE {
+ *         <http://some.silly.domain/foo#article_207> ?property "tissues" .
+ *       }
+ * 
+ * + * The results of all builders, top-level and children, will be merged into a + * single graph, and distributed as RDF in Turtle format. + */ +public class DrillDownGraphBuilder extends AbstractSparqlBindingGraphBuilder { + private static final Log log = LogFactory.getLog(DrillDownGraphBuilder.class); + private List topGraphBuilders = new ArrayList<>(); + private List childGraphBuilders = new ArrayList<>(); + private String drillDownQuery; + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#topLevelGraphBuilder", + minOccurs = 1) + public void addTopLevelGraphBuilder(GraphBuilder builder) { + topGraphBuilders.add(builder); + } + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#childGraphBuilder", + minOccurs = 1) + public void addChildGraphBuilder(GraphBuilder builder) { + childGraphBuilders.add(builder); + } + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#drillDownQuery", minOccurs = 1, + maxOccurs = 1) + public void setDrillDownQuery(String query) { + drillDownQuery = query; + } + @Override + public Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException { + Model topGraph = new GraphBuilders(ddContext, topGraphBuilders).run(); + log.debug("Size of topGraph: " + topGraph.size()); + List> valueMaps = findDrillDownValues(topGraph, ddContext); + log.debug("Results from SELECT: " + valueMaps); + Model childBuildersGraph = runChildBuildersWithDrillDownValues(ddContext, valueMaps); + return topGraph.add(childBuildersGraph); + } + private List> findDrillDownValues(Model topGraph, DataDistributorContext ddContext) + throws DataDistributorException { + QueryHolder query = bindParametersToQuery(ddContext, new QueryHolder(drillDownQuery)); + log.debug("binding the query\n " + drillDownQuery + " becomes " + query.getQueryString()); + return createSelectQueryContext(topGraph, query).execute().toStringFields().getListOfMaps(); + } + private Model runChildBuildersWithDrillDownValues(DataDistributorContext ddContext, + List> valueMaps) throws DataDistributorException { + Model resultGraph = ModelFactory.createDefaultModel(); + for (Map parameterSet : valueMaps) { + resultGraph.add(runChildBuilders(ddContext, parameterSet)); + } + return resultGraph; + } + private Model runChildBuilders(DataDistributorContext ddContext, Map parameterSet) + throws DataDistributorException { + return new GraphBuilders(enhanceContext(ddContext, parameterSet), childGraphBuilders).run(); + } + private EnhancedDataDistributionContext enhanceContext(DataDistributorContext ddContext, + Map parameterSet) { + return new EnhancedDataDistributionContext(ddContext).addParameterValues(parameterSet); + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java new file mode 100644 index 0000000000..c4cf0cdccd --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java @@ -0,0 +1,20 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Creates an empty RDF graph. For use as a placeholder or in tests. + */ +public class EmptyGraphBuilder extends AbstractGraphBuilder { + + @Override + public Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException { + return ModelFactory.createDefaultModel(); + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilder.java new file mode 100644 index 0000000000..ea281e76bb --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilder.java @@ -0,0 +1,23 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import org.apache.jena.rdf.model.Model; + +/** + * Creates a local RDF graph to be distributed, or to use as the context for + * queries. + */ +public interface GraphBuilder { + /** + * Call zero or more times. Each call should initialize as necessary and close + * resources on completion. + */ + Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException; + + String getBuilderName(); + + void setBuilderName(String name); +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilities.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilities.java new file mode 100644 index 0000000000..7c65c0e033 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilities.java @@ -0,0 +1,97 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.arraysToLists; +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.listsToArrays; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * Helpful classes and methods for dealing with GraphBuilders. + */ +public class GraphBuilderUtilities { + private static final Log log = LogFactory.getLog(GraphBuilderUtilities.class); + + /** + * Run a collection of GraphBuilders and produce a merged result. + */ + public static class GraphBuilders { + private final DataDistributorContext ddContext; + private final List builders; + + public GraphBuilders(DataDistributorContext ddContext, Collection builders) { + this.ddContext = ddContext; + this.builders = Collections.unmodifiableList(new ArrayList<>(builders)); + } + + public Model run() throws DataDistributorException { + Model graph = ModelFactory.createDefaultModel(); + for (GraphBuilder builder : builders) { + graph.add(runBuilder(builder)); + log.debug("Graph size is " + graph.size()); + } + return graph; + } + + private Model runBuilder(GraphBuilder builder) throws DataDistributorException { + return builder.buildGraph(ddContext); + } + } + + public static class EnhancedDataDistributionContext implements DataDistributorContext { + private final DataDistributorContext inner; + private final Map> enhancedParameters; + + public EnhancedDataDistributionContext(DataDistributorContext inner) { + this.inner = inner; + this.enhancedParameters = arraysToLists(inner.getRequestParameters()); + } + + public EnhancedDataDistributionContext addParameterValue(String key, String value) { + List values = enhancedParameters.get(key); + if (values == null) { + values = new ArrayList<>(); + } + values.add(value); + enhancedParameters.put(key, values); + return this; + } + + public EnhancedDataDistributionContext addParameterValues(Map map) { + for (String key : map.keySet()) { + addParameterValue(key, map.get(key)); + } + return this; + } + + @Override + public Map getRequestParameters() { + return listsToArrays(enhancedParameters); + } + + @Override + public RequestModelAccess getRequestModels() { + return inner.getRequestModels(); + } + + @Override + public boolean isAuthorized(AuthorizationRequest actions) { + return inner.isAuthorized(actions); + } + + } +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilder.java new file mode 100644 index 0000000000..1a95d3c831 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilder.java @@ -0,0 +1,145 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.EnhancedDataDistributionContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.GraphBuilders; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +/** + * A decorator that runs one or more subordinate graph builders multiple times, + * modifying the map of request parameters each time. The results are + * accumulated and returned as a single RDF graph. + * + * You provide: + * + *
    + *
  • the name of the request parameter that will be added to the map
  • + *
  • one or more values for the request parameter
  • + *
  • one or more subordinate graph builders
  • + *
+ * + * NOTE: if the named parameter already appears with one or more values in the + * parameter map, then each value provided here will be added in turn to the + * array of values for that parameter. + * + *

+ * So if the configuration looks like this: + * + *

+ * :sample_distributor
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor> ;
+ *     :actionName "sampleAction" ;
+ *     :graphBuilder :iterating_graph_builder .
+ *
+ * :iterating_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         IteratingGraphBuilder> .
+ *     :parameterName "subjectArea" ;
+ *     :parameterValue "cancer", "tissues", "echidnae" ;
+ *     :childGraphBuilder :subordinate_graph_builder .
+ *
+ * :subordinate_graph_builder
+ *     a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder> ,
+ *         <java:edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.
+ *         ConstructQueryGraphBuilder> .
+ *     :literalBinding "subjectArea" ;
+ *     :constructQuery """
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword ?subjectArea .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword ?subjectArea .
+ *       }
+ *     """ .
+ * 
+ * + * Then this request: + * + *
+ * dataRequest / sampleAction
+ * 
+ * + * will be treated by iterator as if it were these three successive requests: + * + *
+ *    dataRequest/sampleAction?subjectArea=cancer
+ *    dataRequest/sampleAction?subjectArea=tissues
+ *    dataRequest/sampleAction?subjectArea=echidnae
+ * 
+ * + * and the subordinate graph builder will execute these three queries against + * the VIVO content model: + * + *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword "cancer" .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword "cancer" .
+ *       }
+ *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword "tissues" .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword "tissues" .
+ *       }
+ *
+ *       PREFIX foo: <http://some.silly.domain/foo#>
+ *       CONSTRUCT {
+ *         ?article foo:hasKeyword "echidnae" .
+ *       }
+ *       WHERE {
+ *         ?article foo:hasKeyword "echidnae" .
+ *       }
+ * 
+ * + * The results will be merged into a single graph, and distributed as RDF in + * Turtle format. + */ + +public class IteratingGraphBuilder extends AbstractGraphBuilder { + private String parameterName; + private final List parameterValues = new ArrayList<>(); + private final List childGraphBuilders = new ArrayList<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName", minOccurs = 1, maxOccurs = 1) + public void setParameterName(String name) { + this.parameterName = name; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterValue", minOccurs = 1) + public void addParameterValue(String value) { + this.parameterValues.add(value); + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#childGraphBuilder", minOccurs = 1) + public void addChildGraphBuilder(GraphBuilder builder) { + this.childGraphBuilders.add(builder); + } + + @Override + public Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException { + Model graph = ModelFactory.createDefaultModel(); + for (String parameterValue : parameterValues) { + DataDistributorContext enhancedContext = new EnhancedDataDistributionContext(ddContext) + .addParameterValue(parameterName, parameterValue); + graph.add(new GraphBuilders(enhancedContext, childGraphBuilders).run()); + } + return graph; + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinder.java new file mode 100644 index 0000000000..53cf32b8fc --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinder.java @@ -0,0 +1,74 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.util; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.MissingParametersException; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; + +/** + * Start with a parameter map, like from an HTTPServletRequest. + * + * Given a query holder and a list of names, find values for each name, and bind + * it to the variable of that name, either as a URI or as a plain literal. + * + * If a parameter is not found, or has multiple values, throw an exception. + */ +public class VariableBinder { + private final Map parameters; + + public VariableBinder(Map parameters) { + Objects.requireNonNull(parameters, "'parameters' must not be null."); + this.parameters = Collections.unmodifiableMap(deepCopy(parameters)); + } + + private Map deepCopy(Map original) { + Map copy = new HashMap<>(); + for (String key : original.keySet()) { + String[] values = original.get(key); + copy.put(key, Arrays.copyOf(values, values.length)); + } + return copy; + } + + public QueryHolder bindValuesToQuery(Set uriBindingNames, Set literalBindingNames, + QueryHolder query) throws MissingParametersException { + Objects.requireNonNull(uriBindingNames, "'uriBindingNames' must not be null."); + Objects.requireNonNull(literalBindingNames, "'literalBindingNames' must not be null."); + Objects.requireNonNull(query, "'query' must not be null."); + return bindUriParameters(uriBindingNames, bindLiteralParameters(literalBindingNames, query)); + } + + private QueryHolder bindUriParameters(Set names, QueryHolder queryHolder) + throws MissingParametersException { + for (String name : names) { + queryHolder = queryHolder.bindToUri(name, getOneParameter(name)); + } + return queryHolder; + } + + private QueryHolder bindLiteralParameters(Set names, QueryHolder queryHolder) + throws MissingParametersException { + for (String name : names) { + queryHolder = queryHolder.bindToPlainLiteral(name, getOneParameter(name)); + } + return queryHolder; + } + + private String getOneParameter(String name) throws MissingParametersException { + String[] uris = parameters.get(name); + if (uris == null || uris.length == 0) { + throw new MissingParametersException("A '" + name + "' parameter is required."); + } else if (uris.length > 1) { + throw new MissingParametersException("Unexpected multiple values for '" + name + "' parameter."); + } + return uris[0]; + } + +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java index a86036dd1e..2cf9864503 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/auth/permissions/SimplePermission.java @@ -63,6 +63,9 @@ public class SimplePermission { public static final SimplePermission PAGE_VIEWABLE_EDITOR = new SimplePermission("PageViewableEditor"); public static final SimplePermission PAGE_VIEWABLE_PUBLIC = new SimplePermission("PageViewablePublic"); + public static final SimplePermission MANAGE_DATA_DISTRIBUTORS = new SimplePermission("ManageDataDistributors"); + public static final SimplePermission EXECUTE_REPORTS = new SimplePermission("ExecuteReports"); + public static final SimplePermission MANAGE_REPORTS = new SimplePermission("ManageReports"); public SimpleAuthorizationRequest ACTION; private String uri; diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/DataDistributorConfigController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/DataDistributorConfigController.java new file mode 100644 index 0000000000..d9dc64b98e --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/DataDistributorConfigController.java @@ -0,0 +1,376 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.admin; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder; +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.DataDistributorDao; +import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.InstanceWrapper; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.Property; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.vocabulary.RDF; +import org.reflections.Reflections; + +@WebServlet(name = "DataDistConf", urlPatterns = { "/admin/datadistributor" }) +public class DataDistributorConfigController extends FreemarkerHttpServlet { + private static final Log log = LogFactory.getLog(DataDistributorConfigController.class); + + private static final String SETUP_URI_BASE = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#"; + + private static final String LIST_TEMPLATE_NAME = "admin-dataDistributor.ftl"; + private static final String EDIT_TEMPLATE_NAME = "admin-dataDistributor-edit-DataDistributor.ftl"; + + private static final String SUBMIT_URL_BASE = UrlBuilder.getUrl("admin/datadistributor"); + private static final String REDIRECT_PATH = "/admin/datadistributor"; + + private static List> distributorTypes = new ArrayList<>(); + private static List> graphbuilderTypes = new ArrayList<>(); + + // Static initialiser uses reflection to find out what Java classes are present, + // as this only needs to be done + // when the application is started + static { + // Find all classes that implement the DataDistributor interface + Reflections ddReflections = new Reflections("org.vivoweb", "edu.cornell"); + for (Class distributor : ddReflections.getSubTypesOf(DataDistributor.class)) { + // As long as it is not an abstract class, add it to the list + if (!Modifier.isAbstract(distributor.getModifiers())) { + distributorTypes.add(distributor); + } + } + distributorTypes.sort(new Comparator>() { + @Override + public int compare(Class o1, Class o2) { + return o1.getSimpleName().compareToIgnoreCase(o2.getSimpleName()); + } + }); + + // Find all classes that implement the GraphBuilder interface + Reflections gbReflections = new Reflections("org.vivoweb", "edu.cornell"); + for (Class builder : gbReflections.getSubTypesOf(GraphBuilder.class)) { + // As long as it is not an abstract class, add it to the list + if (!Modifier.isAbstract(builder.getModifiers())) { + graphbuilderTypes.add(builder); + } + } + graphbuilderTypes.sort(new Comparator>() { + @Override + public int compare(Class o1, Class o2) { + return o1.getSimpleName().compareToIgnoreCase(o2.getSimpleName()); + } + }); + } + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + if (!isAuthorizedToDisplayPage(request, response, SimplePermission.MANAGE_DATA_DISTRIBUTORS.ACTION)) { + return; + } + + response.addHeader("X-XSS-Protection", "0"); + + super.doGet(request, response); + } + + @Override + protected ResponseValues processRequest(VitroRequest vreq) throws Exception { + ResponseValues response = null; + + // Determine whether we are adding, editing or deleting an object + if (!StringUtils.isEmpty(vreq.getParameter("addType"))) { + response = processAdd(vreq); + } else if (!StringUtils.isEmpty(vreq.getParameter("editUri"))) { + response = processEdit(vreq); + } else if (!StringUtils.isEmpty(vreq.getParameter("deleteUri"))) { + response = processDelete(vreq); + } + + // If we haven't determined a response, show a list of distributors and + // graphbuilders + return response != null ? response : processList(vreq); + } + + private ResponseValues processList(VitroRequest vreq) { + Map bodyMap = new HashMap<>(); + + bodyMap.put("title", I18n.text(vreq, "page_datadistributor_config")); + bodyMap.put("submitUrlBase", SUBMIT_URL_BASE); + + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + bodyMap.put("distributors", ddDao.getAllDistributors()); + bodyMap.put("distributorTypes", distributorTypes); + bodyMap.put("distributorTypeBase", DataDistributor.class); + + bodyMap.put("graphbuilders", ddDao.getAllGraphBuilders()); + bodyMap.put("graphbuilderTypes", graphbuilderTypes); + bodyMap.put("graphbuilderTypeBase", GraphBuilder.class); + + return new TemplateResponseValues(LIST_TEMPLATE_NAME, bodyMap); + } + + private ResponseValues processDelete(VitroRequest vreq) { + String uri = vreq.getParameter("deleteUri"); + + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + // A delete is simply an update with an empty model + ddDao.updateModel(uri, ModelFactory.createDefaultModel()); + + return new RedirectResponseValues(REDIRECT_PATH); + } + + private ResponseValues processEdit(VitroRequest vreq) { + String uri = vreq.getParameter("editUri"); + + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + // Retrieve the model from the triple store + Model model = ddDao.getModelByUri(uri); + + if (model != null && !model.isEmpty()) { + // Get the class of the object + Class objectClass = ddDao.getClassFromModel(model); + + // If we are processing a submitted form + if (!StringUtils.isEmpty(vreq.getParameter("submitted"))) { + // Generate a model from the submitted form + Model requestModel = getModelFromRequest(vreq, uri, objectClass); + + // Update the model in the triple store with the submitted form + ddDao.updateModel(uri, requestModel); + + // Redirect to the list + return new RedirectResponseValues(REDIRECT_PATH); + } + + Map bodyMap = new HashMap<>(); + Map fieldMap = new HashMap<>(); + + // Convert the statements of the model into a field map for the UI + StmtIterator iterator = model.listStatements(); + while (iterator.hasNext()) { + Statement statement = iterator.nextStatement(); + addFieldToMap(fieldMap, statement.getPredicate(), statement.getObject()); + } + + // Pass the field map to the template + bodyMap.put("fields", fieldMap); + bodyMap.put("editUri", uri); + + // If the uri is not for a "persistent" object (it is in a temporary submodel) + if (!ddDao.isPersistent(uri)) { + // Tell the UI to display in readonly form + bodyMap.put("readOnly", true); + } + + // Create the response + return makeResponseValues(vreq, objectClass, bodyMap); + } + + return null; + } + + private ResponseValues processAdd(VitroRequest vreq) { + // Get the class from the parameter + Class objectClass = findClass(vreq.getParameter("addType")); + + if (objectClass != null) { + // If we are processing a submitted form + if (!StringUtils.isEmpty(vreq.getParameter("submitted"))) { + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + // Generate a unique ID for the object + String uri = SETUP_URI_BASE + UUID.randomUUID().toString(); + + // Generate a model from the submitted form + Model requestModel = getModelFromRequest(vreq, uri, objectClass); + + // Update the model in the triple store with the submitted form + ddDao.updateModel(uri, requestModel); + + // Redirect to the list + return new RedirectResponseValues(REDIRECT_PATH); + } + + Map bodyMap = new HashMap<>(); + Map fieldMap = new HashMap<>(); + bodyMap.put("addType", vreq.getParameter("addType")); + + // Adding a new object, so pass an empty field map to the UI for a blank form + bodyMap.put("fields", fieldMap); + + // Create the response + return makeResponseValues(vreq, objectClass, bodyMap); + } + + return null; + + } + + /** + * Field map is of the form: + * + * key = property uri value = list of values + */ + private void addFieldToMap(Map map, Property property, RDFNode object) { + String propUri = property.getURI(); + + List values; + if (map.containsKey(propUri)) { + values = (List) map.get(propUri); + } else { + values = new ArrayList<>(); + map.put(propUri, values); + } + + if (object.isLiteral()) { + values.add(object.asLiteral().getString()); + } else { + values.add(object.asResource().getURI()); + } + } + + private ResponseValues makeResponseValues(VitroRequest vreq, Class objectClass, Map bodyMap) { + bodyMap.put("properties", getPropertyMethodsFor(objectClass)); + bodyMap.put("objectClass", objectClass); + + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + // Pass existing distributors to the UI for any drop downs to select a child + // distributor + bodyMap.put("datadistributors", ddDao.getAllDistributors()); + + // Pass existing graphbuilders to the UI for any drop downs to select a child + // graphbuilder + bodyMap.put("graphbuilders", ddDao.getAllGraphBuilders()); + + bodyMap.put("submitUrlBase", SUBMIT_URL_BASE); + + return new TemplateResponseValues(EDIT_TEMPLATE_NAME, bodyMap); + } + + private Class findClass(String name) { + Class objectClass = null; + + if (!StringUtils.isEmpty(name)) { + if (name.contains(".")) { + try { + objectClass = Class.forName(name); + } catch (ClassCastException | ClassNotFoundException ce) { + } + } + } + + if (objectClass != null) { + if (DataDistributor.class.isAssignableFrom(objectClass) + || GraphBuilder.class.isAssignableFrom(objectClass)) { + return objectClass; + } + } + + return null; + } + + /** + * Convert the submitted values into a Jena model + */ + private Model getModelFromRequest(VitroRequest vreq, String subjectUri, Class objectClass) { + Model model = ModelFactory.createDefaultModel(); + + // The subject uri passed will be the subject of all statements in this model + Resource subject = model.createResource(subjectUri); + + // Add the interface and object class types to the model + model.add(subject, RDF.type, model.getResource("java:" + objectClass.getName())); + if (DataDistributor.class.isAssignableFrom(objectClass)) { + model.add(subject, RDF.type, model.getResource("java:" + DataDistributor.class.getName())); + } else { + model.add(subject, RDF.type, model.getResource("java:" + GraphBuilder.class.getName())); + } + + // Get all the property methods for this object class + Collection propertyMethods = getPropertyMethodsFor(objectClass); + for (PropertyType.PropertyMethod method : propertyMethods) { + // Get any values for this property URI from the submitted parameters + String[] values = vreq.getParameterValues(method.getPropertyUri()); + + // If we have values + if (values != null) { + for (String value : values) { + if (value != null) { + value = value.trim(); + if (!StringUtils.isEmpty(value)) { + // If the value is a string + if (String.class.equals(method.getParameterType())) { + // Add a statement for this property with the value as a literal object + model.add(subject, model.getProperty(method.getPropertyUri()), value); + } else { + // Not a String, so add a statement for this property with the value as a + // resource + model.add(subject, model.getProperty(method.getPropertyUri()), + model.getResource(value)); + } + } + } + } + } + } + + return model; + } + + private final Map> propertyMethodsMap = new HashMap<>(); + + private Collection getPropertyMethodsFor(Class objectClass) { + if (!propertyMethodsMap.containsKey(objectClass)) { + addPropetyMethodsFor(objectClass); + } + + return propertyMethodsMap.get(objectClass); + } + + private synchronized void addPropetyMethodsFor(Class objectClass) { + if (!propertyMethodsMap.containsKey(objectClass)) { + try { + WrappedInstance wrapped = InstanceWrapper.wrap(objectClass); + Map propertyMethods = wrapped.getPropertyMethods(); + propertyMethodsMap.put(objectClass, propertyMethods.values()); + } catch (InstanceWrapper.InstanceWrapperException e) { + } + } + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java new file mode 100644 index 0000000000..327f8b09d5 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java @@ -0,0 +1,490 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.controller.admin; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor; +import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; +import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.RedirectResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.ResponseValues; +import edu.cornell.mannlib.vitro.webapp.controller.freemarker.responsevalues.TemplateResponseValues; +import edu.cornell.mannlib.vitro.webapp.dao.DataDistributorDao; +import edu.cornell.mannlib.vitro.webapp.dao.ReportingDao; +import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.reporting.AbstractTemplateReport; +import edu.cornell.mannlib.vitro.webapp.reporting.DataDistributorEndpoint; +import edu.cornell.mannlib.vitro.webapp.reporting.DataSource; +import edu.cornell.mannlib.vitro.webapp.reporting.ReportGenerator; +import edu.cornell.mannlib.vitro.webapp.reporting.ReportGeneratorException; +import edu.cornell.mannlib.vitro.webapp.reporting.XmlGenerator; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reflections.Reflections; +import org.w3c.dom.Document; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSOutput; +import org.w3c.dom.ls.LSSerializer; + +/** + * Main controller for the Reporting interface - editing and running reports + * that use DataDistributor sources + */ +@WebServlet(name = "ReportingConf", urlPatterns = { "/admin/reporting", "/admin/reporting/*" }) +public class ReportingController extends FreemarkerHttpServlet { + private static final Log log = LogFactory.getLog(ReportingController.class); + + private static List> reportTypes = new ArrayList<>(); + + private static final String SETUP_URI_BASE = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#"; + + private static final String LIST_TEMPLATE_NAME = "admin-reporting.ftl"; + private static final String EDIT_TEMPLATE_NAME = "admin-reporting-edit-report.ftl"; + private static final String SUBMIT_URL_BASE = UrlBuilder.getUrl("admin/reporting"); + private static final String REDIRECT_PATH = "/admin/reporting"; + + private static final String EXECUTE_ONLY_ATTR = "executeOnly"; + + private static final String DISTRIBUTOR_SELECT_FROM_CONTENT = SelectFromContentDistributor.class.getName(); + private static final String DISTRIBUTOR_SELECT_FROM_GRAPH = SelectFromGraphDistributor.class.getName(); + + private static String defaultDataDistributorBaseUri; + + // Static initialiser uses reflection to find out what Java classes are present, + // as this only needs to be done + // when the application is started + static { + Reflections reportReflections = new Reflections("org.vivoweb", "edu.cornell"); + for (Class report : reportReflections.getSubTypesOf(ReportGenerator.class)) { + // As long as it is not an abstract class, add it to the list + if (!Modifier.isAbstract(report.getModifiers())) { + reportTypes.add(report); + } + } + reportTypes.sort(new Comparator>() { + @Override + public int compare(Class o1, Class o2) { + return o1.getSimpleName().compareToIgnoreCase(o2.getSimpleName()); + } + }); + } + + /** + * Generate the default data distributor uri given the current request The URL + * must be fully formed and actionable - hence this method Should be replaced + * with configuration so that it can work with a scheduler + * + * @param req + */ + private void setDefaultDataDistributorBaseUri(HttpServletRequest req) { + if (StringUtils.isEmpty(defaultDataDistributorBaseUri)) { + String scheme = req.getScheme(); // http + String serverName = req.getServerName(); // hostname.com + int serverPort = req.getLocalPort(); // 80 + // Reconstruct original requesting URL + StringBuilder url = new StringBuilder(); + url.append(scheme).append("://").append(serverName); + if (serverPort != 80 && serverPort != 443) { + url.append(":").append(serverPort); + } + + url.append(UrlBuilder.getUrl("/api/dataRequest/")); + defaultDataDistributorBaseUri = url.toString(); + } + + } + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + setDefaultDataDistributorBaseUri(request); + DataDistributorEndpoint.setDefault(new DataDistributorEndpoint(defaultDataDistributorBaseUri)); + + // First, check to see if we have rights to administer the reports + if (!PolicyHelper.isAuthorizedForActions(request, SimplePermission.MANAGE_REPORTS.ACTION)) { + // Can't admin, but may be able to run reports + if (isAuthorizedToDisplayPage(request, response, SimplePermission.EXECUTE_REPORTS.ACTION)) { + request.setAttribute(EXECUTE_ONLY_ATTR, Boolean.TRUE); + } else { + // Can't run or administer reports, so bail out here + return; + } + } + + response.addHeader("X-XSS-Protection", "0"); + + // Determine if we are running a report or downloading intermediate XML or + // template + String reportName = request.getPathInfo(); + if (reportName == null || reportName.isEmpty()) { + // Not processing a report, so carry on + } else { + // Retrieve the report name + if (reportName.startsWith("/")) { + reportName = reportName.substring(1); + } + if (!StringUtils.isEmpty(reportName)) { + // Check if we are requesting a download format + String[] download = request.getParameterValues("download"); + if (!ArrayUtils.isEmpty(download)) { + for (String format : download) { + if ("xml".equalsIgnoreCase(format)) { + if (processDownloadXml(reportName, request, response)) { + return; + } + } + + if ("template".equalsIgnoreCase(format)) { + if (processDownloadTemplate(reportName, request, response)) { + return; + } + } + } + } else { + // Not a download request, so run the report + if (processReport(reportName, request, response)) { + return; + } + } + + // Add an error message + } + } + + super.doGet(request, response); + } + + @Override + protected ResponseValues processRequest(VitroRequest vreq) throws Exception { + ResponseValues response = null; + + // If we are able to administer the reports, process any admin functions + if (vreq.getAttribute(EXECUTE_ONLY_ATTR) == null) { + // Determine whether we are adding, editing or deleting an object + if (!StringUtils.isEmpty(vreq.getParameter("addType"))) { + response = processAdd(vreq); + } else if (!StringUtils.isEmpty(vreq.getParameter("editUri"))) { + response = processEdit(vreq); + } else if (!StringUtils.isEmpty(vreq.getParameter("deleteUri"))) { + response = processDelete(vreq); + } + } + + // If we haven't determined a response, show a list of reports + return response != null ? response : processList(vreq); + } + + private ResponseValues processList(VitroRequest vreq) { + Map bodyMap = new HashMap<>(); + + bodyMap.put("title", I18n.text(vreq, "page_reporting_config")); + bodyMap.put("submitUrlBase", SUBMIT_URL_BASE); + if (vreq.getAttribute(EXECUTE_ONLY_ATTR) == null) { + bodyMap.put("adminControls", true); + } + + ReportingDao reportDao = vreq.getWebappDaoFactory().getReportingDao(); + + bodyMap.put("reports", reportDao.getAllReports()); + bodyMap.put("reportTypes", reportTypes); + bodyMap.put("reportTypeBase", ReportGenerator.class); + + return new TemplateResponseValues(LIST_TEMPLATE_NAME, bodyMap); + } + + private ResponseValues processAdd(VitroRequest vreq) { + // Get the class from the parameter + Class objectClass = findClass(vreq.getParameter("addType")); + + if (objectClass != null) { + // If we are processing a submitted form + if (!StringUtils.isEmpty(vreq.getParameter("submitted"))) { + ReportingDao reportDao = vreq.getWebappDaoFactory().getReportingDao(); + + // Generate a unique ID for the object + String uri = SETUP_URI_BASE + UUID.randomUUID().toString(); + + // Generate a model from the submitted form + ReportGenerator report = getReportFromRequest(vreq, uri, objectClass); + report.setUri(uri); + + // Update the model in the triple store with the submitted form + reportDao.updateReport(uri, report); + + // Redirect to the list + return new RedirectResponseValues(REDIRECT_PATH); + } + } + + Map bodyMap = new HashMap<>(); + bodyMap.put("addType", vreq.getParameter("addType")); + + // Adding a new object, so pass an empty field map to the UI for a blank form + try { + bodyMap.put("report", objectClass.getDeclaredConstructor().newInstance()); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException + | InvocationTargetException e) { + log.error("Unable to create an empty report instance", e); + } + + // Create the response + return makeEditFormResponseValues(vreq, objectClass, bodyMap); + } + + private ResponseValues processEdit(VitroRequest vreq) { + String uri = vreq.getParameter("editUri"); + + ReportingDao reportDao = vreq.getWebappDaoFactory().getReportingDao(); + + // Retrieve the report from the triple store + ReportGenerator report = reportDao.getReportByUri(uri); + + if (report != null) { + // If we are processing a submitted form + if (!StringUtils.isEmpty(vreq.getParameter("submitted"))) { + // Generate a model from the submitted form + ReportGenerator submittedReport = getReportFromRequest(vreq, uri, report.getClass()); + + // If this is a report with a template + if (submittedReport instanceof AbstractTemplateReport) { + // Copy the existing template if a new one hasn't been submitted + if (ArrayUtils.isEmpty(((AbstractTemplateReport) submittedReport).getTemplate())) { + ((AbstractTemplateReport) submittedReport) + .setTemplate(((AbstractTemplateReport) report).getTemplate()); + } + } + + // Update the model in the triple store with the submitted form + reportDao.updateReport(uri, submittedReport); + + // Redirect to the list + return new RedirectResponseValues(REDIRECT_PATH); + } + + Map bodyMap = new HashMap<>(); + bodyMap.put("editUri", uri); + bodyMap.put("report", report); + + // If the uri is not for a "persistent" object (it is in a temporary submodel) + if (!report.isPersistent()) { + // Tell the UI to display in readonly form + bodyMap.put("readOnly", true); + } + + // Create the response + return makeEditFormResponseValues(vreq, report.getClass(), bodyMap); + } + + return null; + } + + private ResponseValues processDelete(VitroRequest vreq) { + String uri = vreq.getParameter("deleteUri"); + + ReportingDao reportingDao = vreq.getWebappDaoFactory().getReportingDao(); + + // A delete is simply an update with an empty model + reportingDao.deleteReport(uri); + + return new RedirectResponseValues(REDIRECT_PATH); + } + + /** + * Convert the submitted values into a Jena model + */ + private ReportGenerator getReportFromRequest(VitroRequest vreq, String subjectUri, Class objectClass) { + ReportGenerator report; + try { + report = (ReportGenerator) objectClass.getDeclaredConstructor().newInstance(); + } catch (InstantiationException | InvocationTargetException | NoSuchMethodException + | IllegalAccessException e) { + log.error("Unable to create a report instance", e); + return null; + } + + report.setUri(subjectUri); + + report.setReportName(vreq.getParameter("reportName")); + + // Add DataSources + String[] dsIndices = vreq.getParameterValues("dataSourceIndex"); + for (String dsIdx : dsIndices) { + DataSource dataSource = new DataSource(); + + dataSource.setDistributorName(vreq.getParameter("dataSource" + dsIdx + "_distributor")); + dataSource.setOutputName(vreq.getParameter("dataSource" + dsIdx + "_outputName")); + try { + dataSource.setRank(Integer.parseInt(vreq.getParameter("dataSource" + dsIdx + "_rank"), 10)); + } catch (NumberFormatException nfe) { + log.error("Rank must be a number"); + } + + report.addDatasource(dataSource); + } + + // If this is a template based report, add the template + if (report instanceof AbstractTemplateReport) { + FileItem item = vreq.getFileItem("template"); + if (item != null) { + byte[] templateBytes = item.get(); + if (templateBytes != null && templateBytes.length > 0) { + ((AbstractTemplateReport) report).setTemplate(templateBytes); + } + } + } + + return report; + } + + private ResponseValues makeEditFormResponseValues(VitroRequest vreq, Class objectClass, + Map bodyMap) { + bodyMap.put("objectClass", objectClass); + + DataDistributorDao ddDao = vreq.getWebappDaoFactory().getDataDistributorDao(); + + // Pass existing distributors to the UI for any drop downs to select a child + // distributor + List distributors = new ArrayList<>(); + for (DataDistributorDao.Entry distributor : ddDao.getAllDistributors()) { + // Currently, wr only support SELECT distributors + if (DISTRIBUTOR_SELECT_FROM_CONTENT.equals(distributor.getClassName()) + || DISTRIBUTOR_SELECT_FROM_GRAPH.equals(distributor.getClassName())) { + distributors.add(distributor); + } + } + + bodyMap.put("datadistributors", distributors); + bodyMap.put("submitUrlBase", SUBMIT_URL_BASE); + + return new TemplateResponseValues(EDIT_TEMPLATE_NAME, bodyMap); + } + + private boolean processReport(String reportName, HttpServletRequest request, HttpServletResponse response) { + // Need to wrap as a VitroRequest to get the DAO factory + VitroRequest vreq = new VitroRequest(request); + + // Get the reporting DAO + ReportingDao reportingDao = vreq.getWebappDaoFactory().getReportingDao(); + + // Get the named report + ReportGenerator report = reportingDao.getReportByName(reportName); + try { + // Set the content type for this report + response.setContentType(report.getContentType()); + + // Generate the report directly into the output stream + report.generateReport(response.getOutputStream()); + } catch (IOException | ReportGeneratorException e) { + log.error("Unable to generate the report", e); + } + + return true; + } + + private boolean processDownloadTemplate(String action, HttpServletRequest request, HttpServletResponse response) { + // Need to wrap as a VitroRequest to get the DAO factory + VitroRequest vreq = new VitroRequest(request); + + // Get the reporting DAO + ReportingDao reportingDao = vreq.getWebappDaoFactory().getReportingDao(); + + // Get the named report + ReportGenerator report = reportingDao.getReportByName(action); + try { + // All template driven report should be based off of AbstractTemplateReport + // So ensure the is report is, and use it to obtain the template + if (report instanceof AbstractTemplateReport) { + // The template should have the same type as the report, so set that + response.setContentType(report.getContentType()); + + // Return the template contents + response.getOutputStream().write(((AbstractTemplateReport) report).getTemplate()); + return true; + } + } catch (IOException | ReportGeneratorException e) { + log.error("Unable to retrieve template", e); + } + + return false; + } + + private boolean processDownloadXml(String action, HttpServletRequest request, HttpServletResponse response) { + // Need to wrap as a VitroRequest to get the DAO factory + VitroRequest vreq = new VitroRequest(request); + + // Get the reporting DAO + ReportingDao reportingDao = vreq.getWebappDaoFactory().getReportingDao(); + + // Get the named report + ReportGenerator report = reportingDao.getReportByName(action); + try { + // Only do this if the report has an XmlGenerator + if (report instanceof XmlGenerator) { + // Set the response type as xml + response.setContentType("text/xml"); + + // Get the xml from the report + Document xml = ((XmlGenerator) report).generateXml(); + + // Create an xml serializer + DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); + DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("XML 3.0 LS 3.0"); + LSSerializer serializer = impl.createLSSerializer(); + LSOutput output = impl.createLSOutput(); + output.setEncoding("UTF-8"); + output.setByteStream(response.getOutputStream()); + + // Write the XML to the output stream + serializer.write(xml, output); + return true; + } + } catch (IOException | ReportGeneratorException | IllegalAccessException | InstantiationException + | ClassNotFoundException e) { + log.error("Unable to generate the xml", e); + } + + return false; + } + + private Class findClass(String name) { + Class objectClass = null; + + if (!StringUtils.isEmpty(name)) { + if (name.contains(".")) { + try { + objectClass = Class.forName(name); + } catch (ClassCastException | ClassNotFoundException ce) { + } + } + } + + if (objectClass != null) { + if (ReportGenerator.class.isAssignableFrom(objectClass)) { + return objectClass; + } + } + + return null; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java index f65c41a470..f706c78eea 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/freemarker/BaseSiteAdminController.java @@ -72,6 +72,8 @@ public static void registerSiteConfigData(String key, String url, ParamMap urlPa registerSiteConfigData("userAccounts", "/accountsAdmin", null, SimplePermission.MANAGE_USER_ACCOUNTS.ACTION); registerSiteConfigData("manageProxies", "/manageProxies", null, SimplePermission.MANAGE_PROXIES.ACTION); + registerSiteConfigData("manageDataDistributors", "/admin/datadistributor", null, SimplePermission.MANAGE_DATA_DISTRIBUTORS.ACTION); + registerSiteConfigData("manageReports", "/admin/reporting", null, SimplePermission.MANAGE_REPORTS.ACTION); registerSiteConfigData("siteInfo", "/editForm", new ParamMap(new String[] {"controller", "ApplicationBean"}), SimplePermission.EDIT_SITE_INFORMATION.ACTION); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DataDistributorDao.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DataDistributorDao.java new file mode 100644 index 0000000000..c061024775 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/DataDistributorDao.java @@ -0,0 +1,89 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.dao; + +import java.util.List; + +import org.apache.jena.rdf.model.Model; + +/** + * Access object for interacting with DataDistributor and GraphBuilder + * configurations in the Display model + */ +public interface DataDistributorDao { + public class Entry { + private String uri; + private String name; + private Class clazz; + private boolean persistent; + + public Entry(String uri, String name, Class clazz, boolean persistent) { + this.uri = uri; + this.name = name; + this.clazz = clazz; + this.persistent = persistent; + } + + public String getUri() { + return uri; + } + + public String getName() { + return name; + } + + public String getClassName() { + return clazz.getName(); + } + + public boolean isPersistent() { + return persistent; + } + } + + /** + * Retrieve the URIs of all objects declared as being of type DataDistributor + */ + List getDistributorUris(); + + /** + * Retrieve the URIs of all objects declared as being of type GraphBuilder + */ + List getGraphBuilderUris(); + + /** + * Get all DataDistributors + */ + List getAllDistributors(); + + /** + * Get all GraphBuilders + */ + List getAllGraphBuilders(); + + /** + * Get a Jena model for all statements with the given Uri as a subject + */ + Model getModelByUri(String uri); + + /** + * Update the statements for a given Uri subject with those in the passed model + */ + boolean updateModel(String uri, Model newModel); + + /** + * Determine if the Uri is declared in the permanent store (i.e. it is not a + * file loaded from everytime) + */ + boolean isPersistent(String uri); + + /** + * Get the action or builder name associated with the uri + */ + String getNameFromModel(Model model); + + /** + * Get the class for this DataDistributor or GraphBuilder + */ + Class getClassFromModel(Model model); +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/ReportingDao.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/ReportingDao.java new file mode 100644 index 0000000000..e293ead155 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/ReportingDao.java @@ -0,0 +1,51 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.dao; + +import java.util.List; + +import edu.cornell.mannlib.vitro.webapp.reporting.ReportGenerator; + +/** + * Interface for retrieving and updating reporting configurations + */ +public interface ReportingDao { + // URIs for report configuration properties + public final static String PROPERTY_REPORTNAME = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#reportName"; + public final static String PROPERTY_DATASOURCE = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#dataSource"; + public final static String PROPERTY_TEMPLATE = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#template"; + + public final static String PROPERTY_DISTRIBUTORNAME = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#distributorName"; + public final static String PROPERTY_DISTRIBUTORRANK = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#distributorRank"; + public final static String PROPERTY_OUTPUTNAME = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#outputName"; + + /** + * Get a report by the URI + */ + ReportGenerator getReportByUri(String uri); + + /** + * Get a report by it's name + */ + ReportGenerator getReportByName(String name); + + /** + * Get all configured reports + */ + List getAllReports(); + + /** + * Update a given report + */ + boolean updateReport(String uri, ReportGenerator report); + + /** + * Delete the report configuration for the uri + */ + boolean deleteReport(String uri); + + /** + * Is the report configuration part of the persistent triple store + */ + boolean isPersistent(String uri); +} \ No newline at end of file diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactory.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactory.java index 7ebe8a2097..4172b92e96 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactory.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactory.java @@ -136,4 +136,7 @@ public interface WebappDaoFactory { public I18nBundle getI18nBundle(); + public DataDistributorDao getDataDistributorDao(); + + public ReportingDao getReportingDao(); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/filtering/WebappDaoFactoryFiltering.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/filtering/WebappDaoFactoryFiltering.java index fdb386a128..39fde5d751 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/filtering/WebappDaoFactoryFiltering.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/filtering/WebappDaoFactoryFiltering.java @@ -5,24 +5,7 @@ import java.util.List; import java.util.Set; -import edu.cornell.mannlib.vitro.webapp.dao.ApplicationDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.DatatypeDao; -import edu.cornell.mannlib.vitro.webapp.dao.DisplayModelDao; -import edu.cornell.mannlib.vitro.webapp.dao.FauxPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; -import edu.cornell.mannlib.vitro.webapp.dao.MenuDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao; -import edu.cornell.mannlib.vitro.webapp.dao.PageDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyInstanceDao; -import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; +import edu.cornell.mannlib.vitro.webapp.dao.*; import edu.cornell.mannlib.vitro.webapp.dao.filtering.filters.VitroFilters; import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; @@ -273,6 +256,12 @@ public DisplayModelDao getDisplayModelDao(){ return innerWebappDaoFactory.getDisplayModelDao(); } + @Override + public DataDistributorDao getDataDistributorDao() { return innerWebappDaoFactory.getDataDistributorDao(); } + + @Override + public ReportingDao getReportingDao() { return innerWebappDaoFactory.getReportingDao(); } + @Override public void close() { innerWebappDaoFactory.close(); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/DataDistributorDaoJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/DataDistributorDaoJena.java new file mode 100644 index 0000000000..9640158590 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/DataDistributorDaoJena.java @@ -0,0 +1,254 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.dao.jena; + +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.util.ArrayList; +import java.util.List; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder; +import edu.cornell.mannlib.vitro.webapp.dao.DataDistributorDao; +import edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LangAwareOntModel; +import org.apache.jena.ontology.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.shared.Lock; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * Access object for interacting with DataDistributor and GraphBuilder + * configurations in the Display model + */ +public class DataDistributorDaoJena extends JenaBaseDao implements DataDistributorDao { + private static final String DATA_DISTRIBUTOR_URI = "java:" + DataDistributor.class.getName(); + private static final String GRAPH_BUILDER_URI = "java:" + GraphBuilder.class.getName(); + private static final String ACTION_NAME_URI = + "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName"; + private static final String BUILDER_NAME_URI = + "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName"; + + public DataDistributorDaoJena(WebappDaoFactoryJena wadf) { + super(wadf); + } + + private static final String ALL_DISTRIBUTORS = "" + + "PREFIX : \n" + + "SELECT ?distributor \n" // + + "WHERE { \n" // + + " ?distributor a " + + " . \n" // + + "} \n"; + + private static final String ALL_GRAPHBUILDERS = "" + + "PREFIX : \n" + + "SELECT ?graphbuilder \n" // + + "WHERE { \n" // + + " ?graphbuilder a . \n" // + + "} \n"; + + /** + * Retrieve the URIs of all objects declared as being of type DataDistributor + */ + public List getDistributorUris() { + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + return createSelectQueryContext(displayModel, ALL_DISTRIBUTORS).execute().toStringFields("distributor") + .flatten(); + } + + /** + * Retrieve the URIs of all objects declared as being of type GraphBuilder + */ + public List getGraphBuilderUris() { + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + return createSelectQueryContext(displayModel, ALL_GRAPHBUILDERS).execute().toStringFields("graphbuilder") + .flatten(); + } + + /** + * Get all DataDistributors + */ + public List getAllDistributors() { + return getEntries(getDistributorUris()); + } + + /** + * Get all GraphBuilders + */ + public List getAllGraphBuilders() { + return getEntries(getGraphBuilderUris()); + } + + /** + * Get a Jena model for all statements with the given Uri as a subject + */ + public Model getModelByUri(String uri) { + Model model = ModelFactory.createDefaultModel(); + + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + StmtIterator iterator = displayModel.listStatements(displayModel.getResource(uri), null, (RDFNode) null); + if (iterator != null) { + model.add(iterator); + } + + return model; + } + + /** + * Update the statements for a given Uri subject with those in the passed model + */ + public boolean updateModel(String uri, Model newModel) { + // Retrieve the existing model for this uri + Model existingModel = getModelByUri(uri); + + // If we haven't got a model (e.g. the uri doesn't exist), just use an empty + // model + if (existingModel == null) { + existingModel = ModelFactory.createDefaultModel(); + } + + // Calculate what statements need to be removed from the display model + Model retractions = existingModel.difference(newModel); + + // Calculate what statements need to be added to the display model + Model additions = newModel.difference(existingModel); + + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + + displayModel.enterCriticalSection(Lock.WRITE); + try { + // Remove any retractions + if (!retractions.isEmpty()) { + displayModel.remove(retractions); + } + + // Add any additions + if (!additions.isEmpty()) { + displayModel.add(additions); + } + } finally { + displayModel.leaveCriticalSection(); + } + + return true; + } + + /** + * Determine if the Uri is declared in the permanent store (i.e. it is not a + * file loaded from everytime) + */ + public boolean isPersistent(String uri) { + // Get the display model + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + + // If we applied a Language filter to the OntModel, there will be an extended + // OntModel returned + // And we can use this to check the underlying model to see if the uri is in a + // submodel + if (displayModel instanceof LangAwareOntModel) { + return !((LangAwareOntModel) displayModel).isDefinedInSubModel(uri); + } + + // Not language filtered, so we need to check if the uri is declared in an + // attached submodel + ExtendedIterator subModels = displayModel.listSubModels(); + while (subModels.hasNext()) { + OntModel subModel = subModels.next(); + + // If the uri is in a submodel, then we should treat it as not being persistent + if (subModel.contains(subModel.getResource(uri), RDF.type)) { + return false; + } + } + + return true; + } + + /** + * Get the action or builder name associated with the uri + */ + public String getNameFromModel(Model model) { + String nameUri = null; + // Determine which Property we need to access depending on whether it is a + // DataDistributor or GraphBuilder + if (model.contains(null, RDF.type, model.getResource(DATA_DISTRIBUTOR_URI))) { + nameUri = ACTION_NAME_URI; + } else { + nameUri = BUILDER_NAME_URI; + } + + // Retrieve the Property + StmtIterator typeIterator = model.listStatements(null, model.getProperty(nameUri), (RDFNode) null); + while (typeIterator.hasNext()) { + Statement statement = typeIterator.nextStatement(); + if (statement.getObject().isLiteral()) { + return statement.getObject().asLiteral().getString(); + } + } + + // Could not find a name declared for the object + return "[unknown name]"; + } + + /** + * Get the class for this DataDistributor or GraphBuilder + */ + public Class getClassFromModel(Model model) { + Class objectClass = null; + + // Iterate through the types declared in the model + StmtIterator typeIterator = model.listStatements(null, RDF.type, (RDFNode) null); + while (typeIterator.hasNext()) { + Statement statement = typeIterator.nextStatement(); + if (objectClass == null) { + // Only use the class if it is the "most significant" (not the base + // DataDistributor or GraphBuilder interface) + objectClass = getClassIfMostSignificant(statement); + } + } + + return objectClass; + } + + /** + * Retrieve all entries for all of the specificed uris + */ + private List getEntries(List uris) { + List entries = new ArrayList<>(); + + for (String uri : uris) { + Model model = getModelByUri(uri); + String name = getNameFromModel(model); + Class clazz = getClassFromModel(model); + boolean persistent = isPersistent(uri); + + entries.add(new Entry(uri, name, clazz, persistent)); + } + + return entries; + } + + /** + * Retrieve the significant (not DataDistributor or GraphBuilder interface) + * class of the object + */ + private Class getClassIfMostSignificant(Statement statement) { + if (statement.getObject().isURIResource()) { + String classUri = statement.getObject().asResource().getURI(); + if (!DATA_DISTRIBUTOR_URI.equals(classUri) && !GRAPH_BUILDER_URI.equals(classUri)) { + try { + return Class.forName(classUri.substring(5)); + } catch (ClassNotFoundException e) { + } + } + } + + return null; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ReportingDaoJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ReportingDaoJena.java new file mode 100644 index 0000000000..847a5f66e7 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/ReportingDaoJena.java @@ -0,0 +1,271 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.dao.jena; + +import static edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.SparqlQueryRunner.createSelectQueryContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import edu.cornell.mannlib.vitro.webapp.dao.ReportingDao; +import edu.cornell.mannlib.vitro.webapp.rdfservice.filter.LangAwareOntModel; +import edu.cornell.mannlib.vitro.webapp.reporting.AbstractTemplateReport; +import edu.cornell.mannlib.vitro.webapp.reporting.DataSource; +import edu.cornell.mannlib.vitro.webapp.reporting.ReportGenerator; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoaderException; +import org.apache.commons.lang3.StringUtils; +import org.apache.jena.ontology.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.rdf.model.RDFNode; +import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; +import org.apache.jena.rdf.model.StmtIterator; +import org.apache.jena.shared.Lock; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * Jena implementation of the Reporting DAO + */ +public class ReportingDaoJena extends JenaBaseDao implements ReportingDao { + private static final String REPORT_GENERATOR_URI = "java:" + ReportGenerator.class.getName(); + + // SPARQL query to retrieve all configured reports + private static final String ALL_REPORTS = "" + + "PREFIX : \n" + "SELECT ?report \n" // + + "WHERE { \n" // + + " ?report a <" + REPORT_GENERATOR_URI + "> . \n" // + + "} \n"; + + public ReportingDaoJena(WebappDaoFactoryJena wadf) { + super(wadf); + } + + @Override + public ReportGenerator getReportByName(String name) { + try { + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + // Find a report URI that has this name + StmtIterator iterator = displayModel.listStatements(null, displayModel.getProperty(PROPERTY_REPORTNAME), + name); + if (iterator.hasNext()) { + Statement stmt = iterator.nextStatement(); + String uri = stmt.getSubject().asResource().getURI(); + + // Load the report + ReportGenerator report = new ConfigurationBeanLoader(displayModel).loadInstance(uri, + ReportGenerator.class); + report.setUri(uri); + if (isPersistent(uri)) { + report.setIsPersistent(true); + } + return report; + } + } catch (ConfigurationBeanLoaderException e) { + } + + return null; + } + + @Override + public ReportGenerator getReportByUri(String uri) { + try { + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + ReportGenerator report = new ConfigurationBeanLoader(displayModel).loadInstance(uri, ReportGenerator.class); + report.setUri(uri); + if (isPersistent(uri)) { + report.setIsPersistent(true); + } + return report; + } catch (ConfigurationBeanLoaderException e) { + log.error("Unable to load reporting configuration", e); + } + + return null; + } + + @Override + public List getAllReports() { + List uris = getReportUris(); + List reports = new ArrayList<>(); + + for (String uri : uris) { + reports.add(getReportByUri(uri)); + } + + return reports; + } + + @Override + public boolean updateReport(String uri, ReportGenerator report) { + Model existingModel = getModelByUri(uri); + Model newModel = convertReportToModel(uri, report); + + // If we haven't got a model (e.g. the uri doesn't exist), just use an empty + // model + if (existingModel == null) { + existingModel = ModelFactory.createDefaultModel(); + } + + // Calculate what statements need to be removed from the display model + Model retractions = existingModel.difference(newModel); + + // Calculate what statements need to be added to the display model + Model additions = newModel.difference(existingModel); + + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + + displayModel.enterCriticalSection(Lock.WRITE); + try { + // Remove any retractions + if (!retractions.isEmpty()) { + displayModel.remove(retractions); + } + + // Add any additions + if (!additions.isEmpty()) { + displayModel.add(additions); + } + } finally { + displayModel.leaveCriticalSection(); + } + + return true; + } + + @Override + public boolean deleteReport(String uri) { + Model existingModel = getModelByUri(uri); + Model newModel = ModelFactory.createDefaultModel(); + + // If we haven't got a model (e.g. the uri doesn't exist), just use an empty + // model + if (existingModel != null) { + // Calculate what statements need to be removed from the display model + Model retractions = existingModel.difference(newModel); + + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + + displayModel.enterCriticalSection(Lock.WRITE); + try { + // Remove any retractions + if (!retractions.isEmpty()) { + displayModel.remove(retractions); + } + } finally { + displayModel.leaveCriticalSection(); + } + } + + return true; + } + + private Model convertReportToModel(String uri, ReportGenerator report) { + Model model = ModelFactory.createDefaultModel(); + + // The subject uri passed will be the subject of all statements in this model + Resource subject = model.createResource(uri); + + // Add the interface and object class types to the model + model.add(subject, RDF.type, model.getResource("java:" + report.getClass().getName())); + model.add(subject, RDF.type, model.getResource("java:" + ReportGenerator.class.getName())); + + // Add the report name + model.add(subject, model.getProperty(PROPERTY_REPORTNAME), report.getReportName()); + + // Add all the datasources + for (DataSource dataSource : report.getDataSources()) { + // Generate a new URI for this datasource + String dsUri = uri + "-ds" + UUID.randomUUID().toString(); + Resource dsSubject = model.createResource(dsUri); + + // Add the type and properties for this datasource + model.add(subject, model.getProperty(PROPERTY_DATASOURCE), dsSubject); + model.add(dsSubject, RDF.type, model.getResource("java:" + dataSource.getClass().getName())); + model.add(dsSubject, model.getProperty(PROPERTY_DISTRIBUTORNAME), dataSource.getDistributorName()); + model.addLiteral(dsSubject, model.getProperty(PROPERTY_DISTRIBUTORRANK), dataSource.getRank()); + model.add(dsSubject, model.getProperty(PROPERTY_OUTPUTNAME), dataSource.getOutputName()); + } + + // If this is a template based report + if (report instanceof AbstractTemplateReport) { + // Add the template to the model + String template = ((AbstractTemplateReport) report).getTemplateBase64(); + if (!StringUtils.isEmpty(template)) { + model.add(subject, model.getProperty(PROPERTY_TEMPLATE), template); + } + } + + return model; + } + + /** + * Retrieve the URIs of all objects declared as being of type DataDistributor + */ + public List getReportUris() { + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + return createSelectQueryContext(displayModel, ALL_REPORTS).execute().toStringFields("report").flatten(); + } + + /** + * Get a Jena model for all statements with the given Uri as a subject + */ + private Model getModelByUri(String uri) { + Model model = ModelFactory.createDefaultModel(); + + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + StmtIterator iterator = displayModel.listStatements(displayModel.getResource(uri), null, (RDFNode) null); + if (iterator != null) { + model.add(iterator); + } + + // Add statements for datasources to the model + iterator = model.listStatements(displayModel.getResource(uri), model.getProperty(PROPERTY_DATASOURCE), + (RDFNode) null); + if (iterator != null) { + while (iterator.hasNext()) { + Statement stmt = iterator.nextStatement(); + if (stmt.getObject().isResource()) { + model.add(getModelByUri(stmt.getObject().asResource().getURI())); + } + } + } + + return model; + } + + /** + * Determine if the Uri is declared in the permanent store (i.e. it is not a + * file loaded from everytime) + */ + @Override + public boolean isPersistent(String uri) { + // Get the display model + OntModel displayModel = getWebappDaoFactory().getOntModelSelector().getDisplayModel(); + + // If we applied a Language filter to the OntModel, there will be an extended + // OntModel returned + // And we can use this to check the underlying model to see if the uri is in a + // submodel + if (displayModel instanceof LangAwareOntModel) { + return !((LangAwareOntModel) displayModel).isDefinedInSubModel(uri); + } + + // Not language filtered, so we need to check if the uri is declared in an + // attached submodel + ExtendedIterator subModels = displayModel.listSubModels(); + while (subModels.hasNext()) { + OntModel subModel = subModels.next(); + + // If the uri is in a submodel, then we should treat it as not being persistent + if (subModel.contains(subModel.getResource(uri), RDF.type)) { + return false; + } + } + + return true; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/WebappDaoFactoryJena.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/WebappDaoFactoryJena.java index 813cf27ad1..cbf5fefd08 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/WebappDaoFactoryJena.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/dao/jena/WebappDaoFactoryJena.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Set; +import edu.cornell.mannlib.vitro.webapp.dao.*; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.jena.iri.IRI; @@ -30,26 +31,6 @@ import edu.cornell.mannlib.vitro.webapp.beans.Ontology; import edu.cornell.mannlib.vitro.webapp.beans.ResourceBean; -import edu.cornell.mannlib.vitro.webapp.dao.ApplicationDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.DatatypeDao; -import edu.cornell.mannlib.vitro.webapp.dao.DisplayModelDao; -import edu.cornell.mannlib.vitro.webapp.dao.FauxPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; -import edu.cornell.mannlib.vitro.webapp.dao.MenuDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao; -import edu.cornell.mannlib.vitro.webapp.dao.PageDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyInstanceDao; -import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary; -import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; -import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactoryConfig; import edu.cornell.mannlib.vitro.webapp.i18n.I18n; import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; @@ -442,6 +423,12 @@ public DisplayModelDao getDisplayModelDao(){ return new DisplayModelDaoJena( this ); } + @Override + public DataDistributorDao getDataDistributorDao() { return new DataDistributorDaoJena(this); } + + @Override + public ReportingDao getReportingDao() { return new ReportingDaoJena(this); } + @Override public void close() { if (applicationDao != null) { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LangAwareOntModel.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LangAwareOntModel.java new file mode 100644 index 0000000000..dc5772a2ee --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/rdfservice/filter/LangAwareOntModel.java @@ -0,0 +1,38 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.rdfservice.filter; + +import org.apache.jena.ontology.OntModel; +import org.apache.jena.ontology.OntModelSpec; +import org.apache.jena.ontology.impl.OntModelImpl; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.apache.jena.vocabulary.RDF; + +/** + * Custom OntModel implementation that stores a link to an underlying OntModel being wrapped + * allowing it to be interrogated in ways that are hidden by the Model wrapper. + */ +public class LangAwareOntModel extends OntModelImpl { + private OntModel wrappedModel; + + public LangAwareOntModel(OntModelSpec spec, Model model, OntModel wrappedModel) { + super(spec, model); + this.wrappedModel = wrappedModel; + } + + /** + * Determine if a uri is a subject in submodels attached to the underlying OntModel + */ + public boolean isDefinedInSubModel(String uri) { + ExtendedIterator subModels = wrappedModel.listSubModels(); + while (subModels.hasNext()) { + OntModel subModel = subModels.next(); + if (subModel.contains(subModel.getResource(uri), RDF.type)) { + return true; + } + } + + return false; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractReport.java new file mode 100644 index 0000000000..332eb4d387 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractReport.java @@ -0,0 +1,93 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_DATASOURCE; +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_REPORTNAME; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; + +/** + * Base implementation for reports + */ +public abstract class AbstractReport implements ReportGenerator { + protected String uri; + protected String reportName; + protected List dataSources = new ArrayList<>(); + protected boolean isPersistent; + + @Property(uri = PROPERTY_DATASOURCE) + public void addDatasource(DataSource dataSource) { + dataSources.add(dataSource); + } + + @Property(uri = PROPERTY_REPORTNAME) + public void setReportName(String name) { + this.reportName = name; + } + + public void setIsPersistent(boolean isPersistent) { + this.isPersistent = isPersistent; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public boolean isPersistent() { + return isPersistent; + } + + public List getDataSources() { + dataSources.sort(new DatasourceComparator()); + return dataSources; + } + + public String getReportName() { + return reportName; + } + + public String getUri() { + return uri; + } + + public String getClassName() { + return this.getClass().getName(); + } + + /** + * Helper method for the UI + */ + public boolean getImplementsTemplate() { + return false; + } + + /** + * Helper method for the UI + */ + public boolean getImplementsXml() { + return false; + } + + /** + * Report is runnable if there is a datasource set + */ + public boolean isRunnable() { + return dataSources.size() > 0; + } + + private class DatasourceComparator implements Comparator { + @Override + public int compare(DataSource ds1, DataSource ds2) { + if (ds1 == null || ds2 == null) { + return 0; + } + + return ds1.getRank() - ds2.getRank(); + } + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractTemplateReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractTemplateReport.java new file mode 100644 index 0000000000..032cf429fe --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractTemplateReport.java @@ -0,0 +1,50 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_TEMPLATE; + +import java.util.Base64; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.lang3.ArrayUtils; + +/** + * Base implementation for reports that use a template + */ +public abstract class AbstractTemplateReport extends AbstractReport { + protected byte[] template; + + public void setTemplate(byte[] template) { + this.template = template; + } + + public byte[] getTemplate() { + return template; + } + + @Property(uri = PROPERTY_TEMPLATE) + public void setTemplateBase64(String base64) { + template = Base64.getDecoder().decode(base64); + } + + public String getTemplateBase64() { + return template == null ? null : Base64.getEncoder().encodeToString(template); + } + + public boolean getImplementsTemplate() { + return true; + } + + /** + * Report is runnable if the template is defined and all other conditions are + * met + */ + public boolean isRunnable() { + if (ArrayUtils.isEmpty(template)) { + return false; + } + + return super.isRunnable(); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java new file mode 100644 index 0000000000..f4b49e03c5 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java @@ -0,0 +1,94 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import com.haulmont.yarg.formatters.factory.DefaultFormatterFactory; +import com.haulmont.yarg.loaders.factory.DefaultLoaderFactory; +import com.haulmont.yarg.loaders.impl.JsonDataLoader; +import com.haulmont.yarg.reporting.Reporting; +import com.haulmont.yarg.reporting.RunParams; +import com.haulmont.yarg.structure.BandOrientation; +import com.haulmont.yarg.structure.ReportOutputType; +import com.haulmont.yarg.structure.impl.BandBuilder; +import com.haulmont.yarg.structure.impl.ReportBuilder; +import com.haulmont.yarg.structure.impl.ReportTemplateBuilder; +import org.apache.commons.lang3.StringUtils; + +/** + * Base implementation for reports that use the Yet Another Report Generator + * library + */ +public abstract class AbstractYARGTemplateReport extends AbstractTemplateReport { + /** + * Generate the report + */ + protected void generateReport(OutputStream outputStream, String name, ReportOutputType type) { + // Create a new report builder and template + ReportBuilder reportBuilder = new ReportBuilder(); + ReportTemplateBuilder reportTemplateBuilder = new ReportTemplateBuilder(); + + // Document path and name is required but unused + reportTemplateBuilder.documentPath("/"); + reportTemplateBuilder.documentName(name); + + // Add the template as the document content + reportTemplateBuilder.documentContent(template); + + // Set the output type according to the implementation + reportTemplateBuilder.outputType(type); + + // Build the template and add it to the report builder + reportBuilder.template(reportTemplateBuilder.build()); + + // Add the data from the data sources + Map params = new HashMap(); + for (DataSource dataSource : getDataSources()) { + // Get the output of the datasource + String body = dataSource.getBody(new HashMap<>()); + if (!StringUtils.isEmpty(body)) { + // Bind the output to the name given in the datasource configuration + params.put(dataSource.getOutputName(), body); + + // Create a bandbuilder for the header and bind it to the output name configured + BandBuilder headBand = new BandBuilder(); + headBand.name(dataSource.getOutputName() + "_header"); + headBand.query(dataSource.getOutputName() + "_header", + "parameter=" + dataSource.getOutputName() + "_header $.head.vars[*]", "json"); + headBand.orientation(BandOrientation.HORIZONTAL); + reportBuilder.band(headBand.build()); + + // Create a bandbuilder for the results and bind it to the output name + // configured + BandBuilder resultsBand = new BandBuilder(); + resultsBand.name(dataSource.getOutputName()); + resultsBand.query(dataSource.getOutputName(), + "parameter=" + dataSource.getOutputName() + " $.results.bindings[*]", "json"); + resultsBand.orientation(BandOrientation.HORIZONTAL); + reportBuilder.band(resultsBand.build()); + + // Create a bandbuilder for the footer and bind it to the output name configured + BandBuilder footerBand = new BandBuilder(); + footerBand.name(dataSource.getOutputName() + "_footer"); + footerBand.query(dataSource.getOutputName() + "_footer", + "parameter=" + dataSource.getOutputName() + "_footer $.head.vars[*]", "json"); + footerBand.orientation(BandOrientation.HORIZONTAL); + reportBuilder.band(footerBand.build()); + + } + } + + // Create a new reporting object + Reporting reporting = new Reporting(); + + // Attach a JSON loader and formatter + reporting.setLoaderFactory(new DefaultLoaderFactory().setJsonDataLoader(new JsonDataLoader())); + reporting.setFormatterFactory(new DefaultFormatterFactory()); + + // Run the report writing to the output stream + reporting.runReport((new RunParams(reportBuilder.build())).params(params), outputStream); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataDistributorEndpoint.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataDistributorEndpoint.java new file mode 100644 index 0000000000..d6a09c330e --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataDistributorEndpoint.java @@ -0,0 +1,46 @@ +/* $This file is distributed under the terms of the license in LICENSE$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URIBuilder; + +/** + * Defines a data distributor endpoint to access the distributors Can be used in + * the future to define multiple endpoints and configure datasources from + * multiple instances + */ +public class DataDistributorEndpoint { + private String url; + + public DataDistributorEndpoint(String url) { + this.url = url; + } + + public URI generateUri(String actionName, List parameters) { + try { + URIBuilder builder = new URIBuilder(url + actionName); + if (parameters != null) { + builder.addParameters(parameters); + } + return builder.build(); + } catch (URISyntaxException e) { + } + + return null; + } + + private static DataDistributorEndpoint defaultEndpoint; + + public static DataDistributorEndpoint getDefault() { + return defaultEndpoint; + } + + public static void setDefault(DataDistributorEndpoint endpoint) { + defaultEndpoint = endpoint; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java new file mode 100644 index 0000000000..9856bce033 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java @@ -0,0 +1,94 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_DISTRIBUTORNAME; +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_DISTRIBUTORRANK; +import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_OUTPUTNAME; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +import javax.net.ssl.SSLContext; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; + +/** + * Defines a datasource for the report + */ +public class DataSource { + private String distributorName; + private String outputName; + private int rank; + + public String getBody(Map parameterMap) { + // Currently, only supporting the default endpoint. Eventually, this might be + // configurable + // so that the api of other installations can be used in a report + DataDistributorEndpoint endpoint = DataDistributorEndpoint.getDefault(); + URI myUri = endpoint.generateUri(distributorName, null); + + try { + SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (certificate, authType) -> true) + .build(); + HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build(); + HttpResponse sourceResponse = httpClient.execute(new HttpGet(myUri)); + return IOUtils.toString(sourceResponse.getEntity().getContent(), StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return null; + } + + @Property(uri = PROPERTY_DISTRIBUTORNAME, minOccurs = 1, maxOccurs = 1) + public void setDistributorName(String name) { + distributorName = name; + } + + @Property(uri = PROPERTY_DISTRIBUTORRANK, minOccurs = 0, maxOccurs = 1) + public void setRank(Integer rank) { + if (rank != null) { + this.rank = rank; + } + } + + @Property(uri = PROPERTY_OUTPUTNAME, minOccurs = 1, maxOccurs = 1) + public void setOutputName(String name) { + outputName = name; + } + + public String getOutputName() { + return outputName; + } + + public String getDistributorName() { + return distributorName; + } + + public int getRank() { + return rank; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java new file mode 100644 index 0000000000..3e1e1b39c9 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java @@ -0,0 +1,135 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.docx4j.Docx4J; +import org.docx4j.openpackaging.exceptions.Docx4JException; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Template based report that uses the Docx4j library to process Word templates + * that have the OpenDOPE controls embedded in them. + */ +public class OpenDopeWordReport extends AbstractTemplateReport implements XmlGenerator { + /** + * Define the docx mime type, including the UTF-8 character set + */ + @Override + public String getContentType() throws ReportGeneratorException { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=UTF-8"; + } + + /** + * Xml document is required for passing to the Docx4j library + */ + @Override + public Document generateXml() throws ReportGeneratorException { + try { + DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + Document xmlDoc = xmlBuilder.newDocument(); + + Element rootElement = xmlDoc.createElement("results"); + xmlDoc.appendChild(rootElement); + + // Execute all datasources + for (DataSource dataSource : dataSources) { + // Get the result of the datasource + String body = dataSource.getBody(new HashMap<>()); + + // Load the JSON document + JsonFactory factory = new JsonFactory(); + ObjectMapper mapper = new ObjectMapper(factory); + JsonNode rootNode = mapper.readTree(body); + + // Convert the JSON into Xml + convertObjectNodeToXml(xmlDoc, dataSource.getOutputName(), rootNode); + } + + return xmlDoc; + } catch (IOException | ParserConfigurationException e) { + } + + return null; + } + + private void convertObjectNodeToXml(Document xmlDoc, String outputName, JsonNode rootNode) { + Element rootElement = xmlDoc.getDocumentElement(); + Element resultsElement = xmlDoc.createElement(outputName); + rootElement.appendChild(resultsElement); + + // Only process if it is the result Json of a SELECT distributor + if (rootNode.has("head") && rootNode.has("results")) { + // Get the results and bindings + JsonNode results = rootNode.findValue("results"); + JsonNode bindings = results.findValue("bindings"); + if (bindings.isArray()) { + // For each binding + for (int idx = 0; idx < bindings.size(); idx++) { + JsonNode result = bindings.get(idx); + // Get the results + Iterator> iterator = result.fields(); + if (iterator != null) { + // Create a row for the results + Element row = xmlDoc.createElement("row"); + + // For each result + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + + // Get the value + JsonNode value = entry.getValue(); + JsonNode type = value.get("type"); + + // Add each literal as a column + if (type != null && type.isTextual() && "literal".equals(type.asText())) { + Element column = xmlDoc.createElement(entry.getKey()); + column.setTextContent(value.get("value").asText()); + row.appendChild(column); + } + } + resultsElement.appendChild(row); + } + } + } + } + } + + @Override + public void generateReport(OutputStream outputStream) throws ReportGeneratorException { + // Get the XML + Document xmlDoc = generateXml(); + try { + // Create a word processing package from the template + WordprocessingMLPackage wordMLPackage = Docx4J.load(new ByteArrayInputStream(template)); + + // Bind the xml to the template + Docx4J.bind(wordMLPackage, xmlDoc, Docx4J.FLAG_BIND_INSERT_XML & Docx4J.FLAG_BIND_BIND_XML); + + // Process the template + Docx4J.save(wordMLPackage, outputStream, Docx4J.FLAG_NONE); + } catch (Docx4JException e) { + throw new ReportGeneratorException(e); + } + } + + @Override + public boolean getImplementsXml() { + return true; + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java new file mode 100644 index 0000000000..42a51a70d0 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java @@ -0,0 +1,55 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.io.OutputStream; +import java.util.List; + +/** + * Interface for all reports + */ +public interface ReportGenerator { + void addDatasource(DataSource dataSource); + + void setReportName(String name); + + /** + * States the MIME type of the output from this instance. The MIME type may be + * hardcoded, derived from the configuration, or derived from the request + * parameters. + *

+ * Called exactly once, after init(). + * + * @return the MIME type of the (expected) output. + * @throws ReportGeneratorException + */ + String getContentType() throws ReportGeneratorException; + + /** + * Generates the report directly into the specified output stream + * + * @param outputStream Stream to write report into + * @throws ReportGeneratorException + */ + void generateReport(OutputStream outputStream) throws ReportGeneratorException; + + void setIsPersistent(boolean isPersistent); + + void setUri(String uri); + + String getClassName(); + + List getDataSources(); + + String getReportName(); + + String getUri(); + + boolean getImplementsTemplate(); + + boolean getImplementsXml(); + + boolean isPersistent(); + + boolean isRunnable(); +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGeneratorException.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGeneratorException.java new file mode 100644 index 0000000000..21b3774318 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGeneratorException.java @@ -0,0 +1,24 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +/** + * A problem occurred while creating or running the ReportGenerator. + */ +public class ReportGeneratorException extends Exception { + public ReportGeneratorException() { + super(); + } + + public ReportGeneratorException(String message) { + super(message); + } + + public ReportGeneratorException(Throwable cause) { + super(cause); + } + + public ReportGeneratorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java new file mode 100644 index 0000000000..56fc9e7ef1 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java @@ -0,0 +1,23 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.io.OutputStream; + +import com.haulmont.yarg.structure.ReportOutputType; + +/** + * Generate an Excel report using the YARG template processor + */ +public class TemplateExcelReport extends AbstractYARGTemplateReport { + @Override + public String getContentType() throws ReportGeneratorException { + // Return the xlsx mime type include the UTF-8 character set + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=UTF-8"; + } + + @Override + public void generateReport(OutputStream outputStream) throws ReportGeneratorException { + generateReport(outputStream, "report.xlsx", ReportOutputType.xlsx); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java new file mode 100644 index 0000000000..6a757fa3ca --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java @@ -0,0 +1,23 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import java.io.OutputStream; + +import com.haulmont.yarg.structure.ReportOutputType; + +/** + * Generate a Word report using the YARG template processor + */ +public class TemplateWordReport extends AbstractYARGTemplateReport { + @Override + public String getContentType() throws ReportGeneratorException { + // Return the docx mime type, including the UTF-8 character set + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document; charset=UTF-8"; + } + + @Override + public void generateReport(OutputStream outputStream) throws ReportGeneratorException { + generateReport(outputStream, "report.docx", ReportOutputType.docx); + } +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java new file mode 100644 index 0000000000..7791343756 --- /dev/null +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.reporting; + +import org.w3c.dom.Document; + +/** + * Marker interface to say that the report generates an intermediate Xml + * document + */ +public interface XmlGenerator { + Document generateXml() throws ReportGeneratorException; +} diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java index 7bae32b366..757e201294 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java @@ -2,271 +2,309 @@ package edu.cornell.mannlib.vitro.webapp.utils.configuration; -import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDfloat; -import static org.apache.jena.datatypes.xsd.XSDDatatype.XSDstring; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - import org.apache.jena.datatypes.RDFDatatype; import org.apache.jena.datatypes.xsd.impl.RDFLangString; import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Statement; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static org.apache.jena.datatypes.xsd.XSDDatatype.*; + /** * An enumeration of the types of properties that the ConfigurationBeanLoader * will support. - * + *

* Also, classes that represent the Java methods and RDF statements associated * with those types. */ public enum PropertyType { - RESOURCE { - @Override - public PropertyStatement buildPropertyStatement(Statement s) { - return new ResourcePropertyStatement(s.getPredicate().getURI(), s - .getObject().asResource().getURI()); - } - - @Override - protected PropertyMethod buildPropertyMethod(Method method, - Property annotation) { - return new ResourcePropertyMethod(method, annotation); - } - - }, - STRING { - @Override - public PropertyStatement buildPropertyStatement(Statement s) { - return new StringPropertyStatement(s.getPredicate().getURI(), s - .getObject().asLiteral().getString()); - } - - @Override - protected PropertyMethod buildPropertyMethod(Method method, - Property annotation) { - return new StringPropertyMethod(method, annotation); - } - }, - FLOAT { - @Override - public PropertyStatement buildPropertyStatement(Statement s) { - return new FloatPropertyStatement(s.getPredicate().getURI(), s - .getObject().asLiteral().getFloat()); - } - - @Override - protected PropertyMethod buildPropertyMethod(Method method, - Property annotation) { - return new FloatPropertyMethod(method, annotation); - } - }; - - public static PropertyType typeForObject(RDFNode object) - throws PropertyTypeException { - if (object.isURIResource()) { - return RESOURCE; - } - if (object.isLiteral()) { - Literal literal = object.asLiteral(); - RDFDatatype datatype = literal.getDatatype(); - if (datatype == null || datatype.equals(XSDstring) || datatype.equals(RDFLangString.rdfLangString)) { - return STRING; - } - if (datatype.equals(XSDfloat)) { - return FLOAT; - } - } - throw new PropertyTypeException("Unsupported datatype on object: " - + object); - } - - public static PropertyType typeForParameterType(Class parameterType) - throws PropertyTypeException { - if (Float.TYPE.equals(parameterType)) { - return FLOAT; - } - if (String.class.equals(parameterType)) { - return STRING; - } - if (!parameterType.isPrimitive()) { - return RESOURCE; - } - throw new PropertyTypeException( - "Unsupported parameter type on method: " + parameterType); - } - - public static PropertyStatement createPropertyStatement(Statement s) - throws PropertyTypeException { - PropertyType type = PropertyType.typeForObject(s.getObject()); - return type.buildPropertyStatement(s); - } - - public static PropertyMethod createPropertyMethod(Method method, - Property annotation) throws PropertyTypeException { - Class parameterType = method.getParameterTypes()[0]; - PropertyType type = PropertyType.typeForParameterType(parameterType); - return type.buildPropertyMethod(method, annotation); - } - - protected abstract PropertyStatement buildPropertyStatement(Statement s); - - protected abstract PropertyMethod buildPropertyMethod(Method method, - Property annotation); - - public static abstract class PropertyStatement { - private final PropertyType type; - private final String predicateUri; - - public PropertyStatement(PropertyType type, String predicateUri) { - this.type = type; - this.predicateUri = predicateUri; - } - - public PropertyType getType() { - return type; - } - - public String getPredicateUri() { - return predicateUri; - } - - public abstract Object getValue(); - } - - public static class ResourcePropertyStatement extends PropertyStatement { - private final String objectUri; - - public ResourcePropertyStatement(String predicateUri, String objectUri) { - super(RESOURCE, predicateUri); - this.objectUri = objectUri; - } - - @Override - public String getValue() { - return objectUri; - } - } - - public static class StringPropertyStatement extends PropertyStatement { - private final String string; - - public StringPropertyStatement(String predicateUri, String string) { - super(STRING, predicateUri); - this.string = string; - } - - @Override - public String getValue() { - return string; - } - } - - public static class FloatPropertyStatement extends PropertyStatement { - private final float f; - - public FloatPropertyStatement(String predicateUri, float f) { - super(FLOAT, predicateUri); - this.f = f; - } - - @Override - public Float getValue() { - return f; - } - } - - public static abstract class PropertyMethod { - protected final PropertyType type; - protected final Method method; - protected final String propertyUri; - protected final int minOccurs; - protected final int maxOccurs; - - // Add cardinality values here! Final, with getters. - public PropertyMethod(PropertyType type, Method method, - Property annotation) { - this.type = type; - this.method = method; - this.propertyUri = annotation.uri(); - this.minOccurs = annotation.minOccurs(); - this.maxOccurs = annotation.maxOccurs(); - checkCardinalityBounds(); - } - - private void checkCardinalityBounds() { - // This is where we check for negative values or out of order. - } - - public Method getMethod() { - return method; - } - - public Class getParameterType() { - return method.getParameterTypes()[0]; - } - - public String getPropertyUri() { - return propertyUri; - } - - public int getMinOccurs() { - return minOccurs; - } - - public int getMaxOccurs() { - return maxOccurs; - } - - public void confirmCompatible(PropertyStatement ps) - throws PropertyTypeException { - if (type != ps.getType()) { - throw new PropertyTypeException( - "Can't apply statement of type " + ps.getType() - + " to a method of type " + type); - } - } - - public void invoke(Object instance, Object value) - throws PropertyTypeException { - try { - method.invoke(instance, value); - } catch (IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - throw new PropertyTypeException("Property method failed.", e); - } - } - - } - - public static class ResourcePropertyMethod extends PropertyMethod { - public ResourcePropertyMethod(Method method, Property annotation) { - super(RESOURCE, method, annotation); - } - } - - public static class StringPropertyMethod extends PropertyMethod { - public StringPropertyMethod(Method method, Property annotation) { - super(STRING, method, annotation); - } - } - - public static class FloatPropertyMethod extends PropertyMethod { - public FloatPropertyMethod(Method method, Property annotation) { - super(FLOAT, method, annotation); - } - } - - public static class PropertyTypeException extends Exception { - public PropertyTypeException(String message) { - super(message); - } - - public PropertyTypeException(String message, Throwable cause) { - super(message, cause); - } - - } + RESOURCE { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new ResourcePropertyStatement(s.getPredicate().getURI(), s + .getObject().asResource().getURI()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new ResourcePropertyMethod(method, annotation); + } + + }, + STRING { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new StringPropertyStatement(s.getPredicate().getURI(), s + .getObject().asLiteral().getString()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new StringPropertyMethod(method, annotation); + } + }, + FLOAT { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new FloatPropertyStatement(s.getPredicate().getURI(), s + .getObject().asLiteral().getFloat()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new FloatPropertyMethod(method, annotation); + } + }, + INTEGER { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new IntegerPropertyStatement(s.getPredicate().getURI(), s + .getObject().asLiteral().getInt()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new IntegerPropertyMethod(method, annotation); + } + }; + + public static PropertyType typeForObject(RDFNode object) + throws PropertyTypeException { + if (object.isURIResource()) { + return RESOURCE; + } + if (object.isLiteral()) { + Literal literal = object.asLiteral(); + RDFDatatype datatype = literal.getDatatype(); + if (datatype == null || datatype.equals(XSDstring) || datatype.equals(RDFLangString.rdfLangString)) { + return STRING; + } + if (datatype.equals(XSDfloat)) { + return FLOAT; + } + if (datatype.equals(XSDint) || datatype.equals(XSDinteger)) { + return INTEGER; + } + } + throw new PropertyTypeException("Unsupported datatype on object: " + + object); + } + + public static PropertyType typeForParameterType(Class parameterType) + throws PropertyTypeException { + if (Float.TYPE.equals(parameterType)) { + return FLOAT; + } + if (Integer.class.equals(parameterType)) { + return INTEGER; + } + if (String.class.equals(parameterType)) { + return STRING; + } + if (!parameterType.isPrimitive()) { + return RESOURCE; + } + throw new PropertyTypeException( + "Unsupported parameter type on method: " + parameterType); + } + + public static PropertyStatement createPropertyStatement(Statement s) + throws PropertyTypeException { + PropertyType type = PropertyType.typeForObject(s.getObject()); + return type.buildPropertyStatement(s); + } + + public static PropertyMethod createPropertyMethod(Method method, + Property annotation) throws PropertyTypeException { + Class parameterType = method.getParameterTypes()[0]; + PropertyType type = PropertyType.typeForParameterType(parameterType); + return type.buildPropertyMethod(method, annotation); + } + + protected abstract PropertyStatement buildPropertyStatement(Statement s); + + protected abstract PropertyMethod buildPropertyMethod(Method method, + Property annotation); + + public static abstract class PropertyStatement { + private final PropertyType type; + private final String predicateUri; + + public PropertyStatement(PropertyType type, String predicateUri) { + this.type = type; + this.predicateUri = predicateUri; + } + + public PropertyType getType() { + return type; + } + + public String getPredicateUri() { + return predicateUri; + } + + public abstract Object getValue(); + } + + public static class ResourcePropertyStatement extends PropertyStatement { + private final String objectUri; + + public ResourcePropertyStatement(String predicateUri, String objectUri) { + super(RESOURCE, predicateUri); + this.objectUri = objectUri; + } + + @Override + public String getValue() { + return objectUri; + } + } + + public static class StringPropertyStatement extends PropertyStatement { + private final String string; + + public StringPropertyStatement(String predicateUri, String string) { + super(STRING, predicateUri); + this.string = string; + } + + @Override + public String getValue() { + return string; + } + } + + public static class FloatPropertyStatement extends PropertyStatement { + private final float f; + + public FloatPropertyStatement(String predicateUri, float f) { + super(FLOAT, predicateUri); + this.f = f; + } + + @Override + public Float getValue() { + return f; + } + } + + public static class IntegerPropertyStatement extends PropertyStatement { + private final int i; + + public IntegerPropertyStatement(String predicateUri, int i) { + super(INTEGER, predicateUri); + this.i = i; + } + + @Override + public Integer getValue() { + return i; + } + } + + public static abstract class PropertyMethod { + protected final PropertyType type; + protected final Method method; + protected final String propertyUri; + protected final int minOccurs; + protected final int maxOccurs; + + // Add cardinality values here! Final, with getters. + public PropertyMethod(PropertyType type, Method method, + Property annotation) { + this.type = type; + this.method = method; + this.propertyUri = annotation.uri(); + this.minOccurs = annotation.minOccurs(); + this.maxOccurs = annotation.maxOccurs(); + checkCardinalityBounds(); + } + + private void checkCardinalityBounds() { + // This is where we check for negative values or out of order. + } + + public Method getMethod() { + return method; + } + + public Class getParameterType() { + return method.getParameterTypes()[0]; + } + + public String getPropertyUri() { + return propertyUri; + } + + public int getMinOccurs() { + return minOccurs; + } + + public int getMaxOccurs() { + return maxOccurs; + } + + public void confirmCompatible(PropertyStatement ps) + throws PropertyTypeException { + if (type != ps.getType()) { + throw new PropertyTypeException( + "Can't apply statement of type " + ps.getType() + + " to a method of type " + type); + } + } + + public void invoke(Object instance, Object value) + throws PropertyTypeException { + try { + method.invoke(instance, value); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new PropertyTypeException("Property method failed.", e); + } + } + + } + + public static class ResourcePropertyMethod extends PropertyMethod { + public ResourcePropertyMethod(Method method, Property annotation) { + super(RESOURCE, method, annotation); + } + } + + public static class StringPropertyMethod extends PropertyMethod { + public StringPropertyMethod(Method method, Property annotation) { + super(STRING, method, annotation); + } + } + + public static class FloatPropertyMethod extends PropertyMethod { + public FloatPropertyMethod(Method method, Property annotation) { + super(FLOAT, method, annotation); + } + } + + public static class IntegerPropertyMethod extends PropertyMethod { + public IntegerPropertyMethod(Method method, Property annotation) { + super(INTEGER, method, annotation); + } + } + + public static class PropertyTypeException extends Exception { + public PropertyTypeException(String message) { + super(message); + } + + public PropertyTypeException(String message, Throwable cause) { + super(message, cause); + } + + } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java index 9e889426ce..34e3f3dc84 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java @@ -36,6 +36,8 @@ public WrappedInstance(T instance, this.validationMethods = validationMethods; } + public Map getPropertyMethods() { return propertyMethods; } + /** * The loader calls this as soon as the instance is created. * @@ -164,7 +166,7 @@ public T getInstance() { return instance; } - public static class ResourceUnavailableException extends Exception { + public static class ResourceUnavailableException extends Exception { public ResourceUnavailableException(String message) { super(message); } diff --git a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactoryStub.java b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactoryStub.java index 732ae32091..132de6a297 100644 --- a/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactoryStub.java +++ b/api/src/test/java/stubs/edu/cornell/mannlib/vitro/webapp/dao/WebappDaoFactoryStub.java @@ -2,275 +2,270 @@ package stubs.edu.cornell.mannlib.vitro.webapp.dao; +import edu.cornell.mannlib.vitro.webapp.dao.*; +import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; + import java.util.List; import java.util.Set; -import edu.cornell.mannlib.vitro.webapp.dao.ApplicationDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.DataPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.DatatypeDao; -import edu.cornell.mannlib.vitro.webapp.dao.DisplayModelDao; -import edu.cornell.mannlib.vitro.webapp.dao.FauxPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.IndividualDao; -import edu.cornell.mannlib.vitro.webapp.dao.MenuDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyDao; -import edu.cornell.mannlib.vitro.webapp.dao.ObjectPropertyStatementDao; -import edu.cornell.mannlib.vitro.webapp.dao.OntologyDao; -import edu.cornell.mannlib.vitro.webapp.dao.PageDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.PropertyInstanceDao; -import edu.cornell.mannlib.vitro.webapp.dao.UserAccountsDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassDao; -import edu.cornell.mannlib.vitro.webapp.dao.VClassGroupDao; -import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory; -import edu.cornell.mannlib.vitro.webapp.i18n.I18nBundle; - /** * A minimal implementation of the WebappDaoFactory. - * + *

* I have only implemented the methods that I needed. Feel free to implement * others. */ public class WebappDaoFactoryStub implements WebappDaoFactory { - // ---------------------------------------------------------------------- - // Stub infrastructure - // ---------------------------------------------------------------------- - - private String defaultNamespace; - private ApplicationDao applicationDao; - private DataPropertyDao dataPropertyDao; - private DatatypeDao datatypeDao; - private FauxPropertyDao fauxPropertyDao; - private IndividualDao individualDao; - private MenuDao menuDao; - private ObjectPropertyDao objectPropertyDao; - private ObjectPropertyStatementDao objectPropertyStatementDao; - private OntologyDao ontologyDao; - private PropertyGroupDao propertyGroupDao; - private PropertyInstanceDao propertyInstanceDao; - private UserAccountsDao userAccountsDao; - private VClassDao vClassDao; - private VClassGroupDao vClassGroupDao; - - public void setDefaultNamespace(String defaultNamespace) { - this.defaultNamespace = defaultNamespace; - } - - public void setApplicationDao(ApplicationDao applicationDao) { - this.applicationDao = applicationDao; - } - - public void setDataPropertyDao(DataPropertyDao dataPropertyDao) { - this.dataPropertyDao = dataPropertyDao; - } - - public void setDatatypeDao(DatatypeDao datatypeDao) { - this.datatypeDao = datatypeDao; - } - - public void setFauxPropertyDao(FauxPropertyDao fauxPropertyDao) { - this.fauxPropertyDao = fauxPropertyDao; - } - - public void setIndividualDao(IndividualDao individualDao) { - this.individualDao = individualDao; - } - - public void setMenuDao(MenuDao menuDao) { - this.menuDao = menuDao; - } - - public void setObjectPropertyDao(ObjectPropertyDao objectPropertyDao) { - this.objectPropertyDao = objectPropertyDao; - } - - public void setObjectPropertyStatementDao( - ObjectPropertyStatementDao objectPropertyStatementDao) { - this.objectPropertyStatementDao = objectPropertyStatementDao; - } - - public void setOntologyDao(OntologyDao ontologyDao) { - this.ontologyDao = ontologyDao; - } - - public void setPropertyGroupDao(PropertyGroupDao propertyGroupDao) { - this.propertyGroupDao = propertyGroupDao; - } - - public void setPropertyInstanceDao( - PropertyInstanceDao propertyInstanceDao) { - this.propertyInstanceDao = propertyInstanceDao; - } - - public void setUserAccountsDao(UserAccountsDao userAccountsDao) { - this.userAccountsDao = userAccountsDao; - } - - public void setVClassDao(VClassDao vClassDao) { - this.vClassDao = vClassDao; - } - - public void setVClassGroupDao(VClassGroupDao vClassGroupDao) { - this.vClassGroupDao = vClassGroupDao; - } - - // ---------------------------------------------------------------------- - // Stub methods - // ---------------------------------------------------------------------- - - @Override - public String getDefaultNamespace() { - return this.defaultNamespace; - } - - @Override - public ApplicationDao getApplicationDao() { - return this.applicationDao; - } - - @Override - public DataPropertyDao getDataPropertyDao() { - return this.dataPropertyDao; - } - - @Override - public DatatypeDao getDatatypeDao() { - return this.datatypeDao; - } - - @Override - public IndividualDao getIndividualDao() { - return this.individualDao; - } - - @Override - public MenuDao getMenuDao() { - return this.menuDao; - } - - @Override - public ObjectPropertyDao getObjectPropertyDao() { - return this.objectPropertyDao; - } - - @Override - public FauxPropertyDao getFauxPropertyDao() { - return this.fauxPropertyDao; - } - - @Override - public ObjectPropertyStatementDao getObjectPropertyStatementDao() { - return this.objectPropertyStatementDao; - } - - @Override - public OntologyDao getOntologyDao() { - return this.ontologyDao; - } - - @Override - public PropertyGroupDao getPropertyGroupDao() { - return this.propertyGroupDao; - } - - @Override - public PropertyInstanceDao getPropertyInstanceDao() { - return this.propertyInstanceDao; - } - - @Override - public UserAccountsDao getUserAccountsDao() { - return this.userAccountsDao; - } - - @Override - public VClassDao getVClassDao() { - return this.vClassDao; - } - - @Override - public VClassGroupDao getVClassGroupDao() { - return this.vClassGroupDao; - } - - // ---------------------------------------------------------------------- - // Un-implemented methods - // ---------------------------------------------------------------------- - - @Override - public String checkURI(String uriStr) { - throw new RuntimeException( - "WebappDaoFactory.checkURI() not implemented."); - } - - @Override - public String checkURIForEditableEntity(String uriStr) { - throw new RuntimeException( - "WebappDaoFactory.checkURIForNewEditableEntity() not implemented."); - } - - @Override - public boolean hasExistingURI(String uriStr) { - throw new RuntimeException( - "WebappDaoFactory.hasExistingURI() not implemented."); - } - - @Override - public Set getNonuserNamespaces() { - throw new RuntimeException( - "WebappDaoFactory.getNonuserNamespaces() not implemented."); - } - - @Override - public List getPreferredLanguages() { - throw new RuntimeException( - "WebappDaoFactory.getPreferredLanguages() not implemented."); - } - - @Override - public List getCommentsForResource(String resourceURI) { - throw new RuntimeException( - "WebappDaoFactory.getCommentsForResource() not implemented."); - } - - @Override - public WebappDaoFactory getUserAwareDaoFactory(String userURI) { - throw new RuntimeException( - "WebappDaoFactory.getUserAwareDaoFactory() not implemented."); - } - - @Override - public String getUserURI() { - throw new RuntimeException( - "WebappDaoFactory.getUserURI() not implemented."); - } - - @Override - public DataPropertyStatementDao getDataPropertyStatementDao() { - throw new RuntimeException( - "WebappDaoFactory.getDataPropertyStatementDao() not implemented."); - } - - @Override - public DisplayModelDao getDisplayModelDao() { - throw new RuntimeException( - "WebappDaoFactory.getDisplayModelDao() not implemented."); - } - - @Override - public PageDao getPageDao() { - throw new RuntimeException( - "WebappDaoFactory.getPageDao() not implemented."); - } - - @Override - public void close() { - throw new RuntimeException("WebappDaoFactory.close() not implemented."); - } - - @Override - public I18nBundle getI18nBundle() { - throw new RuntimeException("WebappDaoFactory.getI18nBundle() not implemented."); - } + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private String defaultNamespace; + private ApplicationDao applicationDao; + private DataPropertyDao dataPropertyDao; + private DatatypeDao datatypeDao; + private FauxPropertyDao fauxPropertyDao; + private IndividualDao individualDao; + private MenuDao menuDao; + private ObjectPropertyDao objectPropertyDao; + private ObjectPropertyStatementDao objectPropertyStatementDao; + private OntologyDao ontologyDao; + private PropertyGroupDao propertyGroupDao; + private PropertyInstanceDao propertyInstanceDao; + private UserAccountsDao userAccountsDao; + private VClassDao vClassDao; + private VClassGroupDao vClassGroupDao; + + @Override + public String getDefaultNamespace() { + return this.defaultNamespace; + } + + public void setDefaultNamespace(String defaultNamespace) { + this.defaultNamespace = defaultNamespace; + } + + @Override + public ApplicationDao getApplicationDao() { + return this.applicationDao; + } + + public void setApplicationDao(ApplicationDao applicationDao) { + this.applicationDao = applicationDao; + } + + @Override + public DataPropertyDao getDataPropertyDao() { + return this.dataPropertyDao; + } + + public void setDataPropertyDao(DataPropertyDao dataPropertyDao) { + this.dataPropertyDao = dataPropertyDao; + } + + @Override + public DatatypeDao getDatatypeDao() { + return this.datatypeDao; + } + + public void setDatatypeDao(DatatypeDao datatypeDao) { + this.datatypeDao = datatypeDao; + } + + @Override + public IndividualDao getIndividualDao() { + return this.individualDao; + } + + public void setIndividualDao(IndividualDao individualDao) { + this.individualDao = individualDao; + } + + @Override + public MenuDao getMenuDao() { + return this.menuDao; + } + + public void setMenuDao(MenuDao menuDao) { + this.menuDao = menuDao; + } + + @Override + public ObjectPropertyDao getObjectPropertyDao() { + return this.objectPropertyDao; + } + + public void setObjectPropertyDao(ObjectPropertyDao objectPropertyDao) { + this.objectPropertyDao = objectPropertyDao; + } + + @Override + public FauxPropertyDao getFauxPropertyDao() { + return this.fauxPropertyDao; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + public void setFauxPropertyDao(FauxPropertyDao fauxPropertyDao) { + this.fauxPropertyDao = fauxPropertyDao; + } + + @Override + public ObjectPropertyStatementDao getObjectPropertyStatementDao() { + return this.objectPropertyStatementDao; + } + + public void setObjectPropertyStatementDao( + ObjectPropertyStatementDao objectPropertyStatementDao) { + this.objectPropertyStatementDao = objectPropertyStatementDao; + } + + @Override + public OntologyDao getOntologyDao() { + return this.ontologyDao; + } + + public void setOntologyDao(OntologyDao ontologyDao) { + this.ontologyDao = ontologyDao; + } + + @Override + public PropertyGroupDao getPropertyGroupDao() { + return this.propertyGroupDao; + } + + public void setPropertyGroupDao(PropertyGroupDao propertyGroupDao) { + this.propertyGroupDao = propertyGroupDao; + } + + @Override + public PropertyInstanceDao getPropertyInstanceDao() { + return this.propertyInstanceDao; + } + + public void setPropertyInstanceDao( + PropertyInstanceDao propertyInstanceDao) { + this.propertyInstanceDao = propertyInstanceDao; + } + + @Override + public UserAccountsDao getUserAccountsDao() { + return this.userAccountsDao; + } + + public void setUserAccountsDao(UserAccountsDao userAccountsDao) { + this.userAccountsDao = userAccountsDao; + } + + @Override + public VClassDao getVClassDao() { + return this.vClassDao; + } + + public void setVClassDao(VClassDao vClassDao) { + this.vClassDao = vClassDao; + } + + @Override + public VClassGroupDao getVClassGroupDao() { + return this.vClassGroupDao; + } + + public void setVClassGroupDao(VClassGroupDao vClassGroupDao) { + this.vClassGroupDao = vClassGroupDao; + } + + // ---------------------------------------------------------------------- + // Un-implemented methods + // ---------------------------------------------------------------------- + + @Override + public String checkURI(String uriStr) { + throw new RuntimeException( + "WebappDaoFactory.checkURI() not implemented."); + } + + @Override + public String checkURIForEditableEntity(String uriStr) { + throw new RuntimeException( + "WebappDaoFactory.checkURIForNewEditableEntity() not implemented."); + } + + @Override + public boolean hasExistingURI(String uriStr) { + throw new RuntimeException( + "WebappDaoFactory.hasExistingURI() not implemented."); + } + + @Override + public Set getNonuserNamespaces() { + throw new RuntimeException( + "WebappDaoFactory.getNonuserNamespaces() not implemented."); + } + + @Override + public List getPreferredLanguages() { + throw new RuntimeException( + "WebappDaoFactory.getPreferredLanguages() not implemented."); + } + + @Override + public List getCommentsForResource(String resourceURI) { + throw new RuntimeException( + "WebappDaoFactory.getCommentsForResource() not implemented."); + } + + @Override + public WebappDaoFactory getUserAwareDaoFactory(String userURI) { + throw new RuntimeException( + "WebappDaoFactory.getUserAwareDaoFactory() not implemented."); + } + + @Override + public String getUserURI() { + throw new RuntimeException( + "WebappDaoFactory.getUserURI() not implemented."); + } + + @Override + public DataPropertyStatementDao getDataPropertyStatementDao() { + throw new RuntimeException( + "WebappDaoFactory.getDataPropertyStatementDao() not implemented."); + } + + @Override + public DisplayModelDao getDisplayModelDao() { + throw new RuntimeException( + "WebappDaoFactory.getDisplayModelDao() not implemented."); + } + + @Override + public PageDao getPageDao() { + throw new RuntimeException( + "WebappDaoFactory.getPageDao() not implemented."); + } + + @Override + public DataDistributorDao getDataDistributorDao() { + throw new RuntimeException( + "WebappDaoFactory.getDataDistributorDao() not implemented."); + } + + @Override + public ReportingDao getReportingDao() { + throw new RuntimeException( + "WebappDaoFactory.getReportingDao() not implemented."); + } + + @Override + public void close() { + throw new RuntimeException("WebappDaoFactory.close() not implemented."); + } + + @Override + public I18nBundle getI18nBundle() { + throw new RuntimeException("WebappDaoFactory.getI18nBundle() not implemented."); + } } diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 1145f5e5bb..5522904683 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -262,5 +262,31 @@ javax.mail 1.6.0 + + + org.docx4j + docx4j-JAXB-ReferenceImpl + 8.1.2 + + + com.haulmont.yarg + yarg + 2.2.9 + + + javax.xml.bind + jaxb-api + 2.2.11 + + + com.sun.xml.bind + jaxb-core + 2.2.11 + + + com.sun.xml.bind + jaxb-impl + 2.2.11 + diff --git a/home/src/main/resources/rdf/auth/everytime/permission_config.n3 b/home/src/main/resources/rdf/auth/everytime/permission_config.n3 index f94e3516fb..a34c850266 100644 --- a/home/src/main/resources/rdf/auth/everytime/permission_config.n3 +++ b/home/src/main/resources/rdf/auth/everytime/permission_config.n3 @@ -15,6 +15,8 @@ auth:ADMIN auth:hasPermission simplePermission:AccessSpecialDataModels ; auth:hasPermission simplePermission:EnableDeveloperPanel ; auth:hasPermission simplePermission:LoginDuringMaintenance ; + auth:hasPermission simplePermission:ManageDataDistributors ; + auth:hasPermission simplePermission:ManageReports ; auth:hasPermission simplePermission:ManageMenus ; auth:hasPermission simplePermission:ManageProxies ; auth:hasPermission simplePermission:ManageSearchIndex ; @@ -41,6 +43,7 @@ auth:ADMIN # permissions for EDITOR and above. auth:hasPermission simplePermission:DoBackEndEditing ; + auth:hasPermission simplePermission:ExecuteReports ; auth:hasPermission simplePermission:SeeIndividualEditingPanel ; auth:hasPermission simplePermission:SeeRevisionInfo ; auth:hasPermission simplePermission:SeeSiteAdminPage ; @@ -78,6 +81,7 @@ auth:CURATOR auth:hasPermission simplePermission:PageViewableCurator ; # permissions for EDITOR and above. + auth:hasPermission simplePermission:ExecuteReports ; auth:hasPermission simplePermission:DoBackEndEditing ; auth:hasPermission simplePermission:SeeIndividualEditingPanel ; auth:hasPermission simplePermission:SeeRevisionInfo ; @@ -109,6 +113,7 @@ auth:EDITOR rdfs:label "Editor" ; # permissions for EDITOR and above. + auth:hasPermission simplePermission:ExecuteReports ; auth:hasPermission simplePermission:DoBackEndEditing ; auth:hasPermission simplePermission:SeeIndividualEditingPanel ; auth:hasPermission simplePermission:SeeRevisionInfo ; diff --git a/pom.xml b/pom.xml index fea0497f2d..017d7a4228 100644 --- a/pom.xml +++ b/pom.xml @@ -483,6 +483,15 @@ true + + + + false + + repo-cuba-platform-work + repo + https://repo.cuba-platform.com/content/groups/work + diff --git a/webapp/src/main/webapp/css/reporting.css b/webapp/src/main/webapp/css/reporting.css new file mode 100644 index 0000000000..e48785dced --- /dev/null +++ b/webapp/src/main/webapp/css/reporting.css @@ -0,0 +1,13 @@ + + +/*TODO probably the wrong place to put it*/ +.reportDatasource { + border: 2px solid #AF1414; + background-color: #e3e3e3; + padding: 5px; + margin-bottom: 5px; + margin-top: 5px; +} +.reportDatasource button.delete { + float: right; +} \ No newline at end of file diff --git a/webapp/src/main/webapp/images/run.png b/webapp/src/main/webapp/images/run.png new file mode 100644 index 0000000000000000000000000000000000000000..0c444d6ec925fb44150f24e19df3d45dd11a3653 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^{2Template: +reporting_download_xml = XML +reporting_run = RUN + +# Reporting classes +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport = OpenDope Word Template +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport = Excel Template +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport = Word Template + +reporting_config_field_reportName = Report Name + +reporting_config_dataSource_distributor = Distributor +reporting_config_dataSource_rank = Rank +reporting_config_dataSource_outputName = Output Name diff --git a/webapp/src/main/webapp/local/i18n/all_en_US.properties b/webapp/src/main/webapp/local/i18n/all_en_US.properties new file mode 100644 index 0000000000..e2cbd1c0e6 --- /dev/null +++ b/webapp/src/main/webapp/local/i18n/all_en_US.properties @@ -0,0 +1,1058 @@ +# +# Text strings for the controllers and templates +# +# Default (English) +# +save_changes=Save changes +save_entry=Save entry +select_existing=Select existing +select_an_existing=Select an existing +add_an_entry_to=Add an entry of type +change_entry_for=Change entry for: +add_new_entry_for=Add new entry for: +change_text_for=Change text for: +cancel_link = Cancel +cancel_title = cancel +required_fields = required fields +or = or +alt_error_alert = Error alert icon +alt_confirmation = Confirmation icon +for=for +email_address = Email address +first_name = First name +last_name = Last name +roles = Roles +status = Status + +ascending_order = ascending order +descending_order = descending order +select_one = Select one + +type_more_characters = type more characters +no_match = no match + +request_failed = Request failed. Please contact your system administrator. + +# +# Image upload pages +# +upload_page_title = Upload image +upload_page_title_with_name = Upload image for {0} +upload_heading = Photo Upload + +replace_page_title = Replace image +replace_page_title_with_name = Replace image for {0} + +crop_page_title = Crop image +crop_page_title_with_name = Crop image for {0} + +current_photo = Current Photo +upload_photo = Upload a photo +replace_photo = Replace Photo +photo_types = (JPEG, GIF or PNG) +maximum_file_size = Maximum file size: {0} megabytes +minimum_image_dimensions = Minimum image dimensions: {0} x {1} pixels + +cropping_caption = Your profile photo will look like the image below. +cropping_note = To make adjustments, you can drag around and resize the photo to the right. \ +When you are happy with your photo click the "Save Photo" button. + +alt_thumbnail_photo = Individual photo +alt_image_to_crop = Image to be cropped +alt_preview_crop = Preview of photo cropped + +delete_link = Delete photo +submit_upload = Upload photo +submit_save = Save photo + +confirm_delete = Are you sure you want to delete this photo? + +imageUpload.errorNoURI = No entity URI was provided +imageUpload.errorUnrecognizedURI = This URI is not recognized as belonging to anyone: ''{0}'' +imageUpload.errorNoImageForCropping = There is no image file to be cropped. +imageUpload.errorImageTooSmall = The uploaded image should be at least {0} pixels high and {1} pixels wide. +imageUpload.errorUnknown = Sorry, we were unable to process the photo you provided. Please try another photo. +imageUpload.errorFileTooBig = Please upload an image smaller than {0} megabytes. +imageUpload.errorUnrecognizedFileType = ''{0}'' is not a recognized image file type. Please upload JPEG, GIF, or PNG files only. +imageUpload.errorNoPhotoSelected = Please browse and select a photo. +imageUpload.errorBadMultipartRequest = Failed to parse the multi-part request for uploading an image. +imageUpload.errorFormFieldMissing = The form did not contain a ''{0}'' field." + +# +# User Accounts pages +# +account_management = Account Management +user_accounts_link = User accounts +user_accounts_title = user accounts + +login_count = Login count +last_login = Last Login + +add_new_account = Add new account +edit_account = Edit account +external_auth_only = Externally Authenticated Only +reset_password = Reset password +reset_password_note = Note: Instructions for resetting the password will \ +be emailed to the address entered above. The password will not \ +be reset until the user follows the link provided in this email. +new_password = New password +confirm_password = Confirm new password +minimum_password_length = Minimum of {0} characters in length; maximum of {1}. +leave_password_unchanged = Leaving this blank means that the password will not be changed. +confirm_initial_password = Confirm initial password + +new_account_1 = A new account for +new_account_2 = was successfully created. +new_account_title = new account +new_account_notification = A notification email has been sent to {0} \ +with instructions for activating the account and creating a password. +updated_account_1 = The account for +updated_account_2 = has been updated. +updated_account_title = updated account +updated_account_notification = A confirmation email has been sent to {0} \ +with instructions for resetting a password. \ +The password will not be reset until the user follows the link provided in this email. +deleted_accounts = Deleted {0} {0, choice, 0#accounts|1#account|1Template: +reporting_download_xml = XML +reporting_run = RUN + +# Reporting classes +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport = OpenDope Word Template +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport = Excel Template +reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport = Word Template + +reporting_config_field_reportName = Report Name + +reporting_config_dataSource_distributor = Distributor +reporting_config_dataSource_rank = Rank +reporting_config_dataSource_outputName = Output Name \ No newline at end of file diff --git a/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor-edit-DataDistributor.ftl b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor-edit-DataDistributor.ftl new file mode 100644 index 0000000000..8544a68b91 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor-edit-DataDistributor.ftl @@ -0,0 +1,176 @@ +<#-- $This file is distributed under the terms of the license in LICENSE$ --> +<#if objectClass??> +

${i18n()["dd_config_" + objectClass.name]}

+ +<#if addType??> + <#assign submitUrl = "${submitUrlBase}?addType=${addType?url}" /> +<#else> + <#assign submitUrl = "${submitUrlBase}?editUri=${editUri?url}" /> + + +
+
+ <#-- List the action or builder name first --> + <#list properties as property> + <#if property.propertyUri == 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName'> + <@fieldForProperty property /> + <#elseif property.propertyUri == 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName'> + <@fieldForProperty property /> + + + <#list properties as property> + <#if property.propertyUri == 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName'> + <#elseif property.propertyUri == 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName'> + <#else> + <@fieldForProperty property /> + + + <#if readOnly??> + cancel + <#else> + + + cancel + <#if addType??> + <#else> +            + + + + +
+
+ +<#macro fieldForProperty property> + <#if property.minOccurs > 0> + <#local required = true /> + <#else> + <#local required = false /> + +
+ + <#switch property.parameterType.name> + <#case 'java.lang.String'> + <#if fields[property.propertyUri]??> + <#assign values = fields[property.propertyUri] /> + <#list values as value> + <@inputField property.propertyUri value required /> + + <#else> + <@inputField property.propertyUri "" required /> + + <#break> + <#case 'edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor'> + + <#break> + <#case 'edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilder'> + + <#break> + <#default> +
${property.parameterType.name}
+ <#if fields[property.propertyUri]??> + <#assign values = fields[property.propertyUri] /> + <#list values as value> +
${value}
+ + <#else> + + <#break> + +
+ <#switch property.parameterType.name> + <#case 'java.lang.String'> + <#if property.maxOccurs > 1> + <#if property.maxOccurs > 25> + <#local maxOccurs=25 /> + <#else> + <#local maxOccurs=property.maxOccurs /> + +

${i18n().dd_config_add_field} ${i18n().dd_config_add_field}

+ + <#break> + + + +<#macro inputField propertyUri value required> + <#switch propertyUri> + <#case 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query'> + <#case 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#constructQuery'> + <#case 'http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#drillDownQuery'> + + <#if readOnly??> + <#else> + <#assign enableYasqe = true /> + + <#break> + <#default> +

required <#if readOnly??>readonly />

+ <#break> + + + +<#if enableYasqe??> + ${stylesheets.add('')} + ${headScripts.add('')} + + + + diff --git a/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor.ftl b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor.ftl new file mode 100644 index 0000000000..918b056bf3 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-dataDistributor.ftl @@ -0,0 +1,71 @@ +<#-- $This file is distributed under the terms of the license in LICENSE$ --> +

${i18n().dd_config_title}

+
+
+ + +
+ + + + + + + <#list distributors as distributor> + + + + + + +
${i18n().dd_config_name}${i18n().dd_config_type}${i18n().dd_config_edit}
+ ${distributor.name} + + ${i18n()["dd_config_" + distributor.className]} + + ${i18n().dd_config_edit} + <#-- if distributor.persistent> + ${i18n().dd_config_delete} + + ${i18n().dd_config_run} +
+
+ +

${i18n().dd_graphbuilder_config_title}

+
+
+ + +
+ + + + + + + <#list graphbuilders as graphbuilder> + + + + + + +
${i18n().dd_config_name}${i18n().dd_config_type}${i18n().dd_config_edit}
+ ${graphbuilder.name} + + ${i18n()["dd_config_" + graphbuilder.className]} + + ${i18n().dd_config_edit} + <#-- if graphbuilder.persistent> + ${i18n().dd_config_delete} + +
+
diff --git a/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting-edit-report.ftl b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting-edit-report.ftl new file mode 100644 index 0000000000..8e876185e9 --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting-edit-report.ftl @@ -0,0 +1,126 @@ + +<#-- $This file is distributed under the terms of the license in LICENSE$ --> +${stylesheets.add('')} + + +<#if objectClass??> +

${i18n()["reporting_config_" + objectClass.name]}

+ +<#if addType??> + <#assign submitUrl = "${submitUrlBase}?addType=${addType?url}" /> +<#else> + <#assign submitUrl = "${submitUrlBase}?editUri=${editUri?url}" /> + + + +
+
+
+ +

+ readonly /> +

+
+
+ <#list report.dataSources as dataSource> + + +
+

${i18n().reporting_config_add_datasource} ${i18n().reporting_config_add_datasource}

+ <#if report.implementsTemplate> + <#if report.template?? && report.reportName??> + + +
+ ${i18n().reporting_template} +
+ + <#if readOnly??> + cancel + <#else> + + + cancel + <#if addType??> + <#else> + + + + +
+
diff --git a/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting.ftl b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting.ftl new file mode 100644 index 0000000000..88949f011a --- /dev/null +++ b/webapp/src/main/webapp/templates/freemarker/body/admin/admin-reporting.ftl @@ -0,0 +1,46 @@ +<#-- $This file is distributed under the terms of the license in LICENSE$ --> +<#if adminControls> +

${i18n().reporting_config_title}

+<#else> +

${i18n().reporting_title}

+ +
+ <#if reports??> +
+ + +
+ + + + + + + + <#if reports??> + <#list reports as report> + + + + + + + +
${i18n().reporting_config_name}${i18n().reporting_config_type}${i18n().reporting_config_edit}
+ ${report.reportName} + + ${i18n()["reporting_config_" + report.className]} + + ${i18n().reporting_config_edit} + <#if report.implementsXml> + ${i18n().reporting_download_xml} + + <#if report.runnable> + ${i18n().reporting_run} + +
+
diff --git a/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteConfiguration.ftl b/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteConfiguration.ftl index 894b1725a7..8b39b32ff5 100644 --- a/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteConfiguration.ftl +++ b/webapp/src/main/webapp/templates/freemarker/body/siteAdmin/siteAdmin-siteConfiguration.ftl @@ -27,6 +27,14 @@
  • ${i18n().site_information}
  • + <#if siteConfig.manageDataDistributors?has_content> +
  • ${i18n().manage_datadistributors}
  • + + + <#if siteConfig.manageReports?has_content> +
  • ${i18n().manage_reports}
  • + + <#if siteConfig.userAccounts?has_content>
  • ${i18n().user_accounts}
  • From ac1ed45774e96ef82eba027530adfdfda2a92434 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 22 Nov 2024 09:23:18 +0100 Subject: [PATCH 02/19] Revert "rm unimplemented or unnecessary classes" This reverts commit 7c2f665991b94c8d9f7e4f38342502815a4be8f7. --- .../DataDistributionApi_overview.png | Bin 0 -> 140754 bytes .../JavaScriptTransformDistributor.java | 212 ++++++++++++++++++ .../distribute/examples/HelloDistributor.java | 73 ++++++ .../html/RenderedShortViewDistributor.java | 13 ++ .../CascadingRequestsGraphBuilder.java | 13 ++ .../graphbuilder/ParallelGraphBuilder.java | 14 ++ .../graphbuilder/ThreadsafeGraphBuilder.java | 13 ++ 7 files changed, 338 insertions(+) create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/html/RenderedShortViewDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/CascadingRequestsGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..9ec3f1bd11ca9daf19ccc170a45c782b0ab4c535 GIT binary patch literal 140754 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ȢZTD2T$aT^M>P5m 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 literal 0 HcmV?d00001 diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java new file mode 100644 index 0000000000..eddff0a5a5 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java @@ -0,0 +1,212 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.servlet.ServletContext; +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Wrap a data distributor with a JavaScript function that will transform its + * output. + * + *

    + * + * The child distributor might produce any arbitrary output. The JavaScript + * function must be written to accept that output as a String and return a + * String containing the transformed output. + * + *

    + * + * For example, the function might replace all occurences of a namespace with a + * different namespace, like this: + * + *

    + * function transform(rawData) {
    + *     return rawData.split("http://first/").join("http://second/");
    + * }
    + * 
    + * + * The JavaScript method must be named 'transform', must accept a String as + * argument, and must return a String as result. + * + *

    + * + * The JavaScript execution environment will include a global variable named + * 'logger'. This is a binding of an org.apache.commons.logging.Log object, and + * can be used to write to the VIVO log file. + * + *

    + * + * Note: this decorator is only scalable to a limited extent, since the + * JavaScript function works with Strings instead of Streams. + */ +public class JavaScriptTransformDistributor extends AbstractDataDistributor { + private static final Log log = LogFactory + .getLog(JavaScriptTransformDistributor.class); + + /** The content type to attach to the file. */ + private String contentType; + private String script; + private DataDistributor child; + private List supportingScriptPaths = new ArrayList<>(); + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) + public void setContentType(String cType) { + contentType = cType; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script", minOccurs = 1, maxOccurs = 1) + public void setScript(String scriptIn) { + script = scriptIn; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child", minOccurs = 1, maxOccurs = 1) + public void setChild(DataDistributor c) { + child = c; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript") + public void addScriptPath(String path) { + supportingScriptPaths.add(path); + } + + @Override + public String getContentType() throws DataDistributorException { + return contentType; + } + + @Override + public void init(DataDistributorContext ddc) + throws DataDistributorException { + super.init(ddc); + child.init(ddc); + } + + /** + */ + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + ScriptEngine engine = createScriptEngine(); + addLoggerToEngine(engine); + loadSupportingScripts(engine); + loadMainScript(engine); + + writeTransformedOutput(output, + runTransformFunction(engine, runChildDistributor())); + } + + private ScriptEngine createScriptEngine() { + return new ScriptEngineManager().getEngineByName("nashorn"); + } + + private void addLoggerToEngine(ScriptEngine engine) { + String loggerName = this.getClass().getName() + "." + actionName; + Log jsLogger = LogFactory.getLog(loggerName); + engine.put("logger", jsLogger); + } + + private void loadSupportingScripts(ScriptEngine engine) + throws DataDistributorException { + log.debug("loading supporting scripts"); + for (String path : supportingScriptPaths) { + loadSupportingScript(engine, path); + log.debug("loaded supporting script: " + path); + } + } + + private void loadSupportingScript(ScriptEngine engine, String path) + throws DataDistributorException { + ServletContext ctx = ApplicationUtils.instance().getServletContext(); + + InputStream resource = ctx.getResourceAsStream(path); + if (resource == null) { + throw new DataDistributorException( + "Can't locate script resource for '" + path + "'"); + } + + try { + engine.eval(new InputStreamReader(resource)); + } catch (ScriptException e) { + throw new DataDistributorException( + "Script at '" + path + "' contains syntax errors.", e); + } + } + + private void loadMainScript(ScriptEngine engine) + throws DataDistributorException { + try { + engine.eval(script); + } catch (ScriptException e) { + throw new DataDistributorException("Script contains syntax errors.", + e); + } + } + + private String runChildDistributor() throws DataDistributorException { + ByteArrayOutputStream childOut = new ByteArrayOutputStream(); + try { + child.writeOutput(childOut); + log.debug("ran child distributor"); + } catch (Exception e) { + throw new DataDistributorException( + "Child distributor threw an exception", e); + } + try { + return childOut.toString("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("What? No UTF-8 Charset?", e); + } + } + + private String runTransformFunction(ScriptEngine engine, String childOutput) + throws DataDistributorException { + try { + Invocable invocable = (Invocable) engine; + Object result = invocable.invokeFunction("transform", childOutput); + log.debug("ran transform function"); + + if (result instanceof String) { + return (String) result; + } else { + throw new ActionFailedException( + "transform function must return a String"); + } + } catch (NoSuchMethodException e) { + throw new DataDistributorException( + "Script must have a transform() function.", e); + } catch (ScriptException e) { + throw new DataDistributorException("Script contains syntax errors.", + e); + } + } + + private void writeTransformedOutput(OutputStream output, String transformed) + throws DataDistributorException { + try { + output.write(transformed.getBytes("UTF-8")); + } catch (IOException e) { + throw new DataDistributorException(e); + } + } + + @Override + public void close() throws DataDistributorException { + child.close(); + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java new file mode 100644 index 0000000000..15702ae67f --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java @@ -0,0 +1,73 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.examples; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple example of a data distributor. It sends a greeting. + */ +public class HelloDistributor extends AbstractDataDistributor { + + private static final Object NAME_PARAMETER_KEY = "name"; + + /** + * The instance is created to service one HTTP request, and init() is + * called. + * + * The DataDistributorContext provides access to the request parameters, and + * the triple-store connections. + */ + @Override + public void init(DataDistributorContext ddc) + throws DataDistributorException { + super.init(ddc); + } + + /** + * For this distributor, the browser should treat the output as simple text. + */ + @Override + public String getContentType() throws DataDistributorException { + return "text/plain"; + } + + /** + * The text written to the OutputStream will become the body of the HTTP + * response. + * + * This will only be called once for a given instance. + */ + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + try { + if (parameters.containsKey(NAME_PARAMETER_KEY)) { + output.write(String + .format("Hello, %s!", + parameters.get(NAME_PARAMETER_KEY)[0]) + .getBytes()); + } else { + output.write("Hello, World!".getBytes()); + } + } catch (IOException e) { + throw new ActionFailedException(e); + } + } + + /** + * Release any resources. In this case, none. + * + * Garbage collection is uncertain. On the other hand, you can be confident + * that this will be called in a timely manner. + */ + @Override + public void close() throws DataDistributorException { + // Nothing to do. + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/html/RenderedShortViewDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/html/RenderedShortViewDistributor.java new file mode 100644 index 0000000000..5e9ec82ccb --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/html/RenderedShortViewDistributor.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.html; + +/** + * TODO + * + * Provide short view CONTEXT, Individual URI, most-specific class, and get the + * rendered HTML for the short view. + */ +public class RenderedShortViewDistributor { + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/CascadingRequestsGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/CascadingRequestsGraphBuilder.java new file mode 100644 index 0000000000..8aefe468fe --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/CascadingRequestsGraphBuilder.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +/** + * TODO + * + * Iterate like a drillDown, (how much code in common?). Each sub-request is + * handled via HTTP call to the service. + */ +public class CascadingRequestsGraphBuilder { + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java new file mode 100644 index 0000000000..150251aadf --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java @@ -0,0 +1,14 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +/** + * TODO + * + * Iterate like the drill-down builder (share code?) Each sub-request is handled + * by a thread-pool, whose size can be configured. Requires the use of + * ThreadsafeGraphBuilders. + */ +public class ParallelGraphBuilder { + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java new file mode 100644 index 0000000000..22fccffd20 --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +/** + * TODO + * + * Marker interface asserting that the GraphBuilder may be called by several + * threads at once without conflict + */ +public interface ThreadsafeGraphBuilder { + +} From 7605bcbe12dd3385db22601b11aeb066c77f677d Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 22 Nov 2024 09:24:08 +0100 Subject: [PATCH 03/19] Revert "rm unused distributors" This reverts commit 915f96dc8549f11aad44c62a21f5096e02345528. --- .../api/distribute/file/FileDistributor.java | 70 ++++++ .../file/SelectingFileDistributor.java | 221 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java create mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java new file mode 100644 index 0000000000..d0352f1adb --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java @@ -0,0 +1,70 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.file; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Serve a particular file, when requested. + * + * Possible uses include mocking a data distributor that does not yet exist, or + * serving data that was created and cached by an "expensive" background + * process. + * + * Provide a path to the file, either absolute or relative to the Vitro home + * directory. Provide the content type. + */ +public class FileDistributor extends AbstractDataDistributor { + private static final Log log = LogFactory.getLog(FileDistributor.class); + + /** The path to the data file, relative to the Vitro home directory. */ + private String datapath; + + /** The content type to attach to the file. */ + private String contentType; + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#path", minOccurs = 1, maxOccurs = 1) + public void setPath(String path) { + datapath = path; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) + public void setContentType(String cType) { + contentType = cType; + } + + @Override + public String getContentType() throws DataDistributorException { + return contentType; + } + + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + Path home = ApplicationUtils.instance().getHomeDirectory().getPath(); + Path datafile = home.resolve(datapath); + log.debug("data file is at: " + datapath); + try (InputStream input = Files.newInputStream(datafile)) { + IOUtils.copy(input, output); + } catch (IOException e) { + throw new ActionFailedException(e); + } + } + + @Override + public void close() throws DataDistributorException { + // Nothing to close. + } + +} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java new file mode 100644 index 0000000000..8e0e815fab --- /dev/null +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java @@ -0,0 +1,221 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.file; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

    + * Selects one of several files and distributes it, based on the value of a + * request parameter. If the selected file does not exist, distributes an empty + * data set. + *

    + *

    + * The configuration includes: + *

    + *
      + *
    • The name of the request parameter that will contain the file + * selector.
    • + *
    • A regular expression that will be used to extract the file selector from + * the parameter value.
    • + *
    • A template for the file path.
    • + *
    • A string to be served as an "empty data set".
    • + *
    • The content type to attach to the output.
    • + *
    + * + *

    + * An example: this configuration will distribute JSON files based on the + * localname of the department URI, as specified in the request. + *

    + *
    + * :sfd
    + *   a   <java:edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor> ,
    + *       <java:edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor> ;
    + *   :actionName "collaboration_sunburst" ;
    + *   :contentType "application/json" ;
    + *   :parameterName "department" ;
    + *   :parameterPattern "[^/#]+$";
    + *   :filepathTemplate "crossunit-\\0.json" ;
    + *   :emptyResponse "[]" .
    + * 
    + * + *

    + * When the request is received, the "department" parameter contains the URI of + * the department in question. The provided pattern matcher will get just the + * localname from the department URI. That localname is substituted into the + * path template to determine the location of the file. If a file exists at that + * location, it's contents are served as "application/json" text. If the file + * does not exist, the empty response (an empty JSON array) is served. + *

    + */ +public class SelectingFileDistributor extends AbstractDataDistributor { + + private static final Log log = LogFactory + .getLog(SelectingFileDistributor.class); + + /** The name of the request parameter that will select the file. */ + private String parameterName; + + /** The pattern to parse the value of the request parameter. */ + private Pattern parameterParser; + + /** The template to create the file path from the parsed values. */ + private String filepathTemplate; + + /** The content type to attach to the file. */ + private String contentType; + + /** The response to provide if the file does not exist. */ + private String emptyResponse; + + private FileFinder fileFinder; + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName", minOccurs = 1, maxOccurs = 1) + public void setParameterName(String name) { + parameterName = name; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern", minOccurs = 1, maxOccurs = 1) + public void setParameterPattern(String pattern) { + parameterParser = Pattern.compile(pattern); + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate", minOccurs = 1, maxOccurs = 1) + public void setFilepathTemplate(String template) { + filepathTemplate = template; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) + public void setContentType(String cType) { + contentType = cType; + } + + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse", minOccurs = 1, maxOccurs = 1) + public void setEmptyResponse(String response) { + emptyResponse = response; + } + + @Override + public void init(DataDistributorContext context) + throws DataDistributorException { + super.init(context); + fileFinder = new FileFinder(parameters, parameterName, parameterParser, + filepathTemplate, + ApplicationUtils.instance().getHomeDirectory().getPath()); + } + + @Override + public String getContentType() throws DataDistributorException { + return contentType; + } + + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + try { + File file = fileFinder.find(); + if (file != null && file.isFile()) { + IOUtils.copy(new FileInputStream(file), output); + return; + } else { + IOUtils.write(emptyResponse, output, "UTF-8"); + } + } catch (IOException e) { + throw new DataDistributorException(e); + } + } + + @Override + public void close() throws DataDistributorException { + // Nothing to close. + } + + /** + * Does the heavy lifting of locating the file. + */ + protected static class FileFinder { + private final Map parameters; + private final String parameterName; + private final Pattern parameterParser; + private final String filepathTemplate; + private final Path home; + + public FileFinder(Map parameters, + String parameterName, Pattern parameterParser, + String filepathTemplate, Path home) { + this.parameters = parameters; + this.parameterName = parameterName; + this.parameterParser = parameterParser; + this.filepathTemplate = filepathTemplate; + this.home = home; + } + + public File find() { + String parameter = getParameterFromRequest(); + if (parameter == null) { + return null; + } else { + return doPatternMatching(parameter); + } + } + + private String getParameterFromRequest() { + String[] values = parameters.get(parameterName); + if (log.isDebugEnabled()) { + log.debug("Parameter value: =" + Arrays.asList(values)); + } + if (values == null || values.length == 0) { + log.warn("No value provided for request parameter '" + + parameterName + "'"); + return null; + } + if (values.length > 1) { + log.warn("Multiple values provided for request parameter '" + + parameterName + "': " + Arrays.deepToString(values)); + return null; + } + return values[0]; + } + + private File doPatternMatching(String parameter) { + Matcher m = parameterParser.matcher(parameter); + log.debug("Pattern matching: value=" + parameter + ", parser=" + + parameterParser + ", match=" + m); + if (m.find()) { + return substituteIntoFilepath(m); + } else { + log.warn("Failed to parse the request parameter: '" + + parameterParser + "' doesn't match '" + parameter + + "'"); + return null; + } + + } + + private File substituteIntoFilepath(Matcher m) { + String path = filepathTemplate; + for (int i = 0; i <= m.groupCount(); i++) { + path = path.replace("\\" + i, m.group(i)); + } + log.debug("Substitute: " + filepathTemplate + " becomes " + path); + return home.resolve(path).toFile(); + } + + } +} From 0a227d84b2b12cb2b3a195190ffd48a5f72f2800 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Tue, 14 Jan 2025 09:13:25 +0100 Subject: [PATCH 04/19] Fix for report generation --- .../api/DistributeDataApiController.java | 13 ++++ .../DataDistributorContextImpl.java | 22 +++++-- .../controller/admin/ReportingController.java | 10 ++- .../impl/RequestModelAccessImpl.java | 4 +- .../reporting/AbstractYARGTemplateReport.java | 7 ++- .../vitro/webapp/reporting/DataSource.java | 63 +++++++------------ .../webapp/reporting/OpenDopeWordReport.java | 10 +-- .../webapp/reporting/ReportGenerator.java | 5 +- .../webapp/reporting/TemplateExcelReport.java | 6 +- .../webapp/reporting/TemplateWordReport.java | 6 +- .../vitro/webapp/reporting/XmlGenerator.java | 4 +- 11 files changed, 87 insertions(+), 63 deletions(-) diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java index dca406a8bd..47b107ec63 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java @@ -75,6 +75,10 @@ private String findDistributorForAction(HttpServletRequest req, Model model) thr action = action.substring(1); } + return findDistributorUri(model, action); + } + + public static String findDistributorUri(Model model, String action) throws NoSuchActionException { List uris = createSelectQueryContext(model, DISTRIBUTOR_FOR_SPECIFIED_ACTION) .bindVariableToPlainLiteral("action", action).execute().toStringFields("distributor").flatten(); Collections.sort(uris); @@ -99,6 +103,15 @@ private DataDistributor instantiateDistributor(HttpServletRequest req, String di } } + public static DataDistributor instantiateDistributor(String distributorUri, Model model) + throws ActionFailedException { + try { + return new ConfigurationBeanLoader(model).loadInstance(distributorUri, DataDistributor.class); + } catch (ConfigurationBeanLoaderException e) { + throw new ActionFailedException("Failed to instantiate the DataDistributor: " + distributorUri, e); + } + } + private void runIt(HttpServletRequest req, HttpServletResponse resp, DataDistributor instance) throws DataDistributorException { try { diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java index aa1c10f714..382b119a77 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextImpl.java @@ -8,6 +8,7 @@ import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; @@ -15,26 +16,37 @@ * Build a DataDistributorContext around the current HTTP request. */ public class DataDistributorContextImpl implements DataDistributorContext { - private final HttpServletRequest req; + private final RequestModelAccess requestAccess; + private Map parameters; + private UserAccount account; public DataDistributorContextImpl(HttpServletRequest req) { - this.req = req; + this.requestAccess = ModelAccess.on(req); + this.parameters = req.getParameterMap(); + this.account = PolicyHelper.getUserAccount(req); + } + + public DataDistributorContextImpl(RequestModelAccess requestAccess, Map parameters, + UserAccount account) { + this.requestAccess = requestAccess; + this.parameters = parameters; + this.account = account; } @SuppressWarnings("unchecked") @Override public Map getRequestParameters() { - return req.getParameterMap(); + return parameters; } @Override public RequestModelAccess getRequestModels() { - return ModelAccess.on(req); + return requestAccess; } @Override public boolean isAuthorized(AuthorizationRequest actions) { - return PolicyHelper.isAuthorizedForActions(req, actions); + return PolicyHelper.isAuthorizedForActions(account , actions); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java index 327f8b09d5..462d9b62cd 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java @@ -21,6 +21,7 @@ import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor; import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission; import edu.cornell.mannlib.vitro.webapp.auth.policy.PolicyHelper; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.FreemarkerHttpServlet; import edu.cornell.mannlib.vitro.webapp.controller.freemarker.UrlBuilder; @@ -30,6 +31,8 @@ import edu.cornell.mannlib.vitro.webapp.dao.DataDistributorDao; import edu.cornell.mannlib.vitro.webapp.dao.ReportingDao; import edu.cornell.mannlib.vitro.webapp.i18n.I18n; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import edu.cornell.mannlib.vitro.webapp.reporting.AbstractTemplateReport; import edu.cornell.mannlib.vitro.webapp.reporting.DataDistributorEndpoint; import edu.cornell.mannlib.vitro.webapp.reporting.DataSource; @@ -392,9 +395,10 @@ private boolean processReport(String reportName, HttpServletRequest request, Htt try { // Set the content type for this report response.setContentType(report.getContentType()); - + UserAccount account = PolicyHelper.getUserAccount(vreq); + RequestModelAccess rma = ModelAccess.on(vreq); // Generate the report directly into the output stream - report.generateReport(response.getOutputStream()); + report.generateReport(response.getOutputStream(), rma, account); } catch (IOException | ReportGeneratorException e) { log.error("Unable to generate the report", e); } @@ -445,7 +449,7 @@ private boolean processDownloadXml(String action, HttpServletRequest request, Ht response.setContentType("text/xml"); // Get the xml from the report - Document xml = ((XmlGenerator) report).generateXml(); + Document xml = ((XmlGenerator) report).generateXml(ModelAccess.on(vreq), PolicyHelper.getUserAccount(vreq)); // Create an xml serializer DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/impl/RequestModelAccessImpl.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/impl/RequestModelAccessImpl.java index 9749d157d5..54ac682bec 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/impl/RequestModelAccessImpl.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/modelaccess/impl/RequestModelAccessImpl.java @@ -87,15 +87,13 @@ public class RequestModelAccessImpl implements RequestModelAccess { .getLog(RequestModelAccessImpl.class); private final HttpServletRequest req; - private final ServletContext ctx; private final ConfigurationProperties props; private final ShortTermCombinedTripleSource source; public RequestModelAccessImpl(HttpServletRequest req, ShortTermCombinedTripleSource source) { this.req = req; - this.ctx = req.getSession().getServletContext(); - this.props = ConfigurationProperties.getBean(req); + this.props = ConfigurationProperties.getInstance(); this.source = source; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java index f4b49e03c5..dd63f06ee0 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/AbstractYARGTemplateReport.java @@ -16,6 +16,8 @@ import com.haulmont.yarg.structure.impl.BandBuilder; import com.haulmont.yarg.structure.impl.ReportBuilder; import com.haulmont.yarg.structure.impl.ReportTemplateBuilder; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import org.apache.commons.lang3.StringUtils; /** @@ -26,7 +28,8 @@ public abstract class AbstractYARGTemplateReport extends AbstractTemplateReport /** * Generate the report */ - protected void generateReport(OutputStream outputStream, String name, ReportOutputType type) { + protected void generateReport(OutputStream outputStream, String name, ReportOutputType type, + RequestModelAccess request, UserAccount account) { // Create a new report builder and template ReportBuilder reportBuilder = new ReportBuilder(); ReportTemplateBuilder reportTemplateBuilder = new ReportTemplateBuilder(); @@ -48,7 +51,7 @@ protected void generateReport(OutputStream outputStream, String name, ReportOutp Map params = new HashMap(); for (DataSource dataSource : getDataSources()) { // Get the output of the datasource - String body = dataSource.getBody(new HashMap<>()); + String body = dataSource.getBody(new HashMap<>(), request, account); if (!StringUtils.isEmpty(body)) { // Bind the output to the name given in the datasource configuration params.put(dataSource.getOutputName(), body); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java index 9856bce033..6c028c163e 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/DataSource.java @@ -5,61 +5,44 @@ import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_DISTRIBUTORNAME; import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_DISTRIBUTORRANK; import static edu.cornell.mannlib.vitro.webapp.dao.ReportingDao.PROPERTY_OUTPUTNAME; +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY; -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; +import java.io.ByteArrayOutputStream; import java.util.Map; -import javax.net.ssl.SSLContext; - +import edu.cornell.library.scholars.webapp.controller.api.DistributeDataApiController; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextImpl; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; -import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.ssl.SSLContextBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.jena.rdf.model.Model; + /** * Defines a datasource for the report */ public class DataSource { + + private static final Log log = LogFactory.getLog(DataSource.class); private String distributorName; private String outputName; private int rank; - public String getBody(Map parameterMap) { - // Currently, only supporting the default endpoint. Eventually, this might be - // configurable - // so that the api of other installations can be used in a report - DataDistributorEndpoint endpoint = DataDistributorEndpoint.getDefault(); - URI myUri = endpoint.generateUri(distributorName, null); - + public String getBody(Map parameters, RequestModelAccess request, UserAccount account) { + Model model = request.getOntModel(DISPLAY); try { - SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (certificate, authType) -> true) - .build(); - HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext) - .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build(); - HttpResponse sourceResponse = httpClient.execute(new HttpGet(myUri)); - return IOUtils.toString(sourceResponse.getEntity().getContent(), StandardCharsets.UTF_8); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + String uri = DistributeDataApiController.findDistributorUri(model, distributorName); + DataDistributor instance = DistributeDataApiController.instantiateDistributor(uri, model); + instance.init(new DataDistributorContextImpl(request, parameters, account)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + instance.writeOutput(baos); + return baos.toString(); + } catch (Exception e) { + log.error(e, e); } - return null; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java index 3e1e1b39c9..cc31d66cda 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java @@ -16,6 +16,8 @@ import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import org.docx4j.Docx4J; import org.docx4j.openpackaging.exceptions.Docx4JException; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; @@ -39,7 +41,7 @@ public String getContentType() throws ReportGeneratorException { * Xml document is required for passing to the Docx4j library */ @Override - public Document generateXml() throws ReportGeneratorException { + public Document generateXml(RequestModelAccess request, UserAccount account) throws ReportGeneratorException { try { DocumentBuilder xmlBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document xmlDoc = xmlBuilder.newDocument(); @@ -50,7 +52,7 @@ public Document generateXml() throws ReportGeneratorException { // Execute all datasources for (DataSource dataSource : dataSources) { // Get the result of the datasource - String body = dataSource.getBody(new HashMap<>()); + String body = dataSource.getBody(new HashMap<>(), request, account); // Load the JSON document JsonFactory factory = new JsonFactory(); @@ -111,9 +113,9 @@ private void convertObjectNodeToXml(Document xmlDoc, String outputName, JsonNode } @Override - public void generateReport(OutputStream outputStream) throws ReportGeneratorException { + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { // Get the XML - Document xmlDoc = generateXml(); + Document xmlDoc = generateXml(request, account); try { // Create a word processing package from the template WordprocessingMLPackage wordMLPackage = Docx4J.load(new ByteArrayInputStream(template)); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java index 42a51a70d0..55061fe993 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java @@ -5,6 +5,9 @@ import java.io.OutputStream; import java.util.List; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; + /** * Interface for all reports */ @@ -31,7 +34,7 @@ public interface ReportGenerator { * @param outputStream Stream to write report into * @throws ReportGeneratorException */ - void generateReport(OutputStream outputStream) throws ReportGeneratorException; + void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException; void setIsPersistent(boolean isPersistent); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java index 56fc9e7ef1..b2aef926ec 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java @@ -5,6 +5,8 @@ import java.io.OutputStream; import com.haulmont.yarg.structure.ReportOutputType; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; /** * Generate an Excel report using the YARG template processor @@ -17,7 +19,7 @@ public String getContentType() throws ReportGeneratorException { } @Override - public void generateReport(OutputStream outputStream) throws ReportGeneratorException { - generateReport(outputStream, "report.xlsx", ReportOutputType.xlsx); + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { + generateReport(outputStream, "report.xlsx", ReportOutputType.xlsx, request, account); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java index 6a757fa3ca..0688e3b8da 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java @@ -5,6 +5,8 @@ import java.io.OutputStream; import com.haulmont.yarg.structure.ReportOutputType; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; /** * Generate a Word report using the YARG template processor @@ -17,7 +19,7 @@ public String getContentType() throws ReportGeneratorException { } @Override - public void generateReport(OutputStream outputStream) throws ReportGeneratorException { - generateReport(outputStream, "report.docx", ReportOutputType.docx); + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { + generateReport(outputStream, "report.docx", ReportOutputType.docx, request, account); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java index 7791343756..6b36aa6052 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/XmlGenerator.java @@ -2,6 +2,8 @@ package edu.cornell.mannlib.vitro.webapp.reporting; +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import org.w3c.dom.Document; /** @@ -9,5 +11,5 @@ * document */ public interface XmlGenerator { - Document generateXml() throws ReportGeneratorException; + Document generateXml(RequestModelAccess request, UserAccount account) throws ReportGeneratorException; } From 0399e88b0a39272509f1a2b113fe68969e680ddb Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 15 Jan 2025 16:28:18 +0100 Subject: [PATCH 05/19] removed translations not related to data distributor api --- .../webapp/local/i18n/all_de_DE.properties | 101 -- .../webapp/local/i18n/all_en_US.properties | 966 +----------------- 2 files changed, 1 insertion(+), 1066 deletions(-) delete mode 100644 webapp/src/main/webapp/local/i18n/all_de_DE.properties diff --git a/webapp/src/main/webapp/local/i18n/all_de_DE.properties b/webapp/src/main/webapp/local/i18n/all_de_DE.properties deleted file mode 100644 index 8c4bd33cc8..0000000000 --- a/webapp/src/main/webapp/local/i18n/all_de_DE.properties +++ /dev/null @@ -1,101 +0,0 @@ -# -# Local customisations override any provided messages -# -# Default (English) -# - - -### TODO this needs to go into Vitro-languages -### but for simplicity during dev it will be placed here - -# DataDistributor Config -manage_datadistributors = Manage Data Distributors -page_datadistributor_config = Admin: Data Distributor -dd_config_title = Data Distributor -dd_graphbuilder_config_title = Graph Builder -dd_config_name = Name -dd_config_type = Type -dd_config_add = Add Distributor -dd_config_edit = Edit -dd_config_save = Save -dd_config_delete = Delete -dd_config_delete_confirm = Are you sure you want to delete this entry? -dd_config_add_field = Add field -dd_config_run = RUN - -# Classes - DataDistributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor = JavaScript Transfrom Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor = Hello Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.FileDistributor = File Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor = Selecting File Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor = RDF Graph Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor = Select From Content Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor = Select From Graph Distributor - -# Classes - GraphBuilder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.ConstructQueryGraphBuilder = Construct Query Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.DrillDownGraphBuilder = Drill Down Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder = Empty Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.IteratingGraphBuilder = Iterating Graph Builder - -# Select Content -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName = Action Name -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#uriBinding = URI Binding -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#literalBinding = Literal Binding -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query = SPARQL Query - -# File -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType = MIME Type -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#path = File Path - -# Javascript -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript = Path to embedded script -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script = Script -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child = Wrapped distributor - -# RDF Graph -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#graphBuilder = Graph builder - -# Selecting File -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse = Response for empty data set -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName = Request parameter -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate = Template for file path -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern = Regular expression to extract file selector from request parameter - -# GraphBuilder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName = Builder Name - -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterValue = Paramerter Value -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#childGraphBuilder = Child Graph Builder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#topLevelGraphBuilder = Top Level Graph Builder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#drillDownQuery = Drill Down Query -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#constructQuery = Construct Query - -# Reporintg Config -manage_reports = Manage Reports -page_reporting_config = Admin: Reporting -reporting_config_title = Reporting -reporting_title = Reporting -reporting_config_add = Add Report -reporting_config_name = Name -reporting_config_type = Type -reporting_config_edit = Edit -reporting_config_save = Save -reporting_config_delete = Delete - -reporting_config_add_datasource = Add DataSource -reporting_download_template = Download template -reporting_template = Template: -reporting_download_xml = XML -reporting_run = RUN - -# Reporting classes -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport = OpenDope Word Template -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport = Excel Template -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport = Word Template - -reporting_config_field_reportName = Report Name - -reporting_config_dataSource_distributor = Distributor -reporting_config_dataSource_rank = Rank -reporting_config_dataSource_outputName = Output Name diff --git a/webapp/src/main/webapp/local/i18n/all_en_US.properties b/webapp/src/main/webapp/local/i18n/all_en_US.properties index e2cbd1c0e6..ff79caa64d 100644 --- a/webapp/src/main/webapp/local/i18n/all_en_US.properties +++ b/webapp/src/main/webapp/local/i18n/all_en_US.properties @@ -1,967 +1,3 @@ -# -# Text strings for the controllers and templates -# -# Default (English) -# -save_changes=Save changes -save_entry=Save entry -select_existing=Select existing -select_an_existing=Select an existing -add_an_entry_to=Add an entry of type -change_entry_for=Change entry for: -add_new_entry_for=Add new entry for: -change_text_for=Change text for: -cancel_link = Cancel -cancel_title = cancel -required_fields = required fields -or = or -alt_error_alert = Error alert icon -alt_confirmation = Confirmation icon -for=for -email_address = Email address -first_name = First name -last_name = Last name -roles = Roles -status = Status - -ascending_order = ascending order -descending_order = descending order -select_one = Select one - -type_more_characters = type more characters -no_match = no match - -request_failed = Request failed. Please contact your system administrator. - -# -# Image upload pages -# -upload_page_title = Upload image -upload_page_title_with_name = Upload image for {0} -upload_heading = Photo Upload - -replace_page_title = Replace image -replace_page_title_with_name = Replace image for {0} - -crop_page_title = Crop image -crop_page_title_with_name = Crop image for {0} - -current_photo = Current Photo -upload_photo = Upload a photo -replace_photo = Replace Photo -photo_types = (JPEG, GIF or PNG) -maximum_file_size = Maximum file size: {0} megabytes -minimum_image_dimensions = Minimum image dimensions: {0} x {1} pixels - -cropping_caption = Your profile photo will look like the image below. -cropping_note = To make adjustments, you can drag around and resize the photo to the right. \ -When you are happy with your photo click the "Save Photo" button. - -alt_thumbnail_photo = Individual photo -alt_image_to_crop = Image to be cropped -alt_preview_crop = Preview of photo cropped - -delete_link = Delete photo -submit_upload = Upload photo -submit_save = Save photo - -confirm_delete = Are you sure you want to delete this photo? - -imageUpload.errorNoURI = No entity URI was provided -imageUpload.errorUnrecognizedURI = This URI is not recognized as belonging to anyone: ''{0}'' -imageUpload.errorNoImageForCropping = There is no image file to be cropped. -imageUpload.errorImageTooSmall = The uploaded image should be at least {0} pixels high and {1} pixels wide. -imageUpload.errorUnknown = Sorry, we were unable to process the photo you provided. Please try another photo. -imageUpload.errorFileTooBig = Please upload an image smaller than {0} megabytes. -imageUpload.errorUnrecognizedFileType = ''{0}'' is not a recognized image file type. Please upload JPEG, GIF, or PNG files only. -imageUpload.errorNoPhotoSelected = Please browse and select a photo. -imageUpload.errorBadMultipartRequest = Failed to parse the multi-part request for uploading an image. -imageUpload.errorFormFieldMissing = The form did not contain a ''{0}'' field." - -# -# User Accounts pages -# -account_management = Account Management -user_accounts_link = User accounts -user_accounts_title = user accounts - -login_count = Login count -last_login = Last Login - -add_new_account = Add new account -edit_account = Edit account -external_auth_only = Externally Authenticated Only -reset_password = Reset password -reset_password_note = Note: Instructions for resetting the password will \ -be emailed to the address entered above. The password will not \ -be reset until the user follows the link provided in this email. -new_password = New password -confirm_password = Confirm new password -minimum_password_length = Minimum of {0} characters in length; maximum of {1}. -leave_password_unchanged = Leaving this blank means that the password will not be changed. -confirm_initial_password = Confirm initial password - -new_account_1 = A new account for -new_account_2 = was successfully created. -new_account_title = new account -new_account_notification = A notification email has been sent to {0} \ -with instructions for activating the account and creating a password. -updated_account_1 = The account for -updated_account_2 = has been updated. -updated_account_title = updated account -updated_account_notification = A confirmation email has been sent to {0} \ -with instructions for resetting a password. \ -The password will not be reset until the user follows the link provided in this email. -deleted_accounts = Deleted {0} {0, choice, 0#accounts|1#account|1 Date: Thu, 16 Jan 2025 09:52:19 +0100 Subject: [PATCH 06/19] translations for data distribution api --- .../firsttime/vitro_UiLabel.ttl | 464 ++++++++++++++++++ .../webapp/local/i18n/all_en_US.properties | 94 ---- 2 files changed, 464 insertions(+), 94 deletions(-) delete mode 100644 webapp/src/main/webapp/local/i18n/all_en_US.properties diff --git a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl index aedb481253..1d471e5092 100644 --- a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6445,3 +6445,467 @@ uil-data:suppress_operation_for_unrelated_individuals_of_this_class.Vitro uil:hasApp "Vitro" ; uil:hasKey "suppress_operation_for_unrelated_individuals_of_this_class" ; uil:hasPackage "Vitro-languages" . + +uil-data:manage_datadistributors.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Manage Data Distributors"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "manage_datadistributors" . + +uil-data:page_datadistributor_config.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Admin: Data Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "page_datadistributor_config" . + +uil-data:dd_config_title.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Data Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_title" . + +uil-data:dd_graphbuilder_config_title.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_graphbuilder_config_title" . + +uil-data:dd_config_name.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_name" . + +uil-data:dd_config_type.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Type"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_type" . + +uil-data:dd_config_add.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Add Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_add" . + +uil-data:dd_config_edit.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Edit"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edit" . + +uil-data:dd_config_save.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Save"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_save" . + +uil-data:dd_config_delete.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Delete"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_delete" . + +uil-data:dd_config_delete_confirm.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Are you sure you want to delete this entry?"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_delete_confirm" . + +uil-data:dd_config_add_field.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Add field"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_add_field" . + +uil-data:dd_config_run.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "RUN"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_run" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "JavaScript Transfrom Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Hello Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.FileDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "File Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.FileDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Selecting File Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "RDF Graph Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Select From Content Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Select From Graph Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.ConstructQueryGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Construct Query Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.ConstructQueryGraphBuilder" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.DrillDownGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Drill Down Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.DrillDownGraphBuilder" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Empty Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder" . + +uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.IteratingGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Iterating Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.IteratingGraphBuilder" . + +uil-data:dd_config_actionName.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Action Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName" . + +uil-data:dd_config_uriBinding.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "URI Binding"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#uriBinding" . + +uil-data:dd_config_literalBinding.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Literal Binding"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#literalBinding" . + +uil-data:dd_config_query.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "SPARQL Query"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query" . + +uil-data:dd_config_contentType.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "MIME Type"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType" . + +uil-data:dd_config_path.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "File Path"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#path" . + +uil-data:dd_config_supportingScript.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Path to embedded script"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript" . + +uil-data:dd_config_script.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Script"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script" . + +uil-data:dd_config_child.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Wrapped distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child" . + +uil-data:dd_config_graphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Graph builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#graphBuilder" . + +uil-data:dd_config_emptyResponse.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Response for empty data set"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse" . + +uil-data:dd_config_parameterName.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Request parameter"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName" . + +uil-data:dd_config_filepathTemplate.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Template for file path"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate" . + +uil-data:dd_config_parameterPattern.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Regular expression to extract file selector from request parameter"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern" . + +uil-data:dd_config_builderName.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Builder Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName" . + +uil-data:dd_config_parameterValue.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Paramerter Value"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterValue" . + +uil-data:dd_config_childGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Child Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#childGraphBuilder" . + +uil-data:dd_config_topLevelGraphBuilder.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Top Level Graph Builder"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#topLevelGraphBuilder" . + +uil-data:dd_config_drillDownQuery.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Drill Down Query"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#drillDownQuery" . + +uil-data:dd_config_constructQuery.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Construct Query"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "dd_config_http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#constructQuery" . + +uil-data:manage_reports.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Manage Reports"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "manage_reports" . + +uil-data:page_reporting_config.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Admin: Reporting"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "page_reporting_config" . + +uil-data:reporting_config_title.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Reporting"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_title" . + +uil-data:reporting_title.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Reporting"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_title" . + +uil-data:reporting_config_add.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Add Report"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_add" . + +uil-data:reporting_config_name.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_name" . + +uil-data:reporting_config_type.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Type"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_type" . + +uil-data:reporting_config_edit.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Edit"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_edit" . + +uil-data:reporting_config_save.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Save"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_save" . + +uil-data:reporting_config_delete.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Delete"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_delete" . + +uil-data:reporting_config_add_datasource.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Add DataSource"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_add_datasource" . + +uil-data:reporting_download_template.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Download template"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_download_template" . + +uil-data:reporting_template.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Template:"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_template" . + +uil-data:reporting_download_xml.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "XML"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_download_xml" . + +uil-data:reporting_run.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "RUN"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_run" . + +uil-data:reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "OpenDope Word Template"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport" . + +uil-data:reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Excel Template"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport" . + +uil-data:reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Word Template"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport" . + +uil-data:reporting_config_field_reportName.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Report Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_field_reportName" . + +uil-data:reporting_config_dataSource_distributor.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Distributor"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_dataSource_distributor" . + +uil-data:reporting_config_dataSource_rank.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Rank"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_dataSource_rank" . + +uil-data:reporting_config_dataSource_outputName.Vitro + rdf:type owl:NamedIndividual ; + rdf:type uil:UILabel ; + rdfs:label "Output Name"@en-US ; + uil:hasApp "Vitro" ; + uil:hasKey "reporting_config_dataSource_outputName" . + + diff --git a/webapp/src/main/webapp/local/i18n/all_en_US.properties b/webapp/src/main/webapp/local/i18n/all_en_US.properties deleted file mode 100644 index ff79caa64d..0000000000 --- a/webapp/src/main/webapp/local/i18n/all_en_US.properties +++ /dev/null @@ -1,94 +0,0 @@ -### TODO this needs to go into Vitro-languages -### but for simplicity during dev it will be placed here - -# DataDistributor Config -manage_datadistributors = Manage Data Distributors -page_datadistributor_config = Admin: Data Distributor -dd_config_title = Data Distributor -dd_graphbuilder_config_title = Graph Builder -dd_config_name = Name -dd_config_type = Type -dd_config_add = Add Distributor -dd_config_edit = Edit -dd_config_save = Save -dd_config_delete = Delete -dd_config_delete_confirm = Are you sure you want to delete this entry? -dd_config_add_field = Add field -dd_config_run = RUN - -# Classes - DataDistributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor = JavaScript Transfrom Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor = Hello Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.FileDistributor = File Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor = Selecting File Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.RdfGraphDistributor = RDF Graph Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromContentDistributor = Select From Content Distributor -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.SelectFromGraphDistributor = Select From Graph Distributor - -# Classes - GraphBuilder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.ConstructQueryGraphBuilder = Construct Query Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.DrillDownGraphBuilder = Drill Down Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder = Empty Graph Builder -dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.IteratingGraphBuilder = Iterating Graph Builder - -# Select Content -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName = Action Name -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#uriBinding = URI Binding -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#literalBinding = Literal Binding -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#query = SPARQL Query - -# File -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType = MIME Type -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#path = File Path - -# Javascript -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript = Path to embedded script -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script = Script -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child = Wrapped distributor - -# RDF Graph -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#graphBuilder = Graph builder - -# Selecting File -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse = Response for empty data set -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName = Request parameter -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate = Template for file path -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern = Regular expression to extract file selector from request parameter - -# GraphBuilder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#builderName = Builder Name - -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterValue = Paramerter Value -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#childGraphBuilder = Child Graph Builder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#topLevelGraphBuilder = Top Level Graph Builder -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#drillDownQuery = Drill Down Query -dd_config_http\://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#constructQuery = Construct Query - -# Reporintg Config -manage_reports = Manage Reports -page_reporting_config = Admin: Reporting -reporting_config_title = Reporting -reporting_title = Reporting -reporting_config_add = Add Report -reporting_config_name = Name -reporting_config_type = Type -reporting_config_edit = Edit -reporting_config_save = Save -reporting_config_delete = Delete - -reporting_config_add_datasource = Add DataSource -reporting_download_template = Download template -reporting_template = Template: -reporting_download_xml = XML -reporting_run = RUN - -# Reporting classes -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.OpenDopeWordReport = OpenDope Word Template -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateExcelReport = Excel Template -reporting_config_edu.cornell.mannlib.vitro.webapp.reporting.TemplateWordReport = Word Template - -reporting_config_field_reportName = Report Name - -reporting_config_dataSource_distributor = Distributor -reporting_config_dataSource_rank = Rank -reporting_config_dataSource_outputName = Output Name From 8756bb364fec790f69fd87e5238a4f2db1bd9cbf Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Thu, 16 Jan 2025 10:25:36 +0100 Subject: [PATCH 07/19] Permissions for data distributors --- .../rdf/accessControl/firsttime/simple_permissions_admin.n3 | 4 ++++ .../rdf/accessControl/firsttime/simple_permissions_curator.n3 | 1 + .../rdf/accessControl/firsttime/simple_permissions_editor.n3 | 1 + 3 files changed, 6 insertions(+) diff --git a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_admin.n3 b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_admin.n3 index d22823672d..d735ca0346 100644 --- a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_admin.n3 +++ b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_admin.n3 @@ -19,6 +19,9 @@ access:value simplePermission:UseMiscellaneousAdminPages ; access:value simplePermission:UseSparqlQueryPage ; access:value simplePermission:PageViewableAdmin ; + access:value simplePermission:ManageDataDistributors ; + access:value simplePermission:ManageReports ; + # Uncomment the following permission line to enable the SPARQL update API. # Before enabling, be sure that the URL api/sparqlUpdate is secured by HTTPS, @@ -33,6 +36,7 @@ access:value simplePermission:PageViewableCurator ; # permissions for EDITOR and above. + access:value simplePermission:ExecuteReports ; access:value simplePermission:DoBackEndEditing ; access:value simplePermission:SeeIndividualEditingPanel ; access:value simplePermission:SeeRevisionInfo ; diff --git a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_curator.n3 b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_curator.n3 index d488b8e54f..b189d5a69c 100644 --- a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_curator.n3 +++ b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_curator.n3 @@ -12,6 +12,7 @@ access:value simplePermission:PageViewableCurator ; # permissions for EDITOR and above. + access:value simplePermission:ExecuteReports ; access:value simplePermission:DoBackEndEditing ; access:value simplePermission:SeeIndividualEditingPanel ; access:value simplePermission:SeeRevisionInfo ; diff --git a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_editor.n3 b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_editor.n3 index ec155b9fdf..738b0bf941 100644 --- a/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_editor.n3 +++ b/home/src/main/resources/rdf/accessControl/firsttime/simple_permissions_editor.n3 @@ -6,6 +6,7 @@ :EditorSimplePermissionValueSet # permissions for EDITOR and above. + access:value simplePermission:ExecuteReports ; access:value simplePermission:DoBackEndEditing ; access:value simplePermission:SeeIndividualEditingPanel ; access:value simplePermission:SeeRevisionInfo ; From e7ddebef066cdf1664ba62e812f9ba6f5c21ce16 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Thu, 16 Jan 2025 10:38:06 +0100 Subject: [PATCH 08/19] Code style fixes --- .../JavaScriptTransformDistributor.java | 24 ++++--- .../distribute/examples/HelloDistributor.java | 8 +-- .../api/distribute/file/FileDistributor.java | 12 ++-- .../file/SelectingFileDistributor.java | 65 ++++++++++--------- .../controller/admin/ReportingController.java | 6 +- .../webapp/reporting/OpenDopeWordReport.java | 7 +- .../webapp/reporting/ReportGenerator.java | 3 +- .../webapp/reporting/TemplateExcelReport.java | 3 +- .../webapp/reporting/TemplateWordReport.java | 3 +- 9 files changed, 73 insertions(+), 58 deletions(-) diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java index eddff0a5a5..5249681c7c 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java @@ -2,6 +2,21 @@ package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.servlet.ServletContext; + import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; @@ -10,15 +25,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import javax.script.Invocable; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import javax.servlet.ServletContext; -import java.io.*; -import java.util.ArrayList; -import java.util.List; - /** * Wrap a data distributor with a JavaScript function that will transform its * output. diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java index 15702ae67f..454cd82a13 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java @@ -2,12 +2,12 @@ package edu.cornell.library.scholars.webapp.controller.api.distribute.examples; -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; - import java.io.IOException; import java.io.OutputStream; +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; + /** * A simple example of a data distributor. It sends a greeting. */ @@ -46,7 +46,7 @@ public String getContentType() throws DataDistributorException { public void writeOutput(OutputStream output) throws DataDistributorException { try { - if (parameters.containsKey(NAME_PARAMETER_KEY)) { + if (parameters.containsKey(NAME_PARAMETER_KEY)) { output.write(String .format("Hello, %s!", parameters.get(NAME_PARAMETER_KEY)[0]) diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java index d0352f1adb..ea66d23cad 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributor.java @@ -2,6 +2,12 @@ package edu.cornell.library.scholars.webapp.controller.api.distribute.file; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; @@ -9,12 +15,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; - /** * Serve a particular file, when requested. * diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java index 8e0e815fab..18f61ff97b 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java @@ -2,14 +2,6 @@ package edu.cornell.library.scholars.webapp.controller.api.distribute.file; -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; -import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -20,6 +12,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** *

    * Selects one of several files and distributes it, based on the value of a @@ -86,38 +86,41 @@ public class SelectingFileDistributor extends AbstractDataDistributor { private FileFinder fileFinder; - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName", minOccurs = 1, maxOccurs = 1) + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterName", minOccurs = 1, + maxOccurs = 1) public void setParameterName(String name) { parameterName = name; } - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern", minOccurs = 1, maxOccurs = 1) + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#parameterPattern", minOccurs = 1, + maxOccurs = 1) public void setParameterPattern(String pattern) { parameterParser = Pattern.compile(pattern); } - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate", minOccurs = 1, maxOccurs = 1) + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#filepathTemplate", minOccurs = 1, + maxOccurs = 1) public void setFilepathTemplate(String template) { filepathTemplate = template; } - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, + maxOccurs = 1) public void setContentType(String cType) { contentType = cType; } - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse", minOccurs = 1, maxOccurs = 1) + @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#emptyResponse", minOccurs = 1, + maxOccurs = 1) public void setEmptyResponse(String response) { emptyResponse = response; } @Override - public void init(DataDistributorContext context) - throws DataDistributorException { + public void init(DataDistributorContext context) throws DataDistributorException { super.init(context); - fileFinder = new FileFinder(parameters, parameterName, parameterParser, - filepathTemplate, - ApplicationUtils.instance().getHomeDirectory().getPath()); + fileFinder = new FileFinder(parameters, parameterName, parameterParser, filepathTemplate, ApplicationUtils + .instance().getHomeDirectory().getPath()); } @Override @@ -126,8 +129,7 @@ public String getContentType() throws DataDistributorException { } @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { + public void writeOutput(OutputStream output) throws DataDistributorException { try { File file = fileFinder.find(); if (file != null && file.isFile()) { @@ -156,8 +158,7 @@ protected static class FileFinder { private final String filepathTemplate; private final Path home; - public FileFinder(Map parameters, - String parameterName, Pattern parameterParser, + public FileFinder(Map parameters, String parameterName, Pattern parameterParser, String filepathTemplate, Path home) { this.parameters = parameters; this.parameterName = parameterName; @@ -181,13 +182,14 @@ private String getParameterFromRequest() { log.debug("Parameter value: =" + Arrays.asList(values)); } if (values == null || values.length == 0) { - log.warn("No value provided for request parameter '" - + parameterName + "'"); + log.warn("No value provided for request parameter '" + parameterName + "'"); return null; } if (values.length > 1) { - log.warn("Multiple values provided for request parameter '" - + parameterName + "': " + Arrays.deepToString(values)); + log.warn("Multiple values provided for request parameter '" + + parameterName + + "': " + + Arrays.deepToString(values)); return null; } return values[0]; @@ -195,14 +197,15 @@ private String getParameterFromRequest() { private File doPatternMatching(String parameter) { Matcher m = parameterParser.matcher(parameter); - log.debug("Pattern matching: value=" + parameter + ", parser=" - + parameterParser + ", match=" + m); + log.debug("Pattern matching: value=" + parameter + ", parser=" + parameterParser + ", match=" + m); if (m.find()) { return substituteIntoFilepath(m); } else { - log.warn("Failed to parse the request parameter: '" - + parameterParser + "' doesn't match '" + parameter - + "'"); + log.warn("Failed to parse the request parameter: '" + + parameterParser + + "' doesn't match '" + + parameter + + "'"); return null; } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java index 462d9b62cd..df5318a2b7 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/controller/admin/ReportingController.java @@ -449,7 +449,8 @@ private boolean processDownloadXml(String action, HttpServletRequest request, Ht response.setContentType("text/xml"); // Get the xml from the report - Document xml = ((XmlGenerator) report).generateXml(ModelAccess.on(vreq), PolicyHelper.getUserAccount(vreq)); + Document xml = ((XmlGenerator) report).generateXml(ModelAccess.on(vreq), PolicyHelper.getUserAccount( + vreq)); // Create an xml serializer DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); @@ -463,7 +464,8 @@ private boolean processDownloadXml(String action, HttpServletRequest request, Ht serializer.write(xml, output); return true; } - } catch (IOException | ReportGeneratorException | IllegalAccessException | InstantiationException + } catch (IOException | ReportGeneratorException + | IllegalAccessException | InstantiationException | ClassNotFoundException e) { log.error("Unable to generate the xml", e); } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java index cc31d66cda..c381165276 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/OpenDopeWordReport.java @@ -25,8 +25,8 @@ import org.w3c.dom.Element; /** - * Template based report that uses the Docx4j library to process Word templates - * that have the OpenDOPE controls embedded in them. + * Template based report that uses the Docx4j library to process Word templates that have the OpenDOPE controls embedded + * in them. */ public class OpenDopeWordReport extends AbstractTemplateReport implements XmlGenerator { /** @@ -113,7 +113,8 @@ private void convertObjectNodeToXml(Document xmlDoc, String outputName, JsonNode } @Override - public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) + throws ReportGeneratorException { // Get the XML Document xmlDoc = generateXml(request, account); try { diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java index 55061fe993..a073caca1c 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/ReportGenerator.java @@ -34,7 +34,8 @@ public interface ReportGenerator { * @param outputStream Stream to write report into * @throws ReportGeneratorException */ - void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException; + void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) + throws ReportGeneratorException; void setIsPersistent(boolean isPersistent); diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java index b2aef926ec..161c2cc98a 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReport.java @@ -19,7 +19,8 @@ public String getContentType() throws ReportGeneratorException { } @Override - public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) + throws ReportGeneratorException { generateReport(outputStream, "report.xlsx", ReportOutputType.xlsx, request, account); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java index 0688e3b8da..a3c3f50a7d 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReport.java @@ -19,7 +19,8 @@ public String getContentType() throws ReportGeneratorException { } @Override - public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) throws ReportGeneratorException { + public void generateReport(OutputStream outputStream, RequestModelAccess request, UserAccount account) + throws ReportGeneratorException { generateReport(outputStream, "report.docx", ReportOutputType.docx, request, account); } } From 27467e254800d7aefa274e8f6b3533cd8d2ae2bb Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Thu, 16 Jan 2025 10:52:01 +0100 Subject: [PATCH 09/19] Updated dependency --- api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pom.xml b/api/pom.xml index c031ab41d6..773898fa50 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -94,7 +94,7 @@ org.reflections reflections - 0.9.11 + 0.10.2 javax.servlet From a9feb42786d8ed1e8f7f66c21bed67a0df260cc2 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 22 Jan 2025 14:45:47 +0100 Subject: [PATCH 10/19] Copied tests from https://github.com/vivo-community/vivo-data-distribution-api/tree/main/vivo_1_10/src/test/java --- .../api/DistributeDataApiControllerTest.java | 253 +++++++++++ .../DataDistributorContextTest.java | 74 +++ .../JavaScriptTransformDistributorTest.java | 424 ++++++++++++++++++ .../distribute/file/FileDistributorTest.java | 232 ++++++++++ ...lectingFileDistributor_FileFinderTest.java | 139 ++++++ .../rdf/SelectFromContentDistributorTest.java | 289 ++++++++++++ .../rdf/SelectFromGraphDistributorTest.java | 44 ++ .../ConstructQueryGraphBuilderTest.java | 200 +++++++++ .../DrilldownGraphBuilderTest.java | 167 +++++++ .../rdf/graphbuilder/GraphBuilderStub.java | 61 +++ .../GraphBuilderUtilitiesTest.java | 137 ++++++ .../IteratingGraphBuilderTest.java | 73 +++ .../rdf/util/VariableBinderTest.java | 228 ++++++++++ .../DataDistributorContextStub.java | 71 +++ 14 files changed, 2392 insertions(+) create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java create mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java create mode 100644 api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java new file mode 100644 index 0000000000..b4e6257de6 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java @@ -0,0 +1,253 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.typeStatement; +import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; + +import org.apache.jena.ontology.OntModel; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.log4j.Level; +import org.junit.Before; +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; +import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; +import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccessStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpServletResponseStub; + +/** + * TODO + */ +public class DistributeDataApiControllerTest extends AbstractTestClass { + private static final String DDIST_URI_1 = "http://test/distributor1"; + private static final String DDIST_URI_2 = "http://test/distributor2"; + private static final String ACTION_NAME_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName"; + private static final String FAIL_METHOD_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#failMethod"; + + private HttpServletRequestStub req; + private HttpServletResponseStub resp; + private RequestModelAccessStub requestModels; + private DistributeDataApiController controller; + + @Before + public void setup() { + req = new HttpServletRequestStub(); + resp = new HttpServletResponseStub(); + requestModels = new ModelAccessFactoryStub().get(req); + + controller = new DistributeDataApiController(); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void emptyAction_400_badRequest() + throws ServletException, IOException { + setActionPath(""); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + runIt(400, "'action' path was not provided."); + } + + @Test + public void unrecognizedAction_400_badRequest() + throws ServletException, IOException { + setActionPath("unknown"); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + runIt(400, "Did not find a DataDistributor for 'unknown'"); + } + + @Test + public void multipleActions_warning() throws ServletException, IOException { + StringWriter w = new StringWriter(); + captureLogOutput(DistributeDataApiController.class, w, true); + + setActionPath("multiples"); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "multiples"), + dd(DDIST_URI_2, DDTestDistributor.class, "multiples")); + runIt(200, "success"); + + assertTrue(w.toString() + .contains("more than one DataDistributor for 'multiples'")); + } + + @Test + public void failToInstantiate_500_serverError() + throws ServletException, IOException { + setLoggerLevel(DistributeDataApiController.class, Level.ERROR); + setActionPath("simpleSuccess"); + populateDisplayModel( + dd(DDIST_URI_1, TestFailure.class, "simpleSuccess")); + runIt(500, "Failed to instantiate"); + } + + @Test + public void initThrowsException_500_serverError() + throws ServletException, IOException { + setLoggerLevel(DistributeDataApiController.class, Level.OFF); + setActionPath("initFails"); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "initFails", "init")); + runIt(500, "forced error in init()"); + } + + @Test + public void getContextTypeThrowsException_500_serverError() + throws ServletException, IOException { + setLoggerLevel(DistributeDataApiController.class, Level.OFF); + setActionPath("getContentTypeFails"); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, + "getContentTypeFails", "getContentType")); + runIt(500, "forced error in getContentType()"); + } + + @Test + public void writeOutputThrowsException_500_serverError() + throws ServletException, IOException { + setLoggerLevel(DistributeDataApiController.class, Level.OFF); + setActionPath("writeOutputFails"); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, + "writeOutputFails", "writeOutput")); + runIt(500, "forced error in writeOutput()"); + } + + @Test + public void simpleSuccess() throws ServletException, IOException { + setActionPath("simpleSuccess"); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + runIt(200, "success"); + } + + @Test + public void actionPrefixedBySlash_success() + throws ServletException, IOException { + setActionPath("/simpleSuccess"); + populateDisplayModel( + dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + runIt(200, "success"); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void setActionPath(String actionPath) { + req.setRequestUrlByParts("http://test", "", "/dataRequest", actionPath); + } + + private void populateDisplayModel(Model... models) { + OntModel om = ModelFactory.createOntologyModel(); + for (Model model : models) { + om.add(model); + } + requestModels.setOntModel(om, ModelNames.DISPLAY); + } + + private Model dd(String uri, Class class1, String actionName, + String... failMethods) { + Model m = model(typeStatement(uri, toJavaUri(class1)), + typeStatement(uri, toJavaUri(DataDistributor.class)), + dataProperty(uri, ACTION_NAME_PROPERTY, actionName)); + + for (String failMethod : failMethods) { + m.add(dataProperty(uri, FAIL_METHOD_PROPERTY, failMethod)); + } + + return m; + } + + private void runIt(int expectedStatus, String expectedOutput) + throws ServletException, IOException { + controller.doGet(req, resp); + assertEquals(expectedStatus, resp.getStatus()); + String actualOutput = resp.getOutput(); + if (!actualOutput.contains(expectedOutput)) { + fail("expect output to contain>" + expectedOutput + "" + + actualOutput + "<"); + } + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + public static class DDTestDistributor implements DataDistributor { + private Set failureMethods = new HashSet<>(); + + @SuppressWarnings("unused") + @Property(uri = ACTION_NAME_PROPERTY) + public void setActionName(String actionName) { + // Nothing to do + } + + @Property(uri = FAIL_METHOD_PROPERTY) + public void addFailureMethod(String methodName) { + failureMethods.add(methodName); + } + + @Override + public void init(DataDistributorContext ddContext) + throws DataDistributorException { + if (failureMethods.contains("init")) { + throw new DataDistributorException("forced error in init()"); + } + } + + @Override + public String getContentType() throws DataDistributorException { + if (failureMethods.contains("getContentType")) { + throw new DataDistributorException( + "forced error in getContentType()"); + } + return "text/plain"; + } + + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + if (failureMethods.contains("writeOutput")) { + throw new DataDistributorException( + "forced error in writeOutput()"); + } + try { + output.write("success".getBytes()); + } catch (IOException e) { + throw new ActionFailedException(e); + } + } + + @Override + public void close() throws DataDistributorException { + // Nothing to do + } + } + + public static class TestFailure { + // Won't instantiate. + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java new file mode 100644 index 0000000000..6feaed3790 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java @@ -0,0 +1,74 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.arraysToLists; +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.deepCopyParameters; +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.listsToArrays; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + +/** + * TODO + */ +public class DataDistributorContextTest extends AbstractTestClass { + + private static final Map ARRAYS = initArrays(); + + private static Map initArrays() { + HashMap map = new HashMap<>(); + map.put("nameZero", new String[] {}); + map.put("nameOne", new String[] { "valueOne" }); + map.put("nameTwo", new String[] { "valueTwoA", "valueTwoB" }); + return map; + } + + private static final Map> LISTS = initLists(); + + private static Map> initLists() { + HashMap> map = new HashMap<>(); + map.put("nameZero", new ArrayList<>()); + map.put("nameOne", new ArrayList<>(Arrays.asList("valueOne"))); + map.put("nameTwo", + new ArrayList<>(Arrays.asList("valueTwoA", "valueTwoB"))); + return map; + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void convertArraysToLists() { + assertEquals(LISTS, arraysToLists(ARRAYS)); + } + + @Test + public void convertListsToArrays() { + assertEqualArrayMaps(ARRAYS, listsToArrays(LISTS)); + } + + @Test + public void deepCopy() { + assertEqualArrayMaps(ARRAYS, deepCopyParameters(ARRAYS)); + } + + @SuppressWarnings("deprecation") + private void assertEqualArrayMaps(Map a1, + Map a2) { + assertEquals("key sets should be equals", a1.keySet(), a2.keySet()); + for (String key : a1.keySet()) { + assertEquals("array values for '" + key + "'", a1.get(key), + a2.get(key)); + } + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java new file mode 100644 index 0000000000..ca7bf69a51 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java @@ -0,0 +1,424 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; + +import javax.script.ScriptException; + +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; +import stubs.edu.cornell.mannlib.vitro.webapp.modules.ApplicationStub; +import stubs.javax.servlet.ServletContextStub; + +/** + * Test the basic functions of JavaScriptTransformDistributor. + * + * The simple transform just returns a hard-coded String. + * + * The full script accepts a JSON string, parses it, substitutes a value, and + * returns the stringified result. + * + * The multi-script requires two additional scripts in order to assemble a + * hard-coded String. + */ +public class JavaScriptTransformDistributorTest extends AbstractTestClass { + private static final String ACTION_NAME = "tester"; + private static final String JAVASCRIPT_TYPE = "text/javascript"; + private static final String TEXT_TYPE = "text/plain"; + + private static final String BAD_SYNTAX_SCRIPT = "" // + + "function transform( {}"; + + private static final String WRONG_FUNCTION_SCRIPT = "" // + + "function notTransform() { \n" // + + " return 'true'; \n" // + + "}"; + + private static final String WRONG_RETURN_TYPE_SCRIPT = "" // + + "function transform() { \n" // + + " return 3; \n" // + + "}"; + + private static final String SIMPLE_SCRIPT = "" // + + "function transform() { \n" // + + " return 'true'; \n" // + + "}"; + + private static final String SIMPLE_EXPECTED_RESULT = "true"; + + private static final String ECHO_SCRIPT = "" // + + "function transform(data) { \n" // + + " return data; \n" // + + "}"; + + private static final String UNICODE_STRING = "Lévesque"; + + private static final String FULL_SCRIPT = "" // + + "function transform(data) { \n" // + + " var initial = JSON.parse(data); \n" // + + " var result = {}; \n" // + + " Object.keys(initial).forEach(populate); \n" // + + " return JSON.stringify(result); \n" // + + "\n" // + + " function populate(key) { \n" // + + " result[key] = (initial[key] == 'initial') ? \n" // + + " 'transformed' : initial[key]; \n" // + + " } \n" // + + "}"; + + private static final String TRANSFORMED_STRUCTURE = "{ 'a': 'transformed', 'b': 'constant' }" + .replace('\'', '\"'); + + private static final String INITIAL_STRUCTURE = "{ 'a': 'initial', 'b': 'constant' }" + .replace('\'', '"'); + + private static final String PATH_TO_MISSING_SCRIPT = "/no/script/here"; + private static final String PATH_TO_BAD_SYNTAX_SCRIPT = "/bad/syntax/script.js"; + + private static final String PATH_TO_SUPPORTING_SCRIPT_1 = "/support/script1.js"; + private static final String PATH_TO_SUPPORTING_SCRIPT_2 = "/support/script2.js"; + + private static final String SUPPORTING_SCRIPT_1 = "" // + + "function one() { \n" // + + " return '1'; \n" // + + "}"; + private static final String SUPPORTING_SCRIPT_2 = "" // + + "function two() { \n" // + + " return '2'; \n" // + + "}"; + private static final String SUPPORTED_SCRIPT = "" // + + "function transform() { \n" // + + " return one() + ' ' + two() + ' 3'; \n" // + + "}"; + private static final String SUPPORTED_EXPECTED_RESULT = "1 2 3"; + + private static final String LOGGING_SCRIPT = "" // + + "function transform() { \n" // + + " logger.debug('debug message'); \n" // + + " logger.info('info message'); \n" // + + " logger.warn('warn message'); \n" // + + " logger.error('error message'); \n" // + + " return ''; \n" // + + "}"; + private static final String LOGGER_NAME = JavaScriptTransformDistributor.class + .getName() + "." + ACTION_NAME; + + public JavaScriptTransformDistributor transformer; + public TestDistributor child; + public DataDistributorContext ddc; + public ByteArrayOutputStream outputStream; + public String logOutput; + + @Before + public void setup() { + ServletContextStub ctx = new ServletContextStub(); + ctx.setMockResource(PATH_TO_BAD_SYNTAX_SCRIPT, BAD_SYNTAX_SCRIPT); + ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_1, SUPPORTING_SCRIPT_1); + ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_2, SUPPORTING_SCRIPT_2); + ApplicationStub.setup(ctx, null); + + ddc = new DataDistributorContextStub(null); + outputStream = new ByteArrayOutputStream(); + + transformer = new JavaScriptTransformDistributor(); + transformer.setContentType(JAVASCRIPT_TYPE); + transformer.setActionName(ACTION_NAME); + } + + // ---------------------------------------------------------------------- + // Basic tests + // ---------------------------------------------------------------------- + + @Test + public void scriptSuntaxError_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, "syntax", + ScriptException.class, "but found"); + transformAndCheck("", BAD_SYNTAX_SCRIPT, ""); + } + + @Test + public void noTransformFunction_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, "must have", + NoSuchMethodException.class, "transform"); + transformAndCheck("", WRONG_FUNCTION_SCRIPT, ""); + } + + @Test + public void childThrowsException_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, "Child", + DataDistributorException.class, "forced"); + child = new TestDistributor(JAVASCRIPT_TYPE, "", true); + transformAndCheck(SIMPLE_SCRIPT, ""); + } + + @Test + public void wrongReturnType_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, "must return a String"); + transformAndCheck("", WRONG_RETURN_TYPE_SCRIPT, ""); + } + + @Test + public void mostBasicTransform() throws DataDistributorException { + transformAndCheck("", SIMPLE_SCRIPT, SIMPLE_EXPECTED_RESULT); + } + + @Test + public void parseTransformAndStringify() throws DataDistributorException { + transformAndCheck(INITIAL_STRUCTURE, FULL_SCRIPT, + TRANSFORMED_STRUCTURE); + } + + /** + * This test is intended to check whether Unicode is handled properly + * regardless of the system's default file encoding. + * + * However there might be failures that only show up if the default value of + * the system property file.encoding is not UTF-8. + * + * The commented code is a hacky way of ensuring that the file encoding is + * not UTF-8, but in Java 9 or later, it will cause warning messages. + * + * So we have a test that might pass in some environments and fail in + * others. + */ + @Test + public void unicodeCharactersArePreserved() + throws DataDistributorException, UnsupportedEncodingException { + // try { + // System.setProperty("file.encoding", "ANSI_X3.4-1968"); + // Field charset = Charset.class.getDeclaredField("defaultCharset"); + // charset.setAccessible(true); + // charset.set(null, null); + // } catch (Exception e) { + // throw new RuntimeException(e); + // } + + child = new TestDistributor(TEXT_TYPE, UNICODE_STRING); + runTransformer(ECHO_SCRIPT); + assertEquals(UNICODE_STRING, + new String(outputStream.toByteArray(), "UTF-8")); + } + + // ---------------------------------------------------------------------- + // Tests with additional scripts + // ---------------------------------------------------------------------- + + @Test + public void additionalScriptNotFound_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, "Can't locate"); + addScripts(PATH_TO_MISSING_SCRIPT); + transformAndCheck("", SIMPLE_SCRIPT, ""); + } + + @Test + public void additionalScriptSyntaxError_throwsException() + throws DataDistributorException { + expectException(DataDistributorException.class, + PATH_TO_BAD_SYNTAX_SCRIPT, ScriptException.class, "but found"); + addScripts(PATH_TO_BAD_SYNTAX_SCRIPT); + transformAndCheck("", SIMPLE_SCRIPT, ""); + } + + @Test + public void useAdditionalScripts() throws DataDistributorException { + addScripts(PATH_TO_SUPPORTING_SCRIPT_1, PATH_TO_SUPPORTING_SCRIPT_2); + transformAndCheck("", SUPPORTED_SCRIPT, SUPPORTED_EXPECTED_RESULT); + } + + // ---------------------------------------------------------------------- + // Tests with logging + // ---------------------------------------------------------------------- + + @Test + public void loggingAtDebug_producedFourMessages() + throws DataDistributorException { + transformAndCountLogLines("", LOGGING_SCRIPT, Level.DEBUG, 4); + } + + @Test + public void loggingAtInfo_producesThreeMessages() + throws DataDistributorException { + transformAndCountLogLines("", LOGGING_SCRIPT, Level.INFO, 3); + } + + @Test + public void loggingAtWarn_producesTwoMessages() + throws DataDistributorException { + transformAndCountLogLines("", LOGGING_SCRIPT, Level.WARN, 2); + } + + @Test + public void loggingAtError_producesOneMessage() + throws DataDistributorException { + transformAndCountLogLines("", LOGGING_SCRIPT, Level.ERROR, 1); + } + + @Test + public void loggingAtOff_producesNoMessages() + throws DataDistributorException { + transformAndCountLogLines("", LOGGING_SCRIPT, Level.OFF, 0); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void transformAndCheck(String initial, String script, + String expected) throws DataDistributorException { + child = new TestDistributor(JAVASCRIPT_TYPE, initial); + transformAndCheck(script, expected); + } + + private void transformAndCheck(String script, String expected) + throws DataDistributorException { + runTransformer(script); + assertEquivalentJson(expected, new String(outputStream.toByteArray())); + } + + private void transformAndCountLogLines(String initial, String script, + Level level, int expectedCount) throws DataDistributorException { + setLoggerLevel(LOGGER_NAME, level); + logOutput = runTransformer(initial, script); + assertNumberOfLines(logOutput, expectedCount); + } + + private String runTransformer(String initial, String script) + throws DataDistributorException { + child = new TestDistributor(JAVASCRIPT_TYPE, initial); + return runTransformer(script); + } + + private String runTransformer(String script) + throws DataDistributorException { + try (Writer logCapture = new StringWriter()) { + captureLogOutput(LOGGER_NAME, logCapture, true); + + transformer.setScript(script); + transformer.setChild(child); + transformer.init(ddc); + transformer.writeOutput(outputStream); + + return logCapture.toString(); + } catch (IOException e) { + throw new DataDistributorException(e); + } + } + + private void addScripts(String... paths) { + for (String path : paths) { + transformer.addScriptPath(path); + } + } + + private void assertEquivalentJson(String expected, String actual) { + try { + JsonNode expectedNode = new ObjectMapper().readTree(expected); + JsonNode actualNode = new ObjectMapper().readTree(actual); + assertEquals(expectedNode, actualNode); + } catch (IOException e) { + throw new RuntimeException("Failed to compare JSON", e); + } + } + + private void assertNumberOfLines(String s, int expected) { + int actual = s.isEmpty() ? 0 : s.split("[\\n\\r]").length; + assertEquals("number of lines", expected, actual); + } + + /** + * AbstractTextClasss has this method, but is not overloaded to accept a + * String for the logger category. + * + * Capture the log for this class to this Writer. Choose whether or not to + * suppress it from the console. + */ + protected void captureLogOutput(String category, Writer writer, + boolean suppress) { + PatternLayout layout = new PatternLayout("%p %m%n"); + + ConsoleAppender appender = new ConsoleAppender(); + appender.setWriter(writer); + appender.setLayout(layout); + + Logger logger = Logger.getLogger(category); + logger.removeAllAppenders(); + logger.setAdditivity(!suppress); + logger.addAppender(appender); + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + /** + * Just echoes a given string with a given contentType, or throws an + * Exception, if you prefer. + */ + private static class TestDistributor extends AbstractDataDistributor { + private String contentType; + private String outputString; + private boolean throwException; + + public TestDistributor(String contentType, String outputString) { + this(contentType, outputString, false); + } + + public TestDistributor(String contentType, String outputString, + boolean throwException) { + this.contentType = contentType; + this.outputString = outputString; + this.throwException = throwException; + } + + @Override + public String getContentType() throws DataDistributorException { + return contentType; + } + + @Override + public void writeOutput(OutputStream output) + throws DataDistributorException { + try { + if (throwException) { + throw new DataDistributorException("forced exception."); + } + output.write(outputString.getBytes("UTF-8")); + } catch (IOException e) { + throw new RuntimeException(); + } + } + + @Override + public void close() throws DataDistributorException { + // Nothing to do. + } + + } + +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java new file mode 100644 index 0000000000..918a3cc7be --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java @@ -0,0 +1,232 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.file; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; + +import javax.servlet.ServletContext; + +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.ActionFailedException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextImpl; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; +import edu.cornell.mannlib.vitro.webapp.application.VitroHomeDirectory; +import edu.cornell.mannlib.vitro.webapp.audit.AuditModule; +import edu.cornell.mannlib.vitro.webapp.modules.Application; +import edu.cornell.mannlib.vitro.webapp.modules.fileStorage.FileStorage; +import edu.cornell.mannlib.vitro.webapp.modules.imageProcessor.ImageProcessor; +import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngine; +import edu.cornell.mannlib.vitro.webapp.modules.searchIndexer.SearchIndexer; +import edu.cornell.mannlib.vitro.webapp.modules.tboxreasoner.TBoxReasonerModule; +import edu.cornell.mannlib.vitro.webapp.modules.tripleSource.ConfigurationTripleSource; +import edu.cornell.mannlib.vitro.webapp.modules.tripleSource.ContentTripleSource; +import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; + +/** + * Test the basic functionality of FileDistributor. Assume that the + * ConfiguratBeanLoader works as advertised. + */ +public class FileDistributorTest extends AbstractTestClass { + + private static final String SOME_FILE = "some.file"; + private static final String NO_SUCH_FILE = "no.such.file"; + private static final String SOME_DATA = "Some very fine data"; + private static final String SOME_TYPE = "text/plain_type"; + + @Rule + public TemporaryFolder homeFolder = new TemporaryFolder(); + + @Rule + public TemporaryFolder otherFolder = new TemporaryFolder(); + + private File inputFile; + private HttpServletRequestStub req; + private DataDistributorContext ddContext; + private FileDistributor distributor; + private ModelAccessFactoryStub mafs; + + @Before + public void setupData() { + mafs = new ModelAccessFactoryStub(); + setVitroHomeDirectory(homeFolder.getRoot()); + } + + @Before + public void setupContext() { + req = new HttpServletRequestStub(); + ddContext = new DataDistributorContextImpl(req); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void absolutePathSuccess() + throws DataDistributorException, IOException { + createInputFile(otherFolder, SOME_FILE); + createTheDistributor(inputFile.getAbsolutePath(), SOME_TYPE); + assertResults(SOME_TYPE, SOME_DATA); + } + + @Test + public void relativePathSuccess() + throws DataDistributorException, IOException { + createInputFile(homeFolder, SOME_FILE); + createTheDistributor(SOME_FILE, SOME_TYPE); + assertResults(SOME_TYPE, SOME_DATA); + } + + @Test(expected = ActionFailedException.class) + public void noSuchFile_throwsActionFailedException() + throws DataDistributorException, IOException { + createInputFile(homeFolder, SOME_FILE); + createTheDistributor(NO_SUCH_FILE, SOME_TYPE); + assertResults(SOME_TYPE, SOME_DATA); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void setVitroHomeDirectory(File home) { + try { + Field instanceField = ApplicationUtils.class + .getDeclaredField("instance"); + instanceField.setAccessible(true); + ServletContextStub ctx = new ServletContextStub(); + ctx.setRealPath("/WEB-INF/resources/home-files", NO_SUCH_FILE); + instanceField.set(null, new ApplicationStub( + new VitroHomeDirectory(ctx, home.toPath(), null))); + + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private void createInputFile(TemporaryFolder folder, String file) + throws IOException { + inputFile = new File(folder.getRoot(), file); + FileUtils.write(inputFile, SOME_DATA, "UTF-8"); + } + + private void createTheDistributor(String path, String type) + throws DataDistributorException { + distributor = new FileDistributor(); + distributor.setContentType(type); + distributor.setPath(path); + distributor.init(ddContext); + } + + private void assertResults(String type, String data) + throws DataDistributorException { + assertEquals(type, distributor.getContentType()); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + distributor.writeOutput(output); + assertEquals(data, new String(output.toByteArray())); + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + /** + * I would use the usual one, but it doesn't support the VitroHomeDirectory. + */ + private static class ApplicationStub implements Application { + + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private final VitroHomeDirectory vitroHomeDirectory; + + public ApplicationStub(VitroHomeDirectory vitroHomeDirectory) { + this.vitroHomeDirectory = vitroHomeDirectory; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public VitroHomeDirectory getHomeDirectory() { + return this.vitroHomeDirectory; + } + + // ---------------------------------------------------------------------- + // Un-implemented methods + // ---------------------------------------------------------------------- + + @Override + public ServletContext getServletContext() { + throw new RuntimeException("getServletContext() not implemented."); + } + + @Override + public SearchEngine getSearchEngine() { + throw new RuntimeException("getSearchEngine() not implemented."); + } + + @Override + public SearchIndexer getSearchIndexer() { + throw new RuntimeException("getSearchIndexer() not implemented."); + } + + @Override + public ImageProcessor getImageProcessor() { + throw new RuntimeException("getImageProcessor() not implemented."); + } + + @Override + public FileStorage getFileStorage() { + throw new RuntimeException("getFileStorage() not implemented."); + } + + @Override + public ContentTripleSource getContentTripleSource() { + throw new RuntimeException( + "getContentTripleSource() not implemented."); + } + + @Override + public ConfigurationTripleSource getConfigurationTripleSource() { + throw new RuntimeException( + "getConfigurationTripleSource() not implemented."); + } + + @Override + public TBoxReasonerModule getTBoxReasonerModule() { + throw new RuntimeException( + "getTBoxReasonerModule() not implemented."); + } + + @Override + public void shutdown() { + throw new RuntimeException("shutdown() not implemented."); + } + + @Override + public AuditModule getAuditModule() { + // TODO Auto-generated method stub + return null; + } + + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java new file mode 100644 index 0000000000..72aeeae74d --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java @@ -0,0 +1,139 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.file; + +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor.FileFinder; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; + +/** + * TODO + */ +public class SelectingFileDistributor_FileFinderTest extends AbstractTestClass { + private static final Path SCHOLARS_HOME = Paths + .get("//scholars/home/directory"); + + private static final String NAME_NONE = "noValue"; + + private static final String NAME_MULTIPLE = "multiValue"; + private static final String[] VALUE_MULTIPLE = { "first_value", + "second_value" }; + + private static final String NAME_SINGLE = "oneValue"; + private static final String[] VALUE_SINGLE = { + "how much wood would a woodchuck chuck" }; + + private static final Pattern ANY_MATCHER = Pattern.compile("whatever"); + private static final String ANY_TEMPLATE = "/whatever"; + + private static final Pattern MATCH_FAILS = Pattern.compile("no match"); + + private static final Pattern MATCH_ALL = Pattern.compile("would"); + private static final String PATH_TEMPLATE_ONE_SUB = "/users/scholars/\\0"; + private static final File RESULT_ALL = new File("/users/scholars/would"); + + private static final Pattern MATCH_TWO_GROUPS = Pattern + .compile("(how).*(would)"); + private static final String PATH_TEMPLATE_TWO_SUB = "/users/scholars/\\1/\\2"; + private static final File RESULT_TWO_GROUPS = new File( + "/users/scholars/how/would"); + + private static final String PATH_TEMPLATE_MULTI_SUB = "/users/scholars/\\2/\\1/\\2"; + private static final File RESULT_MULTI_SUB = new File( + "/users/scholars/would/how/would"); + + private Map parameters; + private File actual; + + @Before + public void setup() { + parameters = new HashMap<>(); + parameters.put(NAME_SINGLE, VALUE_SINGLE); + parameters.put(NAME_MULTIPLE, VALUE_MULTIPLE); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void noParameterValue_producesWarning_returnsNull() + throws IOException { + failWithWarning(NAME_NONE, ANY_MATCHER, ANY_TEMPLATE, + containsString("No value provided")); + } + + @Test + public void multipleParameterValues_producesWarning_returnsNull() + throws IOException { + failWithWarning(NAME_MULTIPLE, ANY_MATCHER, ANY_TEMPLATE, + containsString("Multiple values")); + } + + @Test + public void noMatch_producesWarning_returnsNull() throws IOException { + failWithWarning(NAME_SINGLE, MATCH_FAILS, ANY_TEMPLATE, + containsString("Failed to parse")); + } + + @Test + public void oneMatchGroup_success() { + assertEquals(RESULT_ALL, + findIt(NAME_SINGLE, MATCH_ALL, PATH_TEMPLATE_ONE_SUB)); + } + + @Test + public void twoMatchGroup_success() { + assertEquals(RESULT_TWO_GROUPS, + findIt(NAME_SINGLE, MATCH_TWO_GROUPS, PATH_TEMPLATE_TWO_SUB)); + } + + @Test + public void twoMatchGroupWithMultipleSubstitutions_success() { + assertEquals(RESULT_MULTI_SUB, + findIt(NAME_SINGLE, MATCH_TWO_GROUPS, PATH_TEMPLATE_MULTI_SUB)); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private File findIt(String parameterName, Pattern parameterParser, + String pathTemplate) { + return new FileFinder(parameters, parameterName, parameterParser, + pathTemplate, SCHOLARS_HOME).find(); + } + + private void failWithWarning(String parameterName, Pattern parameterParser, + String pathTemplate, Matcher warningMatcher) + throws IOException { + try (Writer logCapture = new StringWriter()) { + captureLogOutput(SelectingFileDistributor.class, logCapture, true); + + actual = findIt(parameterName, parameterParser, pathTemplate); + + assertThat(logCapture.toString(), warningMatcher); + assertNull(actual); + } + } + +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java new file mode 100644 index 0000000000..3d0176efda --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java @@ -0,0 +1,289 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.jena.rdf.model.Model; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * Show that this works with an assortment of variable bindings, or with none at + * all. + */ +public class SelectFromContentDistributorTest extends AbstractTestClass { + protected static final String NS = "http://this.name/space#"; + protected static final String BOOK1 = NS + "book1"; + protected static final String BOOK2 = NS + "book2"; + protected static final String AUTHOR1 = NS + "author1"; + protected static final String AUTHOR2 = NS + "author2"; + protected static final String TITLE1 = NS + "title1"; + protected static final String TITLE2 = NS + "title2"; + protected static final String NAME1 = NS + "name1"; + protected static final String NAME2 = NS + "name2"; + protected static final String HAS_AUTHOR = NS + "hasAuthor"; + protected static final String HAS_TITLE = NS + "hasTitle"; + protected static final String HAS_NAME = NS + "hasName"; + protected static final String RAW_QUERY = "" + + "PREFIX ns: \n " // + + "SELECT ?book ?author ?title ?name \n " // + + "WHERE { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?book ns:hasTitle ?title . \n " // + + " ?author ns:hasName ?name . \n " // + + "}"; + + protected DataDistributor distributor; + protected DataDistributorContextStub ddContext; + + @Before + public void setup() { + Model model = model(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + dataProperty(AUTHOR2, HAS_NAME, NAME2), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR2), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + + ddContext = new DataDistributorContextStub(model); + + distributor = new SelectFromContentDistributor(); + ((SelectFromContentDistributor) distributor).setRawQuery(RAW_QUERY); + } + + @After + public void cleanup() throws DataDistributorException { + distributor.close(); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void noBinding() throws DataDistributorException { + runAndAssertResult( + row(uri("book", BOOK1), uri("author", AUTHOR1), + literal("title", TITLE1), literal("name", NAME1)), + row(uri("book", BOOK2), uri("author", AUTHOR1), + literal("title", TITLE2), literal("name", NAME1)), + row(uri("book", BOOK1), uri("author", AUTHOR2), + literal("title", TITLE1), literal("name", NAME2))); + } + + @Test + public void bindAuthorUri() throws DataDistributorException { + bindAsUri("author", AUTHOR1); + runAndAssertResult( + row(uri("book", BOOK1), literal("title", TITLE1), + literal("name", NAME1)), + row(uri("book", BOOK2), literal("title", TITLE2), + literal("name", NAME1))); + } + + @Test + public void bindBookUri() throws DataDistributorException { + bindAsUri("book", BOOK2); + runAndAssertResult(row(uri("author", AUTHOR1), literal("title", TITLE2), + literal("name", NAME1))); + } + + @Test + public void bindAuthorAndBook() throws DataDistributorException { + bindAsUri("author", AUTHOR1); + bindAsUri("book", BOOK1); + runAndAssertResult( + row(literal("title", TITLE1), literal("name", NAME1))); + } + + @Test + public void bindTitleLiteral() throws DataDistributorException { + bindAsLiteral("title", TITLE2); + runAndAssertResult(row(uri("book", BOOK2), uri("author", AUTHOR1), + literal("name", NAME1))); + } + + @Test + public void bindNameLiteral() throws DataDistributorException { + bindAsLiteral("name", NAME1); + runAndAssertResult( + row(uri("book", BOOK1), uri("author", AUTHOR1), + literal("title", TITLE1)), + row(uri("book", BOOK2), uri("author", AUTHOR1), + literal("title", TITLE2))); + } + + @Test + public void bindTitleAndName() throws DataDistributorException { + bindAsLiteral("name", NAME1); + bindAsLiteral("title", TITLE1); + runAndAssertResult(row(uri("book", BOOK1), uri("author", AUTHOR1))); + } + + // ---------------------------------------------------------------------- + // Helper methods and classes + // ---------------------------------------------------------------------- + + private ResultRow row(Binding... bindings) { + return new ResultRow(bindings); + } + + private UriBinding uri(String key, String value) { + return new UriBinding(key, value); + } + + private LiteralBinding literal(String key, String value) { + return new LiteralBinding(key, value); + } + + private void bindAsUri(String name, String uri) { + ((AbstractSparqlBindingDistributor) distributor) + .addUriBindingName(name); + ddContext.setParameter(name, uri); + } + + private void bindAsLiteral(String name, String value) { + ((AbstractSparqlBindingDistributor) distributor) + .addLiteralBindingName(name); + ddContext.setParameter(name, value); + } + + private void runAndAssertResult(ResultRow... rows) + throws DataDistributorException { + distributor.init(ddContext); + assertEqualJsonLists(jsonify(rows), jsonifyOutput(distributor)); + } + + private List jsonify(ResultRow... rows) { + List list = new ArrayList<>(); + for (ResultRow row : rows) { + list.add(row.toJson()); + } + Collections.sort(list, new ObjectNodeComparator()); + return list; + } + + private List jsonifyOutput(DataDistributor dd) + throws DataDistributorException { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + dd.writeOutput(out); + ObjectNode response = (ObjectNode) new ObjectMapper() + .readTree(out.toString()); + ObjectNode results = (ObjectNode) response.get("results"); + ArrayNode bindings = (ArrayNode) results.get("bindings"); + List list = new ArrayList<>(); + for (int i = 0; i < bindings.size(); i++) { + list.add((ObjectNode) bindings.get(i)); + } + Collections.sort(list, new ObjectNodeComparator()); + return list; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void assertEqualJsonLists(List expectedJson, + List actualJson) { + List expectedList = listToSortedStrings(expectedJson); + List actualList = listToSortedStrings(actualJson); + assertEquals(expectedList, actualList); + } + + private List listToSortedStrings(List jsons) { + ArrayList strings = new ArrayList<>(); + for (ObjectNode json : jsons) { + strings.add(json.toString()); + } + return strings; + } + + private static class ResultRow { + private final List bindings; + + public ResultRow(Binding[] bindings) { + this.bindings = new ArrayList<>(Arrays.asList(bindings)); + } + + public ObjectNode toJson() { + ObjectNode json = JsonNodeFactory.instance.objectNode(); + for (Binding binding : bindings) { + json.set(binding.key, binding.toJson()); + } + return json; + } + } + + private static abstract class Binding { + final String key; + final String value; + + public Binding(String key, String value) { + this.key = key; + this.value = value; + } + + public abstract ObjectNode toJson(); + } + + private static class UriBinding extends Binding { + public UriBinding(String key, String value) { + super(key, value); + } + + @Override + public ObjectNode toJson() { + ObjectNode json = JsonNodeFactory.instance.objectNode(); + json.put("type", "uri"); + json.put("value", value); + return json; + } + } + + private static class LiteralBinding extends Binding { + public LiteralBinding(String key, String value) { + super(key, value); + } + + @Override + public ObjectNode toJson() { + ObjectNode json = JsonNodeFactory.instance.objectNode(); + json.put("type", "literal"); + json.put("value", value); + return json; + } + } + + private static class ObjectNodeComparator + implements Comparator { + @Override + public int compare(ObjectNode o1, ObjectNode o2) { + return o1.toString().compareTo(o2.toString()); + } + + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java new file mode 100644 index 0000000000..db0076d165 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java @@ -0,0 +1,44 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; + +import org.junit.Before; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderStub; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * Show that we can do the same binding and queries against a locally built + * graph, as we could against the RDFService from the RequestModelsAccess. + * + * Note that we split the data into two GraphBuilders, to verify that the + * distributor calls both. + */ +public class SelectFromGraphDistributorTest + extends SelectFromContentDistributorTest { + @Override + @Before + public void setup() { + GraphBuilderStub builder1 = new GraphBuilderStub() + .setGraph(model(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + dataProperty(AUTHOR2, HAS_NAME, NAME2))); + + GraphBuilderStub builder2 = new GraphBuilderStub() + .setGraph(model(objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR2), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1))); + + ddContext = new DataDistributorContextStub(model()); + + distributor = new SelectFromGraphDistributor(); + ((SelectFromGraphDistributor) distributor).setRawQuery(RAW_QUERY); + ((SelectFromGraphDistributor) distributor).addGraphBuilder(builder1); + ((SelectFromGraphDistributor) distributor).addGraphBuilder(builder2); + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java new file mode 100644 index 0000000000..0538e3fa1e --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java @@ -0,0 +1,200 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.modelToStrings; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; +import static org.junit.Assert.assertEquals; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.Statement; +import org.junit.Before; +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * TODO + */ +public class ConstructQueryGraphBuilderTest extends AbstractTestClass { + private static final String NS = "http://this.name/space#"; + private static final String BOOK1 = NS + "book1"; + private static final String BOOK2 = NS + "book2"; + private static final String AUTHOR1 = NS + "author1"; + private static final String AUTHOR2 = NS + "author2"; + private static final String TITLE1 = NS + "title1"; + private static final String TITLE2 = NS + "title2"; + private static final String NAME1 = NS + "name1"; + private static final String NAME2 = NS + "name2"; + private static final String HAS_AUTHOR = NS + "hasAuthor"; + private static final String HAS_TITLE = NS + "hasTitle"; + private static final String HAS_NAME = NS + "hasName"; + private static final String RAW_QUERY = "" + + "PREFIX ns: \n " // + + "CONSTRUCT { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?book ns:hasTitle ?title . \n " // + + " ?author ns:hasName ?name . \n " // + + "} WHERE { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?book ns:hasTitle ?title . \n " // + + " ?author ns:hasName ?name . \n " // + + "}"; + private static final String MULTI_QUERY_1 = "" + + "PREFIX ns: \n " // + + "CONSTRUCT { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?book ns:hasTitle ?title . \n " // + + "} WHERE { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?book ns:hasTitle ?title . \n " // + + "}"; + private static final String MULTI_QUERY_2 = "" + + "PREFIX ns: \n " // + + "CONSTRUCT { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?author ns:hasName ?name . \n " // + + "} WHERE { \n " // + + " ?book ns:hasAuthor ?author . \n " // + + " ?author ns:hasName ?name . \n " // + + "}"; + + private Model graph; + private Model expectedResult; + private Model actualResult; + private ConstructQueryGraphBuilder builder; + private DataDistributorContextStub ddContext; + + @Before + public void setup() { + graph = model(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + dataProperty(AUTHOR2, HAS_NAME, NAME2), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR2), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + + ddContext = new DataDistributorContextStub(graph); + + builder = new ConstructQueryGraphBuilder(); + } + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void noBinding() throws DataDistributorException { + setQueries(RAW_QUERY); + runAndAssertResult(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + dataProperty(AUTHOR2, HAS_NAME, NAME2), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR2), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindAuthorUri() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsUri("author", AUTHOR1); + runAndAssertResult(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindBookUri() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsUri("book", BOOK2); + runAndAssertResult(dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindAuthorAndBook() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsUri("author", AUTHOR1); + bindAsUri("book", BOOK1); + runAndAssertResult(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindTitleLiteral() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsLiteral("title", TITLE2); + runAndAssertResult(dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindNameLiteral() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsLiteral("name", NAME1); + runAndAssertResult(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindTitleAndName() throws DataDistributorException { + setQueries(RAW_QUERY); + bindAsLiteral("name", NAME1); + bindAsLiteral("title", TITLE1); + runAndAssertResult(dataProperty(BOOK1, HAS_TITLE, TITLE1), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1)); + } + + @Test + public void bindBookUriWithMultipleQueries() throws DataDistributorException { + setQueries(MULTI_QUERY_1, MULTI_QUERY_2); + bindAsUri("book", BOOK2); + runAndAssertResult(dataProperty(BOOK2, HAS_TITLE, TITLE2), + dataProperty(AUTHOR1, HAS_NAME, NAME1), + objectProperty(BOOK2, HAS_AUTHOR, AUTHOR1)); + } + + // ---------------------------------------------------------------------- + // Helper methods and classes + // ---------------------------------------------------------------------- + + private void setQueries(String... queries) { + for (String query : queries) { + builder.addRawQuery(query); + } + } + + private void bindAsUri(String name, String uri) { + builder.addUriBindingName(name); + ddContext.setParameter(name, uri); + } + + private void bindAsLiteral(String name, String value) { + builder.addLiteralBindingName(name); + ddContext.setParameter(name, value); + } + + private void runAndAssertResult(Statement... statements) + throws DataDistributorException { + expectedResult = model(statements); + actualResult = builder.buildGraph(ddContext); + assertEquals(modelToStrings(expectedResult), + modelToStrings(actualResult)); + } + +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java new file mode 100644 index 0000000000..96a8882d9e --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java @@ -0,0 +1,167 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.modelToStrings; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.jena.rdf.model.Model; +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * TODO + */ +public class DrilldownGraphBuilderTest extends AbstractTestClass { + private static final String IGNORED = "http://ignore/me"; + private static final String SUBJECT_URI = "http://subject/uri"; + private static final String PROPERTY_URI_1 = "http://property/uri_1"; + private static final String PROPERTY_URI_2 = "http://property/uri_2"; + private static final String VALUE_1 = "value_1"; + private static final String VALUE_2 = "value_2"; + private static final String RESPONSE_SUBJECT_1 = "http://responding_1"; + private static final String RESPONSE_SUBJECT_2 = "http://responding_2"; + + private static final Set>> EXPECTED_TOP_LEVEL_MAPS = set( + map(entry("subject", list(SUBJECT_URI)))); + private static final Set>> EXPECTED_CHILD_MAPS = set( + map(entry("subject", list(SUBJECT_URI)), + entry("pred", list(PROPERTY_URI_1)), + entry("obj", list(VALUE_1))), + map(entry("subject", list(SUBJECT_URI)), + entry("pred", list(PROPERTY_URI_2)), + entry("obj", list(VALUE_2)))); + + // Output of all builders, top-level and child + private static final Model EXPECTED_RESULT_GRAPH = model( + dataProperty(SUBJECT_URI, PROPERTY_URI_1, VALUE_1), + dataProperty(SUBJECT_URI, PROPERTY_URI_2, VALUE_2), + dataProperty(IGNORED, PROPERTY_URI_1, VALUE_1), + dataProperty(IGNORED, PROPERTY_URI_2, VALUE_2), + dataProperty(RESPONSE_SUBJECT_1, PROPERTY_URI_1, VALUE_1), + dataProperty(RESPONSE_SUBJECT_1, PROPERTY_URI_2, VALUE_2), + dataProperty(RESPONSE_SUBJECT_2, PROPERTY_URI_1, VALUE_1), + dataProperty(RESPONSE_SUBJECT_2, PROPERTY_URI_2, VALUE_2)); + + @Test + public void omnibusTest() throws DataDistributorException { + GraphBuilderStub topLevelBuilder1 = new GraphBuilderStub().setGraph( // + model( // provide one row for the SELECT results + dataProperty(SUBJECT_URI, PROPERTY_URI_1, VALUE_1), + dataProperty(IGNORED, PROPERTY_URI_1, VALUE_1))); + GraphBuilderStub topLevelBuilder2 = new GraphBuilderStub().setGraph( // + model( // //provide one row for the SELECT results + dataProperty(SUBJECT_URI, PROPERTY_URI_2, VALUE_2), + dataProperty(IGNORED, PROPERTY_URI_2, VALUE_2))); + + GraphBuilderStub childBuilder1 = new AugmentedGraphBuilderStub( + RESPONSE_SUBJECT_1); + GraphBuilderStub childBuilder2 = new AugmentedGraphBuilderStub( + RESPONSE_SUBJECT_2); + + DataDistributorContextStub context = new DataDistributorContextStub( + model()).setParameter("subject", SUBJECT_URI); + + DrillDownGraphBuilder mainBuilder = new DrillDownGraphBuilder(); + + mainBuilder.addTopLevelGraphBuilder(topLevelBuilder1); + mainBuilder.addTopLevelGraphBuilder(topLevelBuilder2); + mainBuilder.addUriBindingName("subject"); + mainBuilder.setDrillDownQuery( + "SELECT ?pred ?obj WHERE { ?subject ?pred ?obj }"); + mainBuilder.addChildGraphBuilder(childBuilder1); + mainBuilder.addChildGraphBuilder(childBuilder2); + + Model actual = mainBuilder.buildGraph(context); + + assertEquals("TOP1 parameter maps", EXPECTED_TOP_LEVEL_MAPS, + set(topLevelBuilder1.getParameterMaps())); + assertEquals("TOP2 parameter maps", EXPECTED_TOP_LEVEL_MAPS, + set(topLevelBuilder2.getParameterMaps())); + + assertEquals("CHILD1 parameter maps", EXPECTED_CHILD_MAPS, + set(childBuilder1.getParameterMaps())); + assertEquals("CHILD2 parameter maps", EXPECTED_CHILD_MAPS, + set(childBuilder2.getParameterMaps())); + + assertEquals(modelToStrings(EXPECTED_RESULT_GRAPH), + modelToStrings(actual)); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + @SafeVarargs + private static List list(String... elements) { + return new ArrayList<>(Arrays.asList(elements)); + } + + private static Map.Entry> entry(String key, + List value) { + Map> map = new HashMap<>(); + map.put(key, value); + return map.entrySet().iterator().next(); + } + + @SafeVarargs + private static Map> map( + Map.Entry>... entries) { + Map> map = new HashMap<>(); + for (Entry> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + + @SafeVarargs + private static Set>> set( + Map>... maps) { + return new HashSet<>(Arrays.asList(maps)); + } + + private Object set(List>> maps) { + return new HashSet<>(maps); + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + /** + * Rather than a pre-recorded response, the response is a model that + * reflects the request parameters. + */ + private static class AugmentedGraphBuilderStub extends GraphBuilderStub { + private final String responseSubject; + + public AugmentedGraphBuilderStub(String responseSubject) { + this.responseSubject = responseSubject; + } + + @Override + public Model buildGraph(DataDistributorContext ddContext) + throws DataDistributorException { + super.buildGraph(ddContext); + return model(dataProperty(responseSubject, + ddContext.getRequestParameters().get("pred")[0], + ddContext.getRequestParameters().get("obj")[0])); + } + + } +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java new file mode 100644 index 0000000000..b9d3761e57 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java @@ -0,0 +1,61 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.arraysToLists; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; + +/** + * TODO + */ +public class GraphBuilderStub implements GraphBuilder { + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private final List>> parameterMaps = new ArrayList<>(); + + private Model graph = ModelFactory.createDefaultModel(); + + public GraphBuilderStub setGraph(Model g) { + this.graph = g; + return this; + } + + public List>> getParameterMaps() { + return parameterMaps; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public Model buildGraph(DataDistributorContext ddContext) + throws DataDistributorException { + parameterMaps.add(arraysToLists(ddContext.getRequestParameters())); + return ModelFactory.createDefaultModel().add(graph); + } + + @Override + public String getBuilderName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setBuilderName(String name) { + // TODO Auto-generated method stub + + } + +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java new file mode 100644 index 0000000000..7ee7510303 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java @@ -0,0 +1,137 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.arraysToLists; +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.listsToArrays; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.Before; +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.EnhancedDataDistributionContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * TODO + */ +public class GraphBuilderUtilitiesTest extends AbstractTestClass { + + private DataDistributorContextStub original; + private EnhancedDataDistributionContext enhanced; + private Map expected; + private Map additions; + + @Before + public void setup() { + original = new DataDistributorContextStub( + ModelFactory.createDefaultModel()); + original.setParameterMap( + map().add("existingKey", "existingValue").toParameterMap()); + enhanced = new EnhancedDataDistributionContext(original); + } + // ---------------------------------------------------------------------- + // The tests: EnhancedDistributionContext + // ---------------------------------------------------------------------- + + @Test + public void createANewParameter() { + expected = map() // + .add("newKey", "newValue1") // + .add("existingKey", "existingValue") // + .toParameterMap(); + + enhanced.addParameterValue("newKey", "newValue1"); + assertExpectedMap(enhanced.getRequestParameters()); + } + + @Test + public void addAValueToAnExistingParameter() { + expected = map() // + .add("existingKey", "existingValue", "newValue1") // + .toParameterMap(); + + enhanced.addParameterValue("existingKey", "newValue1"); + assertExpectedMap(enhanced.getRequestParameters()); + } + + @Test + public void addTwoValuesToAnExistingParameter() { + expected = map() // + .add("existingKey", "existingValue", "newValue1", "newValue2") // + .toParameterMap(); + + enhanced.addParameterValue("existingKey", "newValue1"); + enhanced.addParameterValue("existingKey", "newValue2"); + assertExpectedMap(enhanced.getRequestParameters()); + } + + @Test + public void createNewParameterWithTwoIdenticalValues() { + expected = map() // + .add("newKey", "newValue", "newValue") // + .add("existingKey", "existingValue") // + .toParameterMap(); + + enhanced.addParameterValue("newKey", "newValue"); + enhanced.addParameterValue("newKey", "newValue"); + assertExpectedMap(enhanced.getRequestParameters()); + } + + @Test + public void addMultipleParametersFromAMap() { + expected = map() // + .add("newKey", "newValue") // + .add("existingKey", "existingValue", "anotherValue") // + .toParameterMap(); + + additions = new HashMap() { + { + put("newKey", "newValue"); + put("existingKey", "anotherValue"); + } + }; + enhanced.addParameterValues(additions); + assertExpectedMap(enhanced.getRequestParameters()); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private static ListsMap map() { + return new ListsMap(); + } + + private void assertExpectedMap(Map actual) { + assertEquals(arraysToLists(expected), arraysToLists(actual)); + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + private static class ListsMap { + Map> parameters = new HashMap<>(); + + public ListsMap add(String key, String... values) { + if (!parameters.containsKey(key)) { + parameters.put(key, new ArrayList<>()); + } + parameters.get(key).addAll(Arrays.asList(values)); + return this; + } + + public Map toParameterMap() { + return listsToArrays(parameters); + } + } +} \ No newline at end of file diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java new file mode 100644 index 0000000000..c7b8db990d --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java @@ -0,0 +1,73 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; + +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; + +/** + * Check to see that the subordinate GraphBuilder(s) is getting a suitably + * augmented parameter map. + */ +public class IteratingGraphBuilderTest extends AbstractTestClass { + private static final List>> EXPECTED_PARAMETER_MAPS = list( + map(entry("subjectArea", list("cancer"))), + map(entry("subjectArea", list("tissues"))), + map(entry("subjectArea", list("echidnae")))); + + @Test + public void knockThreeTimes() throws DataDistributorException { + GraphBuilderStub childBuilder = new GraphBuilderStub(); + + IteratingGraphBuilder iteratingBuilder = new IteratingGraphBuilder(); + iteratingBuilder.setParameterName("subjectArea"); + iteratingBuilder.addParameterValue("cancer"); + iteratingBuilder.addParameterValue("tissues"); + iteratingBuilder.addParameterValue("echidnae"); + iteratingBuilder.addChildGraphBuilder(childBuilder); + + iteratingBuilder.buildGraph(new DataDistributorContextStub(model())); + + assertEquals(EXPECTED_PARAMETER_MAPS, childBuilder.getParameterMaps()); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + @SafeVarargs + private static List list(T... elements) { + return new ArrayList<>(Arrays.asList(elements)); + } + + private static Map.Entry> entry(String key, + List value) { + Map> map = new HashMap<>(); + map.put(key, value); + return map.entrySet().iterator().next(); + } + + @SafeVarargs + private static Map> map( + Map.Entry>... entries) { + Map> map = new HashMap<>(); + for (Entry> entry : entries) { + map.put(entry.getKey(), entry.getValue()); + } + return map; + } + +} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java new file mode 100644 index 0000000000..390b902a08 --- /dev/null +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java @@ -0,0 +1,228 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.util; + +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.junit.Test; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.MissingParametersException; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; +import stubs.javax.servlet.http.HttpServletRequestStub; + +/** + * test plan + * + *

    + * null parameters throws NPE
    + * 
    + * null uriBindingNames or literalBindingNames throws NPE
    + * 
    + * null query throws NPE
    + * 
    + * missing parameter throws MissingPE, 
    + * multiple parameter values throws MissingPE
    + * 
    + * show binding of zero/zero, one/one, zero/three, three/zero
    + * 
    + */ + +public class VariableBinderTest extends AbstractTestClass { + private static final Map NO_PARAMETERS = Collections.emptyMap(); + private static final QueryHolder BASIC_QUERY = new QueryHolder("SELECT * WHERE { ?s ?p ?o }"); + + private static final String URI_1_NAME = "uri_1"; + private static final String URI_1_VALUE = "http://test/uri_1"; + private static final String URI_2_NAME = "uri_2"; + private static final String URI_2_VALUE = "http://test/uri_2"; + private static final String URI_3_NAME = "uri_3"; + private static final String URI_3_VALUE = "http://test/uri_3"; + private static final String LITERAL_1_NAME = "literal_1"; + private static final String LITERAL_1_VALUE = "value_1"; + private static final String LITERAL_2_NAME = "literal_2"; + private static final String LITERAL_2_VALUE = "value_2"; + private static final String LITERAL_3_NAME = "literal_3"; + private static final String LITERAL_3_VALUE = "value_3"; + private static final String RAW_ZERO_ZERO = "SELECT * WHERE { ?s ?p ?o }"; + private static final String EXPECTED_ZERO_ZERO = "SELECT * WHERE { ?s ?p ?o }"; + private static final String RAW_ONE_ONE = "SELECT * WHERE { ?%s rdfs:label ?%s }"; + private static final String EXPECTED_ONE_ONE = "SELECT * WHERE { <%s> rdfs:label \"%s\" }"; + private static final String RAW_THREE_ZERO = "SELECT * WHERE { ?%s ?%s ?%s }"; + private static final String EXPECTED_THREE_ZERO = "SELECT * WHERE { <%s> <%s> <%s> }"; + private static final String RAW_ZERO_THREE = "SELECT * WHERE { ?s rdfs:label ?%s, ?%s, ?%s }"; + private static final String EXPECTED_ZERO_THREE = "SELECT * WHERE { ?s rdfs:label \"%s\", \"%s\", \"%s\" }"; + + private VariableBinder binder; + private QueryHolder input; + private QueryHolder expected; + private QueryHolder actual; + + // ---------------------------------------------------------------------- + // The tests + // ---------------------------------------------------------------------- + + @Test + public void nullParameters_throwsException() { + expectException(NullPointerException.class, "parameters"); + binder = new VariableBinder(null); + } + + @Test + public void nullUriBindingNames_throwsException() throws DataDistributorException { + expectException(NullPointerException.class, "uriBindingNames"); + binder = new VariableBinder(NO_PARAMETERS); + binder.bindValuesToQuery(null, set(), BASIC_QUERY); + } + + @Test + public void nullLiteralBindingNames_throwsException() throws DataDistributorException { + expectException(NullPointerException.class, "literalBindingNames"); + binder = new VariableBinder(NO_PARAMETERS); + binder.bindValuesToQuery(set(), null, BASIC_QUERY); + } + + @Test + public void nullQuery_throwsException() throws DataDistributorException { + expectException(NullPointerException.class, "query"); + binder = new VariableBinder(NO_PARAMETERS); + binder.bindValuesToQuery(set(), set(), null); + } + + @Test + public void missingParameterValue_throwsException() throws DataDistributorException { + expectException(MissingParametersException.class, "is required"); + binder = new VariableBinder(NO_PARAMETERS); + binder.bindValuesToQuery(set(URI_1_NAME), set(), BASIC_QUERY); + } + + @Test + public void multipleParameterValues_throwsException() throws DataDistributorException { + expectException(MissingParametersException.class, "multiple values"); + + Map pMap = new HashMap<>(); + pMap.put(LITERAL_3_NAME, new String[] {LITERAL_1_VALUE, LITERAL_3_VALUE}); + + binder = new VariableBinder(pMap); + binder.bindValuesToQuery(set(), set(LITERAL_3_NAME), BASIC_QUERY); + } + + @Test + public void bind_zero_and_zero() throws DataDistributorException { + binder = variableBinder(); + + input = formatQh(RAW_ZERO_ZERO); + expected = formatQh(EXPECTED_ZERO_ZERO); + actual = binder.bindValuesToQuery(set(), set(), input); + + assertBinding(); + } + + @Test + public void bind_one_and_one() throws DataDistributorException { + binder = variableBinder( // + parameter(URI_1_NAME, URI_1_VALUE), + parameter(LITERAL_1_NAME, LITERAL_1_VALUE)); + + input = formatQh(RAW_ONE_ONE, URI_1_NAME, LITERAL_1_NAME); + expected = formatQh(EXPECTED_ONE_ONE, URI_1_VALUE, LITERAL_1_VALUE); + actual = binder.bindValuesToQuery(set(URI_1_NAME), set(LITERAL_1_NAME), + input); + + assertBinding(); + } + + @Test + public void bind_three_and_zero() throws DataDistributorException { + binder = variableBinder( // + parameter(URI_1_NAME, URI_1_VALUE), + parameter(URI_2_NAME, URI_2_VALUE), + parameter(URI_3_NAME, URI_3_VALUE)); + + input = formatQh(RAW_THREE_ZERO, URI_1_NAME, URI_2_NAME, URI_3_NAME); + expected = formatQh(EXPECTED_THREE_ZERO, URI_1_VALUE, URI_2_VALUE, + URI_3_VALUE); + actual = binder.bindValuesToQuery( + set(URI_1_NAME, URI_2_NAME, URI_3_NAME), set(), input); + + assertBinding(); + } + + @Test + public void bind_zero_and_three() throws DataDistributorException { + binder = variableBinder( // + parameter(LITERAL_1_NAME, LITERAL_1_VALUE), + parameter(LITERAL_2_NAME, LITERAL_2_VALUE), + parameter(LITERAL_3_NAME, LITERAL_3_VALUE)); + + input = formatQh(RAW_ZERO_THREE, LITERAL_1_NAME, LITERAL_2_NAME, + LITERAL_3_NAME); + expected = formatQh(EXPECTED_ZERO_THREE, LITERAL_1_VALUE, + LITERAL_2_VALUE, LITERAL_3_VALUE); + actual = binder.bindValuesToQuery(set(), + set(LITERAL_1_NAME, LITERAL_2_NAME, LITERAL_3_NAME), input); + + assertBinding(); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + @SuppressWarnings("unchecked") + private VariableBinder variableBinder(Parm... parms) { + HttpServletRequestStub req = new HttpServletRequestStub(); + for (Parm p : parms) { + req.addParameter(p.name, p.value); + } + return new VariableBinder(req.getParameterMap()); + } + + private Parm parameter(String name, String value) { + return new Parm(name, value); + } + + @SuppressWarnings("unchecked") + private Set set(T... elements) { + return new HashSet<>(Arrays.asList(elements)); + } + + private QueryHolder formatQh(String template, Object... values) { + return new QueryHolder(String.format(template, values)); + } + + private void assertBinding() { + String message = String.format("unexpected binding: \n" // + + " input = '%s' \n" // + + " expected = '%s' \n" // + + " actual = '%s'", input, expected, actual); + // System.out.println(message); + + if (!Objects.equals(expected, actual)) { + fail(message); + } + } + + // ---------------------------------------------------------------------- + // Helper classes + // ---------------------------------------------------------------------- + + private static class Parm { + final String name; + final String value; + + public Parm(String name, String value) { + this.name = name; + this.value = value; + } + } +} diff --git a/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java b/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java new file mode 100644 index 0000000000..e2712359ac --- /dev/null +++ b/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java @@ -0,0 +1,71 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package stubs.edu.cornell.library.scholars.webapp.controller.api.distribute; + +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.*; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jena.rdf.model.Model; + +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccessStub; + +/** + * A minimal implementation. Additional tests may want to set more values into + * the RequestModelAccessStub. + */ +public class DataDistributorContextStub implements DataDistributorContext { + // ---------------------------------------------------------------------- + // Stub infrastructure + // ---------------------------------------------------------------------- + + private final RequestModelAccessStub models = new RequestModelAccessStub(); + private Map parameters = new HashMap<>(); + + public DataDistributorContextStub(Model model) { + models.setRDFService(new RDFServiceModel(model)); + } + + public DataDistributorContextStub setParameterMap(Map map) { + this.parameters = deepCopyParameters(map); + return this; + } + + public DataDistributorContextStub setParameter(String name, String value) { + parameters.put(name, new String[]{value}); + return this; + } + + public DataDistributorContextStub removeParameter(String name) { + parameters.remove(name); + return this; + } + + // ---------------------------------------------------------------------- + // Stub methods + // ---------------------------------------------------------------------- + + @Override + public Map getRequestParameters() { + return parameters; + } + + @Override + public RequestModelAccess getRequestModels() { + return models; + } + + @Override + public boolean isAuthorized(AuthorizationRequest ar) { + return true; + } + + // ---------------------------------------------------------------------- + // Un-implemented methods + // ---------------------------------------------------------------------- + +} From 23b91fbe1d1a96b3289eeb7a858c0cdf843e3e8c Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 22 Jan 2025 14:57:28 +0100 Subject: [PATCH 11/19] Code style fixes for tests --- .../api/DistributeDataApiControllerTest.java | 97 +++++++------------ .../DataDistributorContextTest.java | 12 +-- .../JavaScriptTransformDistributorTest.java | 14 ++- .../distribute/file/FileDistributorTest.java | 14 ++- ...lectingFileDistributor_FileFinderTest.java | 23 ++--- .../rdf/SelectFromContentDistributorTest.java | 10 +- .../rdf/SelectFromGraphDistributorTest.java | 3 +- .../ConstructQueryGraphBuilderTest.java | 7 +- .../DrilldownGraphBuilderTest.java | 5 +- .../rdf/graphbuilder/GraphBuilderStub.java | 7 +- .../GraphBuilderUtilitiesTest.java | 5 +- .../IteratingGraphBuilderTest.java | 3 +- .../rdf/util/VariableBinderTest.java | 15 ++- .../DataDistributorContextStub.java | 15 ++- 14 files changed, 90 insertions(+), 140 deletions(-) diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java index b4e6257de6..1b854d6089 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiControllerTest.java @@ -18,18 +18,17 @@ import javax.servlet.ServletException; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; import org.apache.jena.ontology.OntModel; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; import org.apache.log4j.Level; import org.junit.Before; import org.junit.Test; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; -import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; -import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccessStub; import stubs.javax.servlet.http.HttpServletRequestStub; @@ -41,8 +40,10 @@ public class DistributeDataApiControllerTest extends AbstractTestClass { private static final String DDIST_URI_1 = "http://test/distributor1"; private static final String DDIST_URI_2 = "http://test/distributor2"; - private static final String ACTION_NAME_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName"; - private static final String FAIL_METHOD_PROPERTY = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#failMethod"; + private static final String ACTION_NAME_PROPERTY = + "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#actionName"; + private static final String FAIL_METHOD_PROPERTY = + "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#failMethod"; private HttpServletRequestStub req; private HttpServletResponseStub resp; @@ -63,20 +64,16 @@ public void setup() { // ---------------------------------------------------------------------- @Test - public void emptyAction_400_badRequest() - throws ServletException, IOException { + public void emptyAction_400_badRequest() throws ServletException, IOException { setActionPath(""); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); runIt(400, "'action' path was not provided."); } @Test - public void unrecognizedAction_400_badRequest() - throws ServletException, IOException { + public void unrecognizedAction_400_badRequest() throws ServletException, IOException { setActionPath("unknown"); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); runIt(400, "Did not find a DataDistributor for 'unknown'"); } @@ -86,69 +83,56 @@ public void multipleActions_warning() throws ServletException, IOException { captureLogOutput(DistributeDataApiController.class, w, true); setActionPath("multiples"); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "multiples"), - dd(DDIST_URI_2, DDTestDistributor.class, "multiples")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "multiples"), dd(DDIST_URI_2, + DDTestDistributor.class, "multiples")); runIt(200, "success"); - assertTrue(w.toString() - .contains("more than one DataDistributor for 'multiples'")); + assertTrue(w.toString().contains("more than one DataDistributor for 'multiples'")); } @Test - public void failToInstantiate_500_serverError() - throws ServletException, IOException { + public void failToInstantiate_500_serverError() throws ServletException, IOException { setLoggerLevel(DistributeDataApiController.class, Level.ERROR); setActionPath("simpleSuccess"); - populateDisplayModel( - dd(DDIST_URI_1, TestFailure.class, "simpleSuccess")); + populateDisplayModel(dd(DDIST_URI_1, TestFailure.class, "simpleSuccess")); runIt(500, "Failed to instantiate"); } @Test - public void initThrowsException_500_serverError() - throws ServletException, IOException { + public void initThrowsException_500_serverError() throws ServletException, IOException { setLoggerLevel(DistributeDataApiController.class, Level.OFF); setActionPath("initFails"); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "initFails", "init")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "initFails", "init")); runIt(500, "forced error in init()"); } @Test - public void getContextTypeThrowsException_500_serverError() - throws ServletException, IOException { + public void getContextTypeThrowsException_500_serverError() throws ServletException, IOException { setLoggerLevel(DistributeDataApiController.class, Level.OFF); setActionPath("getContentTypeFails"); - populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, - "getContentTypeFails", "getContentType")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "getContentTypeFails", "getContentType")); runIt(500, "forced error in getContentType()"); } @Test - public void writeOutputThrowsException_500_serverError() - throws ServletException, IOException { + public void writeOutputThrowsException_500_serverError() throws ServletException, IOException { setLoggerLevel(DistributeDataApiController.class, Level.OFF); setActionPath("writeOutputFails"); - populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, - "writeOutputFails", "writeOutput")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "writeOutputFails", "writeOutput")); runIt(500, "forced error in writeOutput()"); } @Test public void simpleSuccess() throws ServletException, IOException { setActionPath("simpleSuccess"); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); runIt(200, "success"); } @Test - public void actionPrefixedBySlash_success() - throws ServletException, IOException { + public void actionPrefixedBySlash_success() throws ServletException, IOException { setActionPath("/simpleSuccess"); - populateDisplayModel( - dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); + populateDisplayModel(dd(DDIST_URI_1, DDTestDistributor.class, "simpleSuccess")); runIt(200, "success"); } @@ -168,10 +152,8 @@ private void populateDisplayModel(Model... models) { requestModels.setOntModel(om, ModelNames.DISPLAY); } - private Model dd(String uri, Class class1, String actionName, - String... failMethods) { - Model m = model(typeStatement(uri, toJavaUri(class1)), - typeStatement(uri, toJavaUri(DataDistributor.class)), + private Model dd(String uri, Class class1, String actionName, String... failMethods) { + Model m = model(typeStatement(uri, toJavaUri(class1)), typeStatement(uri, toJavaUri(DataDistributor.class)), dataProperty(uri, ACTION_NAME_PROPERTY, actionName)); for (String failMethod : failMethods) { @@ -181,14 +163,12 @@ private Model dd(String uri, Class class1, String actionName, return m; } - private void runIt(int expectedStatus, String expectedOutput) - throws ServletException, IOException { + private void runIt(int expectedStatus, String expectedOutput) throws ServletException, IOException { controller.doGet(req, resp); assertEquals(expectedStatus, resp.getStatus()); String actualOutput = resp.getOutput(); if (!actualOutput.contains(expectedOutput)) { - fail("expect output to contain>" + expectedOutput + "" - + actualOutput + "<"); + fail("expect output to contain>" + expectedOutput + "" + actualOutput + "<"); } } @@ -199,8 +179,7 @@ private void runIt(int expectedStatus, String expectedOutput) public static class DDTestDistributor implements DataDistributor { private Set failureMethods = new HashSet<>(); - @SuppressWarnings("unused") - @Property(uri = ACTION_NAME_PROPERTY) + @SuppressWarnings("unused") @Property(uri = ACTION_NAME_PROPERTY) public void setActionName(String actionName) { // Nothing to do } @@ -211,8 +190,7 @@ public void addFailureMethod(String methodName) { } @Override - public void init(DataDistributorContext ddContext) - throws DataDistributorException { + public void init(DataDistributorContext ddContext) throws DataDistributorException { if (failureMethods.contains("init")) { throw new DataDistributorException("forced error in init()"); } @@ -221,18 +199,15 @@ public void init(DataDistributorContext ddContext) @Override public String getContentType() throws DataDistributorException { if (failureMethods.contains("getContentType")) { - throw new DataDistributorException( - "forced error in getContentType()"); + throw new DataDistributorException("forced error in getContentType()"); } return "text/plain"; } @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { + public void writeOutput(OutputStream output) throws DataDistributorException { if (failureMethods.contains("writeOutput")) { - throw new DataDistributorException( - "forced error in writeOutput()"); + throw new DataDistributorException("forced error in writeOutput()"); } try { output.write("success".getBytes()); diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java index 6feaed3790..7f3f7f972a 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextTest.java @@ -13,9 +13,8 @@ import java.util.List; import java.util.Map; -import org.junit.Test; - import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import org.junit.Test; /** * TODO @@ -38,8 +37,7 @@ private static Map> initLists() { HashMap> map = new HashMap<>(); map.put("nameZero", new ArrayList<>()); map.put("nameOne", new ArrayList<>(Arrays.asList("valueOne"))); - map.put("nameTwo", - new ArrayList<>(Arrays.asList("valueTwoA", "valueTwoB"))); + map.put("nameTwo", new ArrayList<>(Arrays.asList("valueTwoA", "valueTwoB"))); return map; } @@ -63,12 +61,10 @@ public void deepCopy() { } @SuppressWarnings("deprecation") - private void assertEqualArrayMaps(Map a1, - Map a2) { + private void assertEqualArrayMaps(Map a1, Map a2) { assertEquals("key sets should be equals", a1.keySet(), a2.keySet()); for (String key : a1.keySet()) { - assertEquals("array values for '" + key + "'", a1.get(key), - a2.get(key)); + assertEquals("array values for '" + key + "'", a1.get(key), a2.get(key)); } } } diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java index ca7bf69a51..64a2c4618b 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java @@ -13,20 +13,18 @@ import javax.script.ScriptException; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.junit.Before; -import org.junit.Test; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.junit.Before; +import org.junit.Test; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; import stubs.edu.cornell.mannlib.vitro.webapp.modules.ApplicationStub; import stubs.javax.servlet.ServletContextStub; diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java index 918a3cc7be..1d574fec12 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java @@ -11,12 +11,6 @@ import javax.servlet.ServletContext; -import org.apache.commons.io.FileUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.ActionFailedException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; @@ -33,6 +27,11 @@ import edu.cornell.mannlib.vitro.webapp.modules.tboxreasoner.TBoxReasonerModule; import edu.cornell.mannlib.vitro.webapp.modules.tripleSource.ConfigurationTripleSource; import edu.cornell.mannlib.vitro.webapp.modules.tripleSource.ContentTripleSource; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; import stubs.javax.servlet.ServletContextStub; import stubs.javax.servlet.http.HttpServletRequestStub; @@ -99,7 +98,7 @@ public void noSuchFile_throwsActionFailedException() createTheDistributor(NO_SUCH_FILE, SOME_TYPE); assertResults(SOME_TYPE, SOME_DATA); } - + // ---------------------------------------------------------------------- // Helper methods // ---------------------------------------------------------------------- @@ -113,7 +112,6 @@ private void setVitroHomeDirectory(File home) { ctx.setRealPath("/WEB-INF/resources/home-files", NO_SUCH_FILE); instanceField.set(null, new ApplicationStub( new VitroHomeDirectory(ctx, home.toPath(), null))); - } catch (Exception e) { throw new IllegalStateException(e); } diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java index 72aeeae74d..c3b6928087 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; @@ -18,29 +17,26 @@ import java.util.Map; import java.util.regex.Pattern; +import edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor.FileFinder; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; -import edu.cornell.library.scholars.webapp.controller.api.distribute.file.SelectingFileDistributor.FileFinder; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; - /** * TODO */ public class SelectingFileDistributor_FileFinderTest extends AbstractTestClass { private static final Path SCHOLARS_HOME = Paths .get("//scholars/home/directory"); - + private static final String NAME_NONE = "noValue"; private static final String NAME_MULTIPLE = "multiValue"; - private static final String[] VALUE_MULTIPLE = { "first_value", - "second_value" }; + private static final String[] VALUE_MULTIPLE = { "first_value", "second_value" }; private static final String NAME_SINGLE = "oneValue"; - private static final String[] VALUE_SINGLE = { - "how much wood would a woodchuck chuck" }; + private static final String[] VALUE_SINGLE = { "how much wood would a woodchuck chuck" }; private static final Pattern ANY_MATCHER = Pattern.compile("whatever"); private static final String ANY_TEMPLATE = "/whatever"; @@ -51,15 +47,12 @@ public class SelectingFileDistributor_FileFinderTest extends AbstractTestClass { private static final String PATH_TEMPLATE_ONE_SUB = "/users/scholars/\\0"; private static final File RESULT_ALL = new File("/users/scholars/would"); - private static final Pattern MATCH_TWO_GROUPS = Pattern - .compile("(how).*(would)"); + private static final Pattern MATCH_TWO_GROUPS = Pattern.compile("(how).*(would)"); private static final String PATH_TEMPLATE_TWO_SUB = "/users/scholars/\\1/\\2"; - private static final File RESULT_TWO_GROUPS = new File( - "/users/scholars/how/would"); + private static final File RESULT_TWO_GROUPS = new File("/users/scholars/how/would"); private static final String PATH_TEMPLATE_MULTI_SUB = "/users/scholars/\\2/\\1/\\2"; - private static final File RESULT_MULTI_SUB = new File( - "/users/scholars/would/how/would"); + private static final File RESULT_MULTI_SUB = new File("/users/scholars/would/how/would"); private Map parameters; private File actual; diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java index 3d0176efda..a8547f36bc 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromContentDistributorTest.java @@ -15,19 +15,17 @@ import java.util.Comparator; import java.util.List; -import org.apache.jena.rdf.model.Model; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import org.apache.jena.rdf.model.Model; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java index db0076d165..654ff2cb8c 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/SelectFromGraphDistributorTest.java @@ -6,9 +6,8 @@ import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; -import org.junit.Before; - import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderStub; +import org.junit.Before; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java index 0538e3fa1e..b527f06f7c 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ConstructQueryGraphBuilderTest.java @@ -8,13 +8,12 @@ import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; import static org.junit.Assert.assertEquals; +import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Statement; import org.junit.Before; import org.junit.Test; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** @@ -159,7 +158,7 @@ public void bindTitleAndName() throws DataDistributorException { dataProperty(AUTHOR1, HAS_NAME, NAME1), objectProperty(BOOK1, HAS_AUTHOR, AUTHOR1)); } - + @Test public void bindBookUriWithMultipleQueries() throws DataDistributorException { setQueries(MULTI_QUERY_1, MULTI_QUERY_2); diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java index 96a8882d9e..36eaa55f48 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/DrilldownGraphBuilderTest.java @@ -16,12 +16,11 @@ import java.util.Map.Entry; import java.util.Set; -import org.apache.jena.rdf.model.Model; -import org.junit.Test; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import org.apache.jena.rdf.model.Model; +import org.junit.Test; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java index b9d3761e57..af778dbda8 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderStub.java @@ -8,11 +8,10 @@ import java.util.List; import java.util.Map; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; /** * TODO @@ -55,7 +54,7 @@ public String getBuilderName() { @Override public void setBuilderName(String name) { // TODO Auto-generated method stub - + } } diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java index 7ee7510303..31fa496d45 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/GraphBuilderUtilitiesTest.java @@ -12,12 +12,11 @@ import java.util.List; import java.util.Map; +import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.EnhancedDataDistributionContext; +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import org.apache.jena.rdf.model.ModelFactory; import org.junit.Before; import org.junit.Test; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.GraphBuilderUtilities.EnhancedDataDistributionContext; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java index c7b8db990d..1ca14a1153 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/IteratingGraphBuilderTest.java @@ -12,10 +12,9 @@ import java.util.Map; import java.util.Map.Entry; -import org.junit.Test; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import org.junit.Test; import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; /** diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java index 390b902a08..0d51fe59b9 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/util/VariableBinderTest.java @@ -12,12 +12,11 @@ import java.util.Objects; import java.util.Set; -import org.junit.Test; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.MissingParametersException; import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import edu.cornell.mannlib.vitro.webapp.utils.sparqlrunner.QueryHolder; +import org.junit.Test; import stubs.javax.servlet.http.HttpServletRequestStub; /** @@ -30,7 +29,7 @@ * * null query throws NPE * - * missing parameter throws MissingPE, + * missing parameter throws MissingPE, * multiple parameter values throws MissingPE * * show binding of zero/zero, one/one, zero/three, three/zero @@ -40,7 +39,7 @@ public class VariableBinderTest extends AbstractTestClass { private static final Map NO_PARAMETERS = Collections.emptyMap(); private static final QueryHolder BASIC_QUERY = new QueryHolder("SELECT * WHERE { ?s ?p ?o }"); - + private static final String URI_1_NAME = "uri_1"; private static final String URI_1_VALUE = "http://test/uri_1"; private static final String URI_2_NAME = "uri_2"; @@ -70,7 +69,7 @@ public class VariableBinderTest extends AbstractTestClass { // ---------------------------------------------------------------------- // The tests // ---------------------------------------------------------------------- - + @Test public void nullParameters_throwsException() { expectException(NullPointerException.class, "parameters"); @@ -97,7 +96,7 @@ public void nullQuery_throwsException() throws DataDistributorException { binder = new VariableBinder(NO_PARAMETERS); binder.bindValuesToQuery(set(), set(), null); } - + @Test public void missingParameterValue_throwsException() throws DataDistributorException { expectException(MissingParametersException.class, "is required"); @@ -108,10 +107,10 @@ public void missingParameterValue_throwsException() throws DataDistributorExcept @Test public void multipleParameterValues_throwsException() throws DataDistributorException { expectException(MissingParametersException.class, "multiple values"); - + Map pMap = new HashMap<>(); pMap.put(LITERAL_3_NAME, new String[] {LITERAL_1_VALUE, LITERAL_3_VALUE}); - + binder = new VariableBinder(pMap); binder.bindValuesToQuery(set(), set(LITERAL_3_NAME), BASIC_QUERY); } diff --git a/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java b/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java index e2712359ac..8646faed1e 100644 --- a/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java +++ b/api/src/test/java/stubs/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributorContextStub.java @@ -2,24 +2,23 @@ package stubs.edu.cornell.library.scholars.webapp.controller.api.distribute; -import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.*; +import static edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext.deepCopyParameters; + import java.util.HashMap; import java.util.Map; -import org.apache.jena.rdf.model.Model; - import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.AuthorizationRequest; import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import org.apache.jena.rdf.model.Model; import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccessStub; /** - * A minimal implementation. Additional tests may want to set more values into - * the RequestModelAccessStub. + * A minimal implementation. Additional tests may want to set more values into the RequestModelAccessStub. */ public class DataDistributorContextStub implements DataDistributorContext { - // ---------------------------------------------------------------------- + // ---------------------------------------------------------------------- // Stub infrastructure // ---------------------------------------------------------------------- @@ -34,9 +33,9 @@ public DataDistributorContextStub setParameterMap(Map map) { this.parameters = deepCopyParameters(map); return this; } - + public DataDistributorContextStub setParameter(String name, String value) { - parameters.put(name, new String[]{value}); + parameters.put(name, new String[] { value }); return this; } From 1abc88be88d9170cd2ba2f793fec9fc11a45e773 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 29 Jan 2025 13:54:29 +0100 Subject: [PATCH 12/19] Removed JavaScriptTransformDistributor and HelloDistributor --- .../JavaScriptTransformDistributor.java | 218 --------- .../distribute/examples/HelloDistributor.java | 73 --- .../JavaScriptTransformDistributorTest.java | 422 ------------------ 3 files changed, 713 deletions(-) delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java delete mode 100644 api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java deleted file mode 100644 index 5249681c7c..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributor.java +++ /dev/null @@ -1,218 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -import javax.script.Invocable; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import javax.servlet.ServletContext; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.webapp.application.ApplicationUtils; -import edu.cornell.mannlib.vitro.webapp.utils.configuration.Property; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * Wrap a data distributor with a JavaScript function that will transform its - * output. - * - *

    - * - * The child distributor might produce any arbitrary output. The JavaScript - * function must be written to accept that output as a String and return a - * String containing the transformed output. - * - *

    - * - * For example, the function might replace all occurences of a namespace with a - * different namespace, like this: - * - *

    - * function transform(rawData) {
    - *     return rawData.split("http://first/").join("http://second/");
    - * }
    - * 
    - * - * The JavaScript method must be named 'transform', must accept a String as - * argument, and must return a String as result. - * - *

    - * - * The JavaScript execution environment will include a global variable named - * 'logger'. This is a binding of an org.apache.commons.logging.Log object, and - * can be used to write to the VIVO log file. - * - *

    - * - * Note: this decorator is only scalable to a limited extent, since the - * JavaScript function works with Strings instead of Streams. - */ -public class JavaScriptTransformDistributor extends AbstractDataDistributor { - private static final Log log = LogFactory - .getLog(JavaScriptTransformDistributor.class); - - /** The content type to attach to the file. */ - private String contentType; - private String script; - private DataDistributor child; - private List supportingScriptPaths = new ArrayList<>(); - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#contentType", minOccurs = 1, maxOccurs = 1) - public void setContentType(String cType) { - contentType = cType; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#script", minOccurs = 1, maxOccurs = 1) - public void setScript(String scriptIn) { - script = scriptIn; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#child", minOccurs = 1, maxOccurs = 1) - public void setChild(DataDistributor c) { - child = c; - } - - @Property(uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#supportingScript") - public void addScriptPath(String path) { - supportingScriptPaths.add(path); - } - - @Override - public String getContentType() throws DataDistributorException { - return contentType; - } - - @Override - public void init(DataDistributorContext ddc) - throws DataDistributorException { - super.init(ddc); - child.init(ddc); - } - - /** - */ - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - ScriptEngine engine = createScriptEngine(); - addLoggerToEngine(engine); - loadSupportingScripts(engine); - loadMainScript(engine); - - writeTransformedOutput(output, - runTransformFunction(engine, runChildDistributor())); - } - - private ScriptEngine createScriptEngine() { - return new ScriptEngineManager().getEngineByName("nashorn"); - } - - private void addLoggerToEngine(ScriptEngine engine) { - String loggerName = this.getClass().getName() + "." + actionName; - Log jsLogger = LogFactory.getLog(loggerName); - engine.put("logger", jsLogger); - } - - private void loadSupportingScripts(ScriptEngine engine) - throws DataDistributorException { - log.debug("loading supporting scripts"); - for (String path : supportingScriptPaths) { - loadSupportingScript(engine, path); - log.debug("loaded supporting script: " + path); - } - } - - private void loadSupportingScript(ScriptEngine engine, String path) - throws DataDistributorException { - ServletContext ctx = ApplicationUtils.instance().getServletContext(); - - InputStream resource = ctx.getResourceAsStream(path); - if (resource == null) { - throw new DataDistributorException( - "Can't locate script resource for '" + path + "'"); - } - - try { - engine.eval(new InputStreamReader(resource)); - } catch (ScriptException e) { - throw new DataDistributorException( - "Script at '" + path + "' contains syntax errors.", e); - } - } - - private void loadMainScript(ScriptEngine engine) - throws DataDistributorException { - try { - engine.eval(script); - } catch (ScriptException e) { - throw new DataDistributorException("Script contains syntax errors.", - e); - } - } - - private String runChildDistributor() throws DataDistributorException { - ByteArrayOutputStream childOut = new ByteArrayOutputStream(); - try { - child.writeOutput(childOut); - log.debug("ran child distributor"); - } catch (Exception e) { - throw new DataDistributorException( - "Child distributor threw an exception", e); - } - try { - return childOut.toString("UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("What? No UTF-8 Charset?", e); - } - } - - private String runTransformFunction(ScriptEngine engine, String childOutput) - throws DataDistributorException { - try { - Invocable invocable = (Invocable) engine; - Object result = invocable.invokeFunction("transform", childOutput); - log.debug("ran transform function"); - - if (result instanceof String) { - return (String) result; - } else { - throw new ActionFailedException( - "transform function must return a String"); - } - } catch (NoSuchMethodException e) { - throw new DataDistributorException( - "Script must have a transform() function.", e); - } catch (ScriptException e) { - throw new DataDistributorException("Script contains syntax errors.", - e); - } - } - - private void writeTransformedOutput(OutputStream output, String transformed) - throws DataDistributorException { - try { - output.write(transformed.getBytes("UTF-8")); - } catch (IOException e) { - throw new DataDistributorException(e); - } - } - - @Override - public void close() throws DataDistributorException { - child.close(); - } - -} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java deleted file mode 100644 index 454cd82a13..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/examples/HelloDistributor.java +++ /dev/null @@ -1,73 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.examples; - -import java.io.IOException; -import java.io.OutputStream; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; - -/** - * A simple example of a data distributor. It sends a greeting. - */ -public class HelloDistributor extends AbstractDataDistributor { - - private static final Object NAME_PARAMETER_KEY = "name"; - - /** - * The instance is created to service one HTTP request, and init() is - * called. - * - * The DataDistributorContext provides access to the request parameters, and - * the triple-store connections. - */ - @Override - public void init(DataDistributorContext ddc) - throws DataDistributorException { - super.init(ddc); - } - - /** - * For this distributor, the browser should treat the output as simple text. - */ - @Override - public String getContentType() throws DataDistributorException { - return "text/plain"; - } - - /** - * The text written to the OutputStream will become the body of the HTTP - * response. - * - * This will only be called once for a given instance. - */ - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - try { - if (parameters.containsKey(NAME_PARAMETER_KEY)) { - output.write(String - .format("Hello, %s!", - parameters.get(NAME_PARAMETER_KEY)[0]) - .getBytes()); - } else { - output.write("Hello, World!".getBytes()); - } - } catch (IOException e) { - throw new ActionFailedException(e); - } - } - - /** - * Release any resources. In this case, none. - * - * Garbage collection is uncertain. On the other hand, you can be confident - * that this will be called in a timely manner. - */ - @Override - public void close() throws DataDistributorException { - // Nothing to do. - } - -} diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java deleted file mode 100644 index 64a2c4618b..0000000000 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/decorator/JavaScriptTransformDistributorTest.java +++ /dev/null @@ -1,422 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.decorator; - -import static org.junit.Assert.assertEquals; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; - -import javax.script.ScriptException; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.cornell.library.scholars.webapp.controller.api.distribute.AbstractDataDistributor; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.junit.Before; -import org.junit.Test; -import stubs.edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContextStub; -import stubs.edu.cornell.mannlib.vitro.webapp.modules.ApplicationStub; -import stubs.javax.servlet.ServletContextStub; - -/** - * Test the basic functions of JavaScriptTransformDistributor. - * - * The simple transform just returns a hard-coded String. - * - * The full script accepts a JSON string, parses it, substitutes a value, and - * returns the stringified result. - * - * The multi-script requires two additional scripts in order to assemble a - * hard-coded String. - */ -public class JavaScriptTransformDistributorTest extends AbstractTestClass { - private static final String ACTION_NAME = "tester"; - private static final String JAVASCRIPT_TYPE = "text/javascript"; - private static final String TEXT_TYPE = "text/plain"; - - private static final String BAD_SYNTAX_SCRIPT = "" // - + "function transform( {}"; - - private static final String WRONG_FUNCTION_SCRIPT = "" // - + "function notTransform() { \n" // - + " return 'true'; \n" // - + "}"; - - private static final String WRONG_RETURN_TYPE_SCRIPT = "" // - + "function transform() { \n" // - + " return 3; \n" // - + "}"; - - private static final String SIMPLE_SCRIPT = "" // - + "function transform() { \n" // - + " return 'true'; \n" // - + "}"; - - private static final String SIMPLE_EXPECTED_RESULT = "true"; - - private static final String ECHO_SCRIPT = "" // - + "function transform(data) { \n" // - + " return data; \n" // - + "}"; - - private static final String UNICODE_STRING = "Lévesque"; - - private static final String FULL_SCRIPT = "" // - + "function transform(data) { \n" // - + " var initial = JSON.parse(data); \n" // - + " var result = {}; \n" // - + " Object.keys(initial).forEach(populate); \n" // - + " return JSON.stringify(result); \n" // - + "\n" // - + " function populate(key) { \n" // - + " result[key] = (initial[key] == 'initial') ? \n" // - + " 'transformed' : initial[key]; \n" // - + " } \n" // - + "}"; - - private static final String TRANSFORMED_STRUCTURE = "{ 'a': 'transformed', 'b': 'constant' }" - .replace('\'', '\"'); - - private static final String INITIAL_STRUCTURE = "{ 'a': 'initial', 'b': 'constant' }" - .replace('\'', '"'); - - private static final String PATH_TO_MISSING_SCRIPT = "/no/script/here"; - private static final String PATH_TO_BAD_SYNTAX_SCRIPT = "/bad/syntax/script.js"; - - private static final String PATH_TO_SUPPORTING_SCRIPT_1 = "/support/script1.js"; - private static final String PATH_TO_SUPPORTING_SCRIPT_2 = "/support/script2.js"; - - private static final String SUPPORTING_SCRIPT_1 = "" // - + "function one() { \n" // - + " return '1'; \n" // - + "}"; - private static final String SUPPORTING_SCRIPT_2 = "" // - + "function two() { \n" // - + " return '2'; \n" // - + "}"; - private static final String SUPPORTED_SCRIPT = "" // - + "function transform() { \n" // - + " return one() + ' ' + two() + ' 3'; \n" // - + "}"; - private static final String SUPPORTED_EXPECTED_RESULT = "1 2 3"; - - private static final String LOGGING_SCRIPT = "" // - + "function transform() { \n" // - + " logger.debug('debug message'); \n" // - + " logger.info('info message'); \n" // - + " logger.warn('warn message'); \n" // - + " logger.error('error message'); \n" // - + " return ''; \n" // - + "}"; - private static final String LOGGER_NAME = JavaScriptTransformDistributor.class - .getName() + "." + ACTION_NAME; - - public JavaScriptTransformDistributor transformer; - public TestDistributor child; - public DataDistributorContext ddc; - public ByteArrayOutputStream outputStream; - public String logOutput; - - @Before - public void setup() { - ServletContextStub ctx = new ServletContextStub(); - ctx.setMockResource(PATH_TO_BAD_SYNTAX_SCRIPT, BAD_SYNTAX_SCRIPT); - ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_1, SUPPORTING_SCRIPT_1); - ctx.setMockResource(PATH_TO_SUPPORTING_SCRIPT_2, SUPPORTING_SCRIPT_2); - ApplicationStub.setup(ctx, null); - - ddc = new DataDistributorContextStub(null); - outputStream = new ByteArrayOutputStream(); - - transformer = new JavaScriptTransformDistributor(); - transformer.setContentType(JAVASCRIPT_TYPE); - transformer.setActionName(ACTION_NAME); - } - - // ---------------------------------------------------------------------- - // Basic tests - // ---------------------------------------------------------------------- - - @Test - public void scriptSuntaxError_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "syntax", - ScriptException.class, "but found"); - transformAndCheck("", BAD_SYNTAX_SCRIPT, ""); - } - - @Test - public void noTransformFunction_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "must have", - NoSuchMethodException.class, "transform"); - transformAndCheck("", WRONG_FUNCTION_SCRIPT, ""); - } - - @Test - public void childThrowsException_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "Child", - DataDistributorException.class, "forced"); - child = new TestDistributor(JAVASCRIPT_TYPE, "", true); - transformAndCheck(SIMPLE_SCRIPT, ""); - } - - @Test - public void wrongReturnType_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "must return a String"); - transformAndCheck("", WRONG_RETURN_TYPE_SCRIPT, ""); - } - - @Test - public void mostBasicTransform() throws DataDistributorException { - transformAndCheck("", SIMPLE_SCRIPT, SIMPLE_EXPECTED_RESULT); - } - - @Test - public void parseTransformAndStringify() throws DataDistributorException { - transformAndCheck(INITIAL_STRUCTURE, FULL_SCRIPT, - TRANSFORMED_STRUCTURE); - } - - /** - * This test is intended to check whether Unicode is handled properly - * regardless of the system's default file encoding. - * - * However there might be failures that only show up if the default value of - * the system property file.encoding is not UTF-8. - * - * The commented code is a hacky way of ensuring that the file encoding is - * not UTF-8, but in Java 9 or later, it will cause warning messages. - * - * So we have a test that might pass in some environments and fail in - * others. - */ - @Test - public void unicodeCharactersArePreserved() - throws DataDistributorException, UnsupportedEncodingException { - // try { - // System.setProperty("file.encoding", "ANSI_X3.4-1968"); - // Field charset = Charset.class.getDeclaredField("defaultCharset"); - // charset.setAccessible(true); - // charset.set(null, null); - // } catch (Exception e) { - // throw new RuntimeException(e); - // } - - child = new TestDistributor(TEXT_TYPE, UNICODE_STRING); - runTransformer(ECHO_SCRIPT); - assertEquals(UNICODE_STRING, - new String(outputStream.toByteArray(), "UTF-8")); - } - - // ---------------------------------------------------------------------- - // Tests with additional scripts - // ---------------------------------------------------------------------- - - @Test - public void additionalScriptNotFound_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, "Can't locate"); - addScripts(PATH_TO_MISSING_SCRIPT); - transformAndCheck("", SIMPLE_SCRIPT, ""); - } - - @Test - public void additionalScriptSyntaxError_throwsException() - throws DataDistributorException { - expectException(DataDistributorException.class, - PATH_TO_BAD_SYNTAX_SCRIPT, ScriptException.class, "but found"); - addScripts(PATH_TO_BAD_SYNTAX_SCRIPT); - transformAndCheck("", SIMPLE_SCRIPT, ""); - } - - @Test - public void useAdditionalScripts() throws DataDistributorException { - addScripts(PATH_TO_SUPPORTING_SCRIPT_1, PATH_TO_SUPPORTING_SCRIPT_2); - transformAndCheck("", SUPPORTED_SCRIPT, SUPPORTED_EXPECTED_RESULT); - } - - // ---------------------------------------------------------------------- - // Tests with logging - // ---------------------------------------------------------------------- - - @Test - public void loggingAtDebug_producedFourMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.DEBUG, 4); - } - - @Test - public void loggingAtInfo_producesThreeMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.INFO, 3); - } - - @Test - public void loggingAtWarn_producesTwoMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.WARN, 2); - } - - @Test - public void loggingAtError_producesOneMessage() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.ERROR, 1); - } - - @Test - public void loggingAtOff_producesNoMessages() - throws DataDistributorException { - transformAndCountLogLines("", LOGGING_SCRIPT, Level.OFF, 0); - } - - // ---------------------------------------------------------------------- - // Helper methods - // ---------------------------------------------------------------------- - - private void transformAndCheck(String initial, String script, - String expected) throws DataDistributorException { - child = new TestDistributor(JAVASCRIPT_TYPE, initial); - transformAndCheck(script, expected); - } - - private void transformAndCheck(String script, String expected) - throws DataDistributorException { - runTransformer(script); - assertEquivalentJson(expected, new String(outputStream.toByteArray())); - } - - private void transformAndCountLogLines(String initial, String script, - Level level, int expectedCount) throws DataDistributorException { - setLoggerLevel(LOGGER_NAME, level); - logOutput = runTransformer(initial, script); - assertNumberOfLines(logOutput, expectedCount); - } - - private String runTransformer(String initial, String script) - throws DataDistributorException { - child = new TestDistributor(JAVASCRIPT_TYPE, initial); - return runTransformer(script); - } - - private String runTransformer(String script) - throws DataDistributorException { - try (Writer logCapture = new StringWriter()) { - captureLogOutput(LOGGER_NAME, logCapture, true); - - transformer.setScript(script); - transformer.setChild(child); - transformer.init(ddc); - transformer.writeOutput(outputStream); - - return logCapture.toString(); - } catch (IOException e) { - throw new DataDistributorException(e); - } - } - - private void addScripts(String... paths) { - for (String path : paths) { - transformer.addScriptPath(path); - } - } - - private void assertEquivalentJson(String expected, String actual) { - try { - JsonNode expectedNode = new ObjectMapper().readTree(expected); - JsonNode actualNode = new ObjectMapper().readTree(actual); - assertEquals(expectedNode, actualNode); - } catch (IOException e) { - throw new RuntimeException("Failed to compare JSON", e); - } - } - - private void assertNumberOfLines(String s, int expected) { - int actual = s.isEmpty() ? 0 : s.split("[\\n\\r]").length; - assertEquals("number of lines", expected, actual); - } - - /** - * AbstractTextClasss has this method, but is not overloaded to accept a - * String for the logger category. - * - * Capture the log for this class to this Writer. Choose whether or not to - * suppress it from the console. - */ - protected void captureLogOutput(String category, Writer writer, - boolean suppress) { - PatternLayout layout = new PatternLayout("%p %m%n"); - - ConsoleAppender appender = new ConsoleAppender(); - appender.setWriter(writer); - appender.setLayout(layout); - - Logger logger = Logger.getLogger(category); - logger.removeAllAppenders(); - logger.setAdditivity(!suppress); - logger.addAppender(appender); - } - - // ---------------------------------------------------------------------- - // Helper classes - // ---------------------------------------------------------------------- - - /** - * Just echoes a given string with a given contentType, or throws an - * Exception, if you prefer. - */ - private static class TestDistributor extends AbstractDataDistributor { - private String contentType; - private String outputString; - private boolean throwException; - - public TestDistributor(String contentType, String outputString) { - this(contentType, outputString, false); - } - - public TestDistributor(String contentType, String outputString, - boolean throwException) { - this.contentType = contentType; - this.outputString = outputString; - this.throwException = throwException; - } - - @Override - public String getContentType() throws DataDistributorException { - return contentType; - } - - @Override - public void writeOutput(OutputStream output) - throws DataDistributorException { - try { - if (throwException) { - throw new DataDistributorException("forced exception."); - } - output.write(outputString.getBytes("UTF-8")); - } catch (IOException e) { - throw new RuntimeException(); - } - } - - @Override - public void close() throws DataDistributorException { - // Nothing to do. - } - - } - -} From 3bda5993dc16b598bef740d84bb9cff0891c2137 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 31 Jan 2025 08:42:19 +0100 Subject: [PATCH 13/19] Fixes for SelectingFileDistributor and tests --- .../api/distribute/file/SelectingFileDistributor.java | 3 ++- .../file/SelectingFileDistributor_FileFinderTest.java | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java index 18f61ff97b..8014c59f56 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor.java @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Map; import java.util.regex.Matcher; @@ -217,7 +218,7 @@ private File substituteIntoFilepath(Matcher m) { path = path.replace("\\" + i, m.group(i)); } log.debug("Substitute: " + filepathTemplate + " becomes " + path); - return home.resolve(path).toFile(); + return Paths.get(home.toString(), path).toFile(); } } diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java index c3b6928087..9874475973 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java @@ -45,14 +45,14 @@ public class SelectingFileDistributor_FileFinderTest extends AbstractTestClass { private static final Pattern MATCH_ALL = Pattern.compile("would"); private static final String PATH_TEMPLATE_ONE_SUB = "/users/scholars/\\0"; - private static final File RESULT_ALL = new File("/users/scholars/would"); + private static final File RESULT_ALL = new File("/scholars/home/directory/users/scholars/would"); private static final Pattern MATCH_TWO_GROUPS = Pattern.compile("(how).*(would)"); private static final String PATH_TEMPLATE_TWO_SUB = "/users/scholars/\\1/\\2"; - private static final File RESULT_TWO_GROUPS = new File("/users/scholars/how/would"); + private static final File RESULT_TWO_GROUPS = new File("/scholars/home/directory/users/scholars/how/would"); private static final String PATH_TEMPLATE_MULTI_SUB = "/users/scholars/\\2/\\1/\\2"; - private static final File RESULT_MULTI_SUB = new File("/users/scholars/would/how/would"); + private static final File RESULT_MULTI_SUB = new File("/scholars/home/directory/users/scholars/would/how/would"); private Map parameters; private File actual; From b5e664d7c5ff69b200a3aea45b9638dfa86087b0 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 31 Jan 2025 08:54:07 +0100 Subject: [PATCH 14/19] Removed double slashes from FileFinder tests --- .../file/SelectingFileDistributor_FileFinderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java index 9874475973..71e420136a 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/SelectingFileDistributor_FileFinderTest.java @@ -28,7 +28,7 @@ */ public class SelectingFileDistributor_FileFinderTest extends AbstractTestClass { private static final Path SCHOLARS_HOME = Paths - .get("//scholars/home/directory"); + .get("/scholars/home/directory"); private static final String NAME_NONE = "noValue"; From 57973e1574209748e3fdb98848e51df6f8bdf7d8 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 5 Feb 2025 10:38:12 +0100 Subject: [PATCH 15/19] Report generation tests --- api/pom.xml | 6 ++ .../reporting/TemplateExcelReportTest.java | 69 ++++++++++++++++++ .../reporting/TemplateWordReportTest.java | 69 ++++++++++++++++++ .../mannlib/vitro/webapp/reporting/content.n3 | 8 ++ .../mannlib/vitro/webapp/reporting/display.n3 | 20 +++++ .../webapp/reporting/reportTemplate.docx | Bin 0 -> 8586 bytes .../webapp/reporting/reportTemplate.xlsx | Bin 0 -> 6047 bytes dependencies/pom.xml | 2 +- 8 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReportTest.java create mode 100644 api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReportTest.java create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/content.n3 create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/display.n3 create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.docx create mode 100644 api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.xlsx diff --git a/api/pom.xml b/api/pom.xml index 773898fa50..c85a0cb5ff 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -109,6 +109,12 @@ provided + + org.mockito + mockito-core + 5.15.2 + test + junit junit diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReportTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReportTest.java new file mode 100644 index 0000000000..50612ceb2f --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateExcelReportTest.java @@ -0,0 +1,69 @@ +package edu.cornell.mannlib.vitro.webapp.reporting; + +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Base64; + +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; +import edu.cornell.mannlib.vitro.webapp.rdfservice.adapters.VitroModelFactory; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import org.apache.jena.ontology.OntModel; +import org.junit.Test; +import org.mockito.Mockito; + +public class TemplateExcelReportTest { + + private static final String RESOURCES = "src/test/resources"; + private static final String REPORTING_DIR = RESOURCES + "/edu/cornell/mannlib/vitro/webapp/reporting"; + private static final String CONFIGURATION_PATH = REPORTING_DIR + "/display.n3"; + private static final String TEMPLATE_PATH = REPORTING_DIR + "/reportTemplate.xlsx"; + private static final String CONTENT_PATH = REPORTING_DIR + "/content.n3"; + + private static final boolean debug = false; + + @Test + public void testExcelReport() throws Exception { + UserAccount account = new UserAccount(); + OntModel displayModel = VitroModelFactory.createOntologyModel(); + OntModel content = VitroModelFactory.createOntologyModel(); + content.read(CONTENT_PATH); + RDFServiceModel contentService = new RDFServiceModel(content); + + RequestModelAccess rma = Mockito.mock(RequestModelAccess.class); + when(rma.getOntModel(DISPLAY)).thenReturn(displayModel); + when(rma.getRDFService()).thenReturn(contentService); + when(rma.getOntModel(ModelNames.FULL_UNION)).thenReturn(content); + + displayModel.read(CONFIGURATION_PATH); + byte[] template = Files.readAllBytes(new File(TEMPLATE_PATH).toPath()); + String string = Base64.getEncoder().encodeToString(template); + TemplateExcelReport reportGenerator = new TemplateExcelReport(); + reportGenerator.addDatasource(getDataSource()); + reportGenerator.setTemplateBase64(string); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + reportGenerator.generateReport(baos, rma, account); + assertFalse(baos.size() == 0); + if (debug) { + File file = new File(REPORTING_DIR + "/report.xlsx"); + try (OutputStream os = new FileOutputStream(file)) { + os.write(baos.toByteArray()); + } + } + } + + private DataSource getDataSource() { + DataSource dataSource = new DataSource(); + dataSource.setOutputName("dataSource"); + dataSource.setDistributorName("dataSource"); + return dataSource; + } +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReportTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReportTest.java new file mode 100644 index 0000000000..ca53b0e5e7 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/reporting/TemplateWordReportTest.java @@ -0,0 +1,69 @@ +package edu.cornell.mannlib.vitro.webapp.reporting; + +import static edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames.DISPLAY; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Base64; + +import edu.cornell.mannlib.vitro.webapp.beans.UserAccount; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelNames; +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; +import edu.cornell.mannlib.vitro.webapp.rdfservice.adapters.VitroModelFactory; +import edu.cornell.mannlib.vitro.webapp.rdfservice.impl.jena.model.RDFServiceModel; +import org.apache.jena.ontology.OntModel; +import org.junit.Test; +import org.mockito.Mockito; + +public class TemplateWordReportTest { + + private static final String RESOURCES = "src/test/resources"; + private static final String REPORTING_DIR = RESOURCES + "/edu/cornell/mannlib/vitro/webapp/reporting"; + private static final String CONFIGURATION_PATH = REPORTING_DIR + "/display.n3"; + private static final String TEMPLATE_PATH = REPORTING_DIR + "/reportTemplate.docx"; + private static final String CONTENT_PATH = REPORTING_DIR + "/content.n3"; + + private static final boolean debug = false; + + @Test + public void testWordReport() throws Exception { + UserAccount account = new UserAccount(); + OntModel displayModel = VitroModelFactory.createOntologyModel(); + OntModel content = VitroModelFactory.createOntologyModel(); + content.read(CONTENT_PATH); + RDFServiceModel contentService = new RDFServiceModel(content); + + RequestModelAccess rma = Mockito.mock(RequestModelAccess.class); + when(rma.getOntModel(DISPLAY)).thenReturn(displayModel); + when(rma.getRDFService()).thenReturn(contentService); + when(rma.getOntModel(ModelNames.FULL_UNION)).thenReturn(content); + + displayModel.read(CONFIGURATION_PATH); + byte[] template = Files.readAllBytes(new File(TEMPLATE_PATH).toPath()); + String string = Base64.getEncoder().encodeToString(template); + TemplateWordReport reportGenerator = new TemplateWordReport(); + reportGenerator.addDatasource(getDataSource()); + reportGenerator.setTemplateBase64(string); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + reportGenerator.generateReport(baos, rma, account); + assertFalse(baos.size() == 0); + if (debug) { + File file = new File(REPORTING_DIR + "/report.docx"); + try (OutputStream os = new FileOutputStream(file)) { + os.write(baos.toByteArray()); + } + } + } + + private DataSource getDataSource() { + DataSource dataSource = new DataSource(); + dataSource.setOutputName("dataSource"); + dataSource.setDistributorName("dataSource"); + return dataSource; + } +} diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/content.n3 b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/content.n3 new file mode 100644 index 0000000000..d33ec7666d --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/content.n3 @@ -0,0 +1,8 @@ +@prefix owl: . +@prefix rdf: . +@prefix xsd: . +@prefix rdfs: . + + rdfs:label "label1" . + rdfs:label "label2" . + rdfs:label "label3" . diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/display.n3 b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/display.n3 new file mode 100644 index 0000000000..835fcb524e --- /dev/null +++ b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/display.n3 @@ -0,0 +1,20 @@ +@prefix owl: . +@prefix rdf: . +@prefix xsd: . +@prefix rdfs: . + + + a ; + + "dataSource" ; + + "1"^^xsd:int ; + + "dataSource" . + + + a , ; + + "dataSource" ; + + "PREFIX rdfs: \r\nSELECT ?uri ?object\r\nWHERE\r\n{\r\n ?uri rdfs:label ?object\r\n}" . diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.docx b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.docx new file mode 100644 index 0000000000000000000000000000000000000000..dbda10b50275150b5ff625c2f93575509f553353 GIT binary patch literal 8586 zcma)h1z23mvNi4+Ah^4`L-62k!6mp2?(Xg+K#<@bT!L#LxVr~;3k1Tz8*=Ww;pDx0 z{=Z=6W3%_F?yBm&y1Gh977_{*3=R$sY~!_rHrO*k1pRH`Xlmoc%=GkKG1(;p$%+QE z?v-tv=H=F)E^boW7C!z#%Ev!`_yp#IoK&Iq+Nu&lvyw&^5&P}#E_wb60kGY(D47rz zx~w@MGSKL-`{TOHO+DCAwbKzX63)lz*x~$H9^suf4mSc8YbAkPS3a8 zT)OP~nkyJLmM#&Ml)c~st|SW!x9xZ>z6UaY90CkX>AyM%9rT0=;H{D)z}|`ZEx^%~ z$=%k*M@3$-hXtwalD;&pf0tF4uJ|(j{SqigMJp0qM}VB+*ir*L?f- zp3hI)SwoBoSc>z}*c*?+9IrTp&K}~nS}(3O{|0p%2@76&GoHdIBiZ3yk(R>163(ON zDRi7eS(M6UGuK>KUZFLefSbQS1w;>r<}A=+B0va%2~b?lZ3l!Nu>ZVABlgwLMl2?? z1>=oF@d=8;H_?i0#SE)Q{NYxS#FQHeVW1EOiK?|G9DRKn|13UP~prJhvnvuQzQ_M0|mSlQaFuiZ+WzJ^` zk6p{?7s9c{MbxVFSKqyLY>ugumdZVPXfxMi^x;C|aWZ2|!WUXX^0^3kWnul!GGW^? z@U?cqh-ca>?m0<*p8Z7zFxU*h9~;D_p>CNkMxnuI|HM_Y)= zSp&fmJ{qtuHE3_9q+~;;4-5)a;@Xkng&YjTgzm4NU(r9zscC&)GH|zSh`omb*xL(= zW8So+2%5hQ;^q=v+XI(SI7UBn+Vx~4ooox|2cDd7P=}|eYnXAMp4Qzim#-=usOwQ; z^hGLM1hCb3fV*fhXnR*)e*3H~Oj*{fk15lTDqA0~zSO|3<;XR%2;FhCCWd**P;^fI zE7UT zllfZ0!kW>iFCkugCoKE3zI%m^3w%LAyr$|6`4pY@P`6n_P;^ZHF*+E}qx06q$r)h# zD?p>w2;D50AsZ`2a0^RnunCiOQ9S%=H}R_t;7))6`6wSR^1zeWFydDVQ-VOTa ztIy2zv|xDlPJgTdNWEBZv9Y9gr*llMO(3*`ym3So)e@-uI7N*28r5`2v1zVO3SGW+LDwR3)Y ze>U5?EYMx3uU^SRH>f#jC~EA{PYDmH?EZpmAG|_l8P@qW|8jzE@a>U=l4p@8=3%Zo zn!^G3_Ymr(h8LR+i~*cs{*UL^dgdIjyxJP~_GE&koN-u9C9%9H;QQ0eiP7f1&B=ao zTUE|`^;!TUBcC^XNrGQE_x|0~OWzdU;1)`Bno|Y7AGmSGy#4ka6hW)h7uSMCnLIo~ zW!-3tLh0(1OzmDksMpgf8LR%z02j)HNk23g;!l@;;H*vW)zJe&>rJU{=~@AG{JUV%U*kBXk3 z)g@4xoMG8)1U&%^RE5TKoGMZpCK=;3eR$!4$Qj4=CApU8`n9AITSPN~ng|lp44*4i z(W<6^T%tpHDhbTD8GQW%7~7lpM?KPXpPxnUD?x)Y(8-oD5j4Sm3v^wHGh-|?Q!A3uhW@y5>rR>%#xbjUH7w-Q_?IBqAJ9MCY6De@A20 zHWVv}Cp?1mc?$AmLGHe6dNcIXeQ_x}rN=wuk(M1BYgp}g?1T&+n!jv6pCZt^@ zmP6y}Sg{$nyY}jbGZyr?iv7e;l+r#9-eL^}Iz|}XE9gcmt;W3-KAOTMi>6N6CT{4uCW4Wm$9X`3 zGkqt5FUC{p)W`1C@`XNKk3X%daltqv=Az*M>(b_MK5VMwdxMAOLL%u7a*r@x{Fl(? zHM8&bq&S{|+u^A9*Yj42iBY4LE+HG?&BiW*8q6{LuQs`AY97vSG9!JhiQ$PH$JpnY zMev|=U+yI;*%*4k>9S>q4+?Klrcy|9nEtRZdhnKtZ8JJOt?%@sD(!ktMcf+TJsXy?q<1+9UYSdbTJJj+QD{PvTH2O8+=#g zl2kLR{#=Pu1Co^44os+Gs@~dKsK*DV(WvalyY^d=y9DJ+A149PZ;`+iQe198261I3 z=+|yKhtHT>u+I)Ds^K)5ZBiP82e5^-|& zurYOd;@o2$9nB@4Ckom4=9Z2~3a&6kBsJ8{R8SYNV&10_{&ML;1|i_#F2$4=J1zz4 zXn#S_+(6swC=I~Den$a2@-uGCVM|b%6jw3ScUN!e#^S|UJSD2wy9@@7*dxg86&0vg+h z&Rw`*dgoj5jBVr3iqxuJ=3!^gPNiXpF3RjuH}E(eZ$49S&cZs+BHK@!#VlyBKY3nY zobxE6*0m+ma2cViGJnBciwc@53kybMi@36=7kYpdoQ;Rg-b)@Pr>xa+pg*c;V}H~; z6PE@!$e47_j}s?^%R^n(o4CFkkcX#K$&`N06&yft=WDdC;8iIYp60s4HZ``UWWDM1 z(hjFgjdlJ^n%+t4VVqeY!D7#?CB%ecs3x2OH6)q}wj&FGA{=$npJtJ}f_o=U5Ix4$ z_bx`RjG(g4+uVQGlFr#EV+5n^7^Lp!L1v&k+OdJ+R&0)un(P*td4%9==E z(!TsEC}-{3X#+S@VW_*ej0#M`peLA0)vIC2U$CLkmosC?_p{1Ap+wfa0ALpumiPIO zVfd^Zqso01XS=^A7+2emD9EOwknNN&L^B19Eg&ox4{;ijr@mK;IXi`@dF!Ma!j?zz zp+mn2$vF^ex&q2GC8Fn!%JaKC!3_6ACBRr|t=GW^gBrk{;|>;edS(&^Q=FTL_tC=D z${%l`1YE|r2&ULS6OUwrpHtY^&C1`|ZU!#=Gc2E^*IXx|Dmvq#TY4d%?9Ww3^evHi z=N;2B7XKAvBZ2H<8BKoV7CwzYYE;uyyGEnbF$X^KL}{>T=e^S!#>AU4LnGV+3mwyy z*AySwi9*!e-LACM`8Q@eHdc8k!Gm&a_>AU`j0#)NhsTX@%R2x(ZVMe|FQ%>i)^d*6 z^iwh)+Oc-I9zsHGh_G`4)YgP-@F2>Of%>XwS`c)h_S3~adww0XKeb}%?}<6Ai0}NC zi+!pRZ*Eq~?6mN&H1)Ty@TRj{$cY?SGbNckai+7oWWxtZr?Y!yf2vck77GP&?@jR| zLICA8l#Apqit!7iMP2eZ#>(x=cW7s0W?1H$0}hf--0pf!s-1k)J+~K`UL1Md#B{cM zMFE^Xb-t@zy1Kbl&$S>c(Ts4NzY4W~+vc>}v=K6fJbi|+2V><<>kP@+M8zk9_EM5t zqo*t%zj$MN5;?fet{aH`r5RqCWmY~su+dJGq16Qgh~Ec-^+)2vMYNKlz3|BM>u(A` z8^HG`N#yPoB?G_(i?5=A?rrol!gz>bB3*>ixNjj>pAFy}lO}LOJ$vF8`T=hnN`wLs z9`YCJ>3=~TIJxoM;$0+w@Ic8`G{m@8(w8qh#7HPELWQNakOSfe@F9MwA_0H}N`W5u zYa&k%z=Uxt4dCY*KEcUBN}u4r?>>XabA$aks*^L#7P2x62)>@c-P`>H|2?PQqoe&{ zYVj*>6KV_cgU={gu2l-0Jc`X+zK-0)>u!7nfmGlE(i0qptfJh;Q$?X;yna`7aW=#! zIwW3V)zZC9yk?gWoiIx%1b&1b@9Qp_R*WP7qSnu*LX z$lKvx(VS^*5!HG)m3ntruc=(w#&_NU7!= z8>FyX99O+G__@Z4?6G6+j{B(oh3epStzpG{ zk^Oc)M!wiCcow#NQ`&r=C8ger)J@0+GcmNOGr~GlSkK6R9Kz(u)#Ye9xnI^v7)oRr ztWz=0L_T>lHo=RXvva5(JKI>Cm03Z5kSG-D`=DCx8t}=t^DFxgsufe$wS2v;2l+v{ zBo!&m;vmZ)r^d7ewB18 zl!p|0gppb=vC7(}2huesu1A zyOFs_j@YNPzn?#j5EP?>&?3`en*}(nPlPf;WP&Nb0+(RM=DuvVF_y}8^op;kbpD{C ziAUZbfyx#^7#hu)Aeeu@ow-jA;E#PNQPb>Zrh)191rpll-NxELZy{-v;zYX_(mqA^ z9?3Rxeu2W(ra4|!&F`)#E)>hKOB2GXX7JL|yRdY%O|Nn< z1p(|_3s;Mz0)ctymK>pFAADlK1EUM<`36tj9;ehtQMJ4wo>DS%$KE6w&@sRJ|L9+YrUZ-TO$Q8L6v|KN8J}I5EK^b?8UD#@nZB6!)tGs1*^9z z&{qO9n7f*r_a`lcaehS+o!&!W#WN!V*UXu;Qw_gS=WW7}rr1yB*!ze^D9-vpUw%;l z0=?F_n^1RnNz^97JFBpIF)+m;oQU8pCk%Z=rBPA?PBQ8nk!W2jdA`w_57RQoy~#Q! z=(dwPM555K2(0a+;+|lb(>1JhiHDRq=7S39qRX+0RW@+M{2kEL5k46X2*?ERSlZ0{ zhjjv@qvF(zEqv_<^<3%A&~#`>oFb1GCN5Yk#`0oJ($$|_pxfvjUJ}N1$Ub0->bs01 z%tr)Jy1>Xd*G+q+@_%0zpIolycIblgL++$E-sX-qoAIC#QNiGud_-6M5vhiEjLipy zq^RIf{ywr`?HdKtvE~mluaztqyEA(~8VFoVqI!njjfUAs2}Q=y4!1k_r?}8A;$CQh z;sWuHal!ic`ggW4wKZk__09T}Fpp?c3;lxo~u&*y4=tmS6CR@(vfNq>;Agx zvlOWkUKGLQ>v61Q6==XL@uzbY&AP%)25hSc&8o>}J+l08*0(MTb!w@fa}zjc&1htk zW$$e)tYdRYQuFZG!QBqGX%9c-8V0X&Zb{0AxKboKmdo#z8dn4guP^rg=!57I!zR%^ z6$1`fHY&KlbZ-k*pZ}z1;POLiVWu29dMA4pe2V~c4LNa#_Z{q0QB$|CYZD=fUo=%y zRXDb$#~3zmJ8i6rjK%z=78q6Uk?{dP(dD2AcLJ~$!# zvva6ym`fZw6wUhum0JJ_3hfj-&j`z=|+3MbzvMS7KULWKRTmj;kh>p zoq>n6e)u+v2wW5_J->Nvhtx^*>2p5Bo~UWmWB6AqyW~i3v^T2`i6iblRsJNRI8Bw2oF#u0%kd@y`iGccD8+?CY$0z?pF32Re7JOS>QP?>Z3NYVe4|~ADy0k@ z3-d^xLfw)keM);zsiRJ9Nu8f|ZzndT1NQwboVletk?55Qm^S9?F-?Y^@nPP?pxRy7 zJh24R?HwUT|MLiF7GK$t$eRMI423ASuakRwDEADu)t`jtQu+HSHcm=TCXaF!b_>1G zn(tffIg4c{GbbplQaxAsO8pGvVbi8C#!g}aM^~lCxBRod;%5k5$Lt~t62HQ*5+R&K1*j)+h4EFulin0+z6K%%EP$6 zm^sUDaWJzHn3QiLod+MW9W4v-I=5<9pai`_E(CS&!u)JPX3%_EK+5Gk-fba6qku2_ zM|~EpCB$%B1MowA@JFMcMl1+%`Xet1fyL`q3R}k`3cGtwWvKq>Wvw9d;{ z?Kp2ber=dF#tm+tKz3n+NaMest)J?(zb*fd%@xtV>a`MX9l5P5g?!=3vnpuR=%ffJ zOwxCYFo~^+`Nof{;T`MlW1llhJ#>=V#$n|@h05y-hbkx+ttj;XUA0ehc+2Uu9Mjvn z;|n?v3(Ae69q4cot;Fd?6)>8&JTEkQIe?wTr}f?BB@4MxHaeL6RP`L`FwBJx0R~4x zw6QSiy|Mj$hR9>ri#w>k3Y&;b-BhVKRXwmzY&c$A(47IA`W5K&-;KlhU&j4v_56xz zX8eUg(sh*A zCk)?O;N>1_+b>(Zl2VJ6%e7GWv95K&zz-*&O_Ri< zJG@Uumt-p-?WRbx`YO8{a`u|P-6IJ7F;cI$0Hc|;9B?We8GWyou%?O0{NxXNCpm~g zkhS9f&tU6_fifdd<=Q~a!`{?M?`dynOA!PFfn0HOkI<_fkc6VHo?x=qKW`>gV3%Tz zviZ_)z;$wK?FK1a13e@^Un_q7#3#F@wmRun1G5VzdRs=JM_K>{GG4AgcW3I%;}%UX z>~eud>x5$-%&8zVlz#ULne<(Suo6B!@(c1bwm?2_PTq-RpK*eLZFg^p)I^m0sEivp z^RI*^&U#JjzERoqOoKh^4TG1(mQwXhJb5TRy^_*pqryh~!T4z>>ew?WeZ>q;N0HWE z=lmi=-l&}+r^9{#%~orR_sQVX0vk=pillFpz7dslLR}p2lb%QXv?MQxWQ?AC`Myth zS?~u+mZl_LOeYUY82ZGW`^Q1HQ82+-2uju;B1Q2;LuS^8$vDxKHYmY{>pTOt+wWq@TZy^N@ z^}iaSf5M+PfS#IgzhxFgu>XPot0ng*`gxn>iFAI8GpGRgAN22*(Vq&QHyob2F~3C) z`k#9Y;(&TR$K=>E@ccJo6@bmQg>9F!!E({oZvn^uQ^9ZU|5MBJx9#uKKnJa~|AR4= WWT8PT1q=)Y^pgZi$TtX|uKouj)Q?~Q literal 0 HcmV?d00001 diff --git a/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.xlsx b/api/src/test/resources/edu/cornell/mannlib/vitro/webapp/reporting/reportTemplate.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..ccc039a325721c9e577c2dfa79cf751ea3223fe3 GIT binary patch literal 6047 zcmaJ_2|QE{*B|@7jX{=dAv;+z)(9D6&GJ|h!q^$ikX>1_FAZUoA|{ok$P&gb*|TKd zWlMy}mgSr2dA=ua-}iRzZM;bh$? z%XxhJ@lwy3gFKh)No%*O)4f|=L*6UzE?!&38XG#ulOx+kvfY@1*QOs^+nV-titT0% zvXtHAzVF8&^tyY_6e`6`riZ3m8OY!A?t5hG4(oG#bt7$C$)*!Ld}sBVvAZjYw=oO1 zU%{Qz{$0I6R;wVyxxTIQR2?oXsX0kvW#Kmd0`a1;cd<`yslZ6*jgyfy-fhWiEXLzu z2X8ONw~r8qnNaR+E3M5342TycnKvMG-^-Rb=-M=QLjw>#Lvn^XWaRaI3aqoZD7+v9 z01W?Yoap|J)7c0m8c+C)OVWpIn7w7Qj`i>)Ax-6>7eH%EFb*tOR1TYKrYoVkG1)5R z4gFl7o%q5&R!|c^3t1sDuN8TsT)XU#tnP`bBK3HSVxW*9oWH9J`sBlU{{!8&;}icj zqmJi>K;AMUb@BJ`YwQ@aQX>(%nG6A5bj7>AJw23~eovIJsvW{h56mf*=c_WXlB$R$hn6eK!zZ_5 zr61HLRXQ64EtJyK|?Ndl6`k=?PfCV33N+ zt|;%eS=pl6JW*sWG~3qkd>)YEwGmeo2iI7ghc4UPYwsa)trP8sTNaTZ!*F8dCNXCY z+n3N-H;O`K7s!mry*@?`xXqQavG>yS>GSjA;oL<_k;Qe=!^(SKnJGVn$y;rjtFOB_ z!RH@jX)R3DS{KT#?^*T?^SR5lY}QO*U2oU)9DEN>veS1OY&uf3oXVx$k}LkqUbt0T z7mX}cuk~=7FyP?lf)AZ0p~JjB@6+KS2#QsgyM}`wA@QFe$oMw|kVY17LX!I4#t)|OuZ8n(DF(RpFNsq53BFdZoMJ^+4nfLzUaWuHC}`K8f0f%JX}b~} z3sDZ%j7zAiInCt@xMr{Uf{|@XS5U2|%Vb3SRX9e( zkY{UdT%0F23NeJb)K$m6re~468)V@E0>iT&VMwj9MJRU#bT^tX*M$)tda=G+s>$2< z+r_ByAzo5`@RJ%b4VX}WPH>V9A!KBk?~1mP=4fh!&YMp>+gTy>K@r7wcIdN1i#qnW zFgzc)85Jn#U|*PO0RuGEqDnL)fX}xMv!=_3*?pAj0D$HCw7IcjFtKW-O3fX5_@+veh!Js zoJgfZkkA~-x%ghWZKRASgXJam;K&UF2eR>c&W`mbnq}7W&=on$ce38Yo+i!@UD>a8 zW-CI2U*|$@K<|6jNuMLAP0%#m=ckFw$R1xMK1PsS7S&lT(X<9CBs&!pMszl~1jCPw zOkJ)P_X-i9e|#XgEkKQc6;0d`TLm?1`$ngEIcM#qhCb{p3X{)-wNFVXnGJ3X@#fWr zaXH$f*SFVCtabC7 z1{0E?keOGnk5o|UMz+w1bH)XAy3Qa9m+=VgN3G3V_t&qsyqfWSlur@mAx!6nQ5jRI zUfy3gv@B)3SwyUrY{LF*fjT+qYQl3}lTHVfma6!J_tFUo{8Ic@+6pa9virOz^sG0$ z1C0$`83ZBHEm0MV1>BO_Y9yDmUgiTIJ3y)IZav>_Ov^%j1fyn~z|;kIxpn8kD4k0z z!#Nsa7!vmLk-^(yBwh+l?OiutfQo`KDA#6hdM&jJ^jc_?TQwjd`lRcq*$`X1!CdUVB$~B&YmBR!2=+Wy8yB;k2GvXyr1R5$ z+lar95&5BRF>l&9m@2SH*eD*W`|Nw`W;ZCSBRVSU&DTr$FV@_-OHgE4l>$`hgcU5T zMP)2#%8>vzkB0?q%Uv4Z=R%*2OO0|#e~LiMb-UM9(K`O`QF%#Pei|dO=umF#DWJIM zh}H4zJ$-}D2@BqfJ~)vNov=R=oye;rA@eVEiPcY43y+B(peDYvP9izf%TP ze>tEIyB#L(FX6GL(Su5YV-rf^qwMR1uY?)2tseF5s>E9m(dJfOK7LX^{SfPMk|6QIxiYS{>9 z7z~M!`1$d(Xx9DJZ{Yg$9Y7juG_I3tgV1~tkI-Jkon~J8(Y)62TBv!(3w2LMm$T zm=V^y<6(j2TML(}02z%2ae0Abl}{ef8u5s&?6tpW7fL0Cn7w8aZ44E}PEXG=QLy>S z#O57-<68<}oy#O)8}Rm509S*(n5SoN6R-~+oX@0|TW)=o)-B^@?Vn5Xgo1>xqNXTn zoOi+(?ny&Ds9eT;2!KE;Y!KYqQd^&S?*Is+YUF`o%99AI z)u-A=TKzeMGxW7n%)uqMC^8>LHo=d^Cs0y$1s}GZ{6=-cjXr z6|st8-(ld^a4d)C3{!{J+`tXVkto@CNn5^~>hB!=MwQLXh_*XeB9R_-9A103jk6G^ zcF#Zh3J6khkyNp6vF%(V%WGQk0=sT&mBAIsKnB; zSwcO@SifKH8J`k0Utemhk6gMwh-P)&y3;8okAVRKwVFI+L{J5B&)Hlz(AE?_qQ~Vc zdr8(@iK`4{ibkmiiC{L)3NGjXPz<)%AcN+*u6PI3@7H@>(XcE0@GYQ@p6+A!`?P}I z?#+lw7wqAvpXE7lO!aOk*z=5;vFyJ9WT zcu=-Hx(}Oa_w}!r-g)|Ww+j<}iB`+HGYoxgU1o!Aur_gTuJ#!t?#jrHY6C@yW#CUc zhpRtaZ`8~(%1-;fub7qS-+S=t%hceb*7kr31_23kYb^S(K|&rLgZt@NXvk5}5B|+>A{$TJ67xK} zk?yClSw~d(;&8NIygNb!0K`!IsW^cC>2}Vyd|(d7NFNtZr=LwPB)JLCcr-SSDN7eD zyE<~yxP_us>$wjArqgZ0!=3#ly3mdWEt&4dSHgZa#&9MTr%x0MC+Q=%bG zR0W^rzMluh7c?25`A@hGB`%==i;&RW^&^89JmJwWC*eBTly3;+U)|nrKOo39aZ0qUt6D-@WIRKPZqkV3g-j+IX zh*^rXSW5tUJN@-^G>jM#5Df5vU?PZ^oO-l3E) z(l*d|%g0AuOFxI9s4SVRuec!e{*mr6;I43h+Z+18y$}-ia32_cAPO;3!v%{TyB*iQ z_fZ>~tKpFXXo`YDKzHA=I=Rrq+K-8Qz+)u57FaYPi>foWp^@2_b5-AY+;;Nwd;g{P zgzp2EEG3Q!8{d5Gj?!#7gp~`~uX#EOzhL0fY>Z;!sBxyGT)%kZys(`m1e_WLc)=?8 zfzq4FxL;GfSTmvYsd{^?t2t@atOpO`sfRF`Ienl3>76|8(I$Ck6j1Lq zV*Ym!M~o`;l0yp72xW94eHcQv-O9sV@V=~L?~Y01HCK}1wOBh@ZzWE-wjPx09^IDd z#tyc8Rr>4WI6|mGp{AYiU1x~02))F6({|YQ=JT)wlwb_+mC@}yzbnq~iyg6bK~qf) zhuO1I;tqC?dmdT@t7Mr;%WXV%vhF_G?&x;ky~XUfRO43WrmJ?nBeGIXAG)d4w8T0W zabFoiIC({0P{_9``hEQM3Ixb2>0g?IhSV|eu<*|2Yy1#L6fy~Lt~xU&Pu znfc?Pb}ffxfv1~nO*PTWjP%vkwye+8>5IZg+cv2Zc&~ql4&; zC#(na;GK5_ZO(OW3}O_*jY6-ACn{Io1U>9FWJpC??oO-4>(lpNRoYDMCp(uKW$q9j zFE)+=EuGVj>Jk@eOODW$=cpQGw`alzR&X`)TQM|Caxj8v0)Zu}NS-P?2 zt#7KR_qk|)lV>?v1lO+TC&84D0sC=wmL_fyhS}h?W1Q4U4 zy(k7h#(=e(I%aJ-BXbr>MQi;=tS*qmbP&Xga=XbFfLt-7y|XkK^7i5vG~q^%y^Um& zdYjQ}^8~31CPUZ9q8JZ&gXIslR%MTMXdPxY_NU=IK^}RbP7^G6zvhMkcQSByy@h-I z_gxR(uN&%cPbAC}X=4)L1w-7#`&DP!fq(q#_C$DXuFt0vT^0186V-aIA%DT%066G> ze?5J7gY}}}gMlbvrVR6~?i-F$rQ2IZLmh;VGOp2eaDPc9mXW)<>I1QUM)z5kH97DA z32VBTS$Ldc!ZXSaReq&<=?NqP&!v?dTdKSR$@#`slN{B#rl-4uJyW(z#Z1K5VZ9UToXqdB*A|fCtQjXui zSXz@up1ft5k_TNyo4LuPs=D2iB_!~j;rNa{@j9Y!OkEwJ(HCt>b27PjIX}$VmuR!x zqGsgBU~AT?IhpX1suEdr!@(GPX-7!Amg45=rG*vbe8P8skUY3<5?&XhF9y6z^=-4t zT7=hk`Q)mS6>*Gl#la+5o-1z@tKfhZ!J38P8qhGR5{r3(0J|_Kc;cQpM zj~KsX6X(&Hf#Y|@vy~g)bbkp8E)l<~%RepnclEP{3SZuT$r|qTU-f^A{qNdm%fWwU tP6@{>|JAntx3v9k;4C)((*Ou}`v24rLmd)aN&x^W+|h( com.haulmont.yarg yarg - 2.2.9 + 2.2.22 javax.xml.bind From 11d6d0d7fef3e97bf2dffd4598afb5ad5e3151de Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Wed, 5 Feb 2025 10:38:28 +0100 Subject: [PATCH 16/19] File distributor test fixes --- .../controller/api/distribute/file/FileDistributorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java index 1d574fec12..6d3dfaebe1 100644 --- a/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java +++ b/api/src/test/java/edu/cornell/library/scholars/webapp/controller/api/distribute/file/FileDistributorTest.java @@ -60,7 +60,7 @@ public class FileDistributorTest extends AbstractTestClass { private ModelAccessFactoryStub mafs; @Before - public void setupData() { + public void setupData() throws IOException { mafs = new ModelAccessFactoryStub(); setVitroHomeDirectory(homeFolder.getRoot()); } @@ -109,7 +109,7 @@ private void setVitroHomeDirectory(File home) { .getDeclaredField("instance"); instanceField.setAccessible(true); ServletContextStub ctx = new ServletContextStub(); - ctx.setRealPath("/WEB-INF/resources/home-files", NO_SUCH_FILE); + ctx.setRealPath("/WEB-INF/resources/home-files", home.getAbsolutePath()); instanceField.set(null, new ApplicationStub( new VitroHomeDirectory(ctx, home.toPath(), null))); } catch (Exception e) { From e52b981c6b9f72e5ed399172ed86f2bbc2f36c06 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 7 Feb 2025 14:58:05 +0100 Subject: [PATCH 17/19] Removed not used/not completely implemented compontents, overview image and not used translations --- .../DataDistributionApi_overview.png | Bin 140754 -> 0 bytes .../rdf/graphbuilder/EmptyGraphBuilder.java | 20 ----------------- .../graphbuilder/ParallelGraphBuilder.java | 14 ------------ .../graphbuilder/ThreadsafeGraphBuilder.java | 13 ----------- .../firsttime/vitro_UiLabel.ttl | 21 ------------------ 5 files changed, 68 deletions(-) delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java delete mode 100644 api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/DataDistributionApi_overview.png deleted file mode 100644 index 9ec3f1bd11ca9daf19ccc170a45c782b0ab4c535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 140754 zcmeFYWmuHm7dJ|GNlSxL(nv@TAf=Ra4Im-iF*FPa2uO*9bT>*j(#^T? z@ll`u`}JJs%X#@Q*Kp6?d+oh?ul2jbRF&m$u_&+*5D;(`acT zE#K9ZE9ZXSo0*g4i@q~t(A@=$VAc{UguFn-XED{34;gsF!x9lN=s+|qNTQ)B^scx# zhv@#4wC4l%d^<_XR#(3B{E$%fpscPQmXL5eWF8eff&th!fu{ zl4T;l_puEpdQoE+ZfbG5vtxYFtKp=3KES`KpA?s z^2}pA{AGi;s~>Z6lzw2o#WlT=Vg5GHXZj(TN;&YY#hke?2F)eTb1aMAn_8+H5jFMC z<;|IqOpMQ#h+fXc5AYW#D<8y_(YY(Mupf91Uk#_S9Al=L+lP3Mupd04@hfLn#&b(E zkFwm+^RMk2g)LG(`bfkArd>c3g-UqwKpDN>A(LXD8`5zhBI0oE|1%!dPvYr-SQ zv!Q62&(=5&QDu#>6hlX!`+6XL4Fo@zc!Xkz$K0~+D3FTn5c>4nlT-2;6r(_gPbbEh zmxMHO2vV}-uSiY8O9&wM5yGUt-?>wU*d_NQ=1DeK#>7`906Bm(&QfkmGLOp1+n zpNE#xIzFO8wg@Qya<;(Wq)<~?BQ=9)fDFeS52E}Ox%k7wya99W4sF2d!owe)r@p7A zrxB-ym)zGxP{o1!I)n`v&rlHQLsg}684c(T3Db#~UO7J&doRWl(0LC|PkJYsKtHTH ztnD-A8_}+SC3!4*##n*4CkjW|Y}s z5h~qLuTqavn_@X(-Obg=7at9#km=2~|2U?~rJkWG(KTT9)og-S^j#$HGOrSEBX6;J zSeaKDyo}AP-2ADTO4nS1FQeSs;Y63j#ZJ2zwnUM8<6MHADsj~@_%SN+TyKlxn>i)k z2WSsy?`bTsEqsww1tENv;w2cbx;3U+_#9xR~PF^Icn>qXcY;FmflL#wW#YLH8w zQ=?aE@T1}VODOl@U0YeKtXEEN20Ko9ReaMo8)`hKS4R6c(@vf#Xm-i4Dzh5pNv@|4 zk(uX|eRM9{S6+Ux-0^^%#5DfHg8~h_e1m76AD(EtsV?WQ6r^kT!q5ut7NF)8X^IrO zKeaOeOV8?qbDLDI%FfwvLqZ~C-?ZCDVpC)#^C>YaHLck9MkI43g)3^mz$$+`*-AC6 zHoQMbXrpQAW~F3FX&Gw0YAI+bF-E^$;`-gqeY0Zo^}6*2=NR~-{>KYYbZ*Dc(~;?d z@w}5~^B@TW{{p}A>sQb>h|_e_u)c*@`q6<&?%9%|J@}ozSLSbRU0ZUe$pxwel=(OX zo9vKMCX)7)@RAO{cJx)Ja3vWDHP62_*|1JF*Rv3|Z09}@?BTHy@NNV_Bu4_%u!YHd z1giy9cqB}&2c%j*jWcw-AN^kRp_FmFY-S%$GTPl^ouwHuni9-}y+}%9@W@!H+|agh zv~Ko1C9){8GiUC_h}O73yL+>pL_{B!8A%y@^7;16mcUe6`Gu{6X@-^m&@~sjdZ<~b zUTCZgTEur!YF)mi-0e=MLdUm{c+yN#{oLO?vJrAgWlPJbTdUbK$Tvu@r>JGBrE&4y zezC*kY~y;dX207s*Vv2vWAC{0WNlw#59f&ez~}pi?f3=FRsI#b?Z&bCUi8hy)$-ww z8AW|7Wi}J0O;t^sz9N?_eyx7yekxayS2EW{5#PQvML;KusVR=wl z5+u=hxUcoQrt%!ZzoyF9gJxt0`7T7IjUeq^ONk*{8`tc|TTuV>ouVmf(s z(r~i5@Jrz`+|KA@rTsx&>k)jDO#nn`B3R)tGMQBGd|)YLT+K$1%j%i>5sg&jSQQGD>JAZ3 z4!N|SD0&x2+brsqm&^5ArLjpAdo-;ftxN3$cSTH+bxgmsE9PW0ps+TYyxw#Eyj)NH zRHP1m3a)h@-P~AK9`B17rz%dXxA(vLiaQp$k|H3OVy`=Iec5|n-OiZ;&zY{O*00;H z)AuGlGFWrlX>{#a>>I7$su$els2v|Bn-UzLo#nc_fcttzrS@h;ax5q;G==6l?XpCI zx5577ZH47s*=9j`+xx5NVrULxxPIr3uS5>_qhCuj;jM~iiL-ga&l+BQc3zsv9k)D* zFy{&}!ZK2B=)O?D?!PL=HOjc|SuWN$y<^(u@YsQAp0r8b4{~_*>12Bmx~L+a?U!_# zbfa*leq=GW+Q;?HC|#xSJp4xQymw816VkjJ&7{uE987h`VhW4S^3#tW2n}Lb2>gTy zdV&b|-`$NCr5<=)oWZmQ5nassi0sDc#)*JphZGPzx0xe~(On^cfHb&E&xue~_GU%m z-o3eQXW59E*Lrbpy1F2_f}bf7_lo?To!@&p@9UEYZy_U0iGTGt$cvG_YVKihNpQ35 zXkp&V_<^s9jQX{$6d@V`0WsZ5L&sG|Nm1C;!H)B}nS+Trr-z**kk}$1h!w1cxb9X}@@CpUvQ79Aa(sI%D% zVYSDyzorBKi7{BZx;hGTaX}yuPRK(}2WJZ|9w8wiE^b~fUS1Ah1c!^Kz3X!i4ttk- z|19$FI*-j=Or5P9U9BAK>2BA3ZsOqPD#pNYOX&CCKl?QIu=+#E-sRV^fCF;f{=&t> z$<6h9ZD6YC?Neb@D-Uy9oyS&o=JqZC4RIdshuoq+C;T5j{UQ0+RNX&Q1-S+Op83}= zzh;VZ-JamD6aB-kpHBgPiDQX!{pP(mR-G!64*~)RLE*8ah6m!-EV>WXm)V;i8D0>F z8p_4+)`<~>Od>CAMgltMec+FtUWI0;D{lW%JzX>=6cm7h0Ugj^%YoR|mGTG(2yoUw z+}7`3orvsHeLToZb!DDi7hg!C}5SBl|n#7AprgL0ewx}+1IJ3 zZuQ^Cz$ix~6tqWo{xcYO+G&i4gnZX$FHaZ=gDwE!uMc1}`u`pM|3{wG5>#-gKASb3 zF4!o{7d9KMq%Xkd*OfL63YLnrbLdq2uWCV;u*}{quPmWROtz{d#FH-TIz5Xz;$SWL zVFUcktRVX1N#m=); z#a+u0?alwFotGl4%k|qBJMm?HfkR1tG8!8`=Z2ORzmA_@0`88!aXk)u*|(slwx44N zDXX8-_Vj0QkomThB?l5;yTe@xl!0Ytc2#MNOpYy>?AG;;R=K+69@vL=H-q;9w<>tr(uK^Sq?**O>O+RcL~f>xfVmSA!41#6pahiXmS!Jdf>|0_|Z#QRXG2DJs{w@xEa;N?n-z(&#&#PLy;57!0&!IwX zqcEhXvD&pDl3L4T7_weBq-&oG-cy3SZE7EPv3|* zedEIY4_N>4E+7b|Wld{}WWd6gT1s2v3?z{s3xfiFk9YA14)a?AmzLN}aY#7}b$B7*r3sdssPKf)T>QGKS4JAJeZwYvj(s1(5yvhe0 zOzq#oDd6%Qv~!@ZG zQqBl$exY*LQf}uVAGvr;`7m?|_kVX%$56spbM#G`%ip9xP-}WpF0#CQ*2s2Q^T>VWc~+1L!D@h~Z<=-lLD{Dav%SRV!DqPjcQ?eJ`3PeNK$01f^ znk!q&FM_QEeLF+HKR5ACB$PpEtn z(E@_&5AZjbrss8Q6q}=WcUEOu{)wg&! z8YmE|n)7sg+pi@iBHJ;s32AAvW$P|@s{iY}P%tz`Uj{C;$O*typkT*O*%g?0*|x?o z0dfJy-bQTOQy7ED_P{gDM~}^*fS@q>ta$MWky!7?#4BeW6FXMpu>wpV0zjq%FA}Y#;ROIpX zP|>aJuOG8jx<$Tw=4t)T842}lQXtm>szk19t=@55p1LsCvc3mRc8(#&_+^-YS1c1Y z8yBm&U!yK*1Q<}Mm{wHO&ZA`{ zuWeP{`n_%b=f-S9Mc^JPe;J@x(e>(C*OQU5LU2Qo# zhpJHpbTdyuNxA(_aD0j~qG~cRD#N%wl4aYMZ&|FM*Jj3rDp`mIMdh9*XrGmoZCFI(Og`8)wfWmKD$Hy+NRiQG(-qM)iB zzs)pmlmtmaLySA4ys#RYEDNsoX2dYD4nvTqu{TRPPW_IrT#idR=5;J+eamK1%U!R3 z2OFp>!GyVSY?|9=mintM^D286^KMchb0lQSDvil)6^`vJdA*Rs(GMK)I{6+bhI)@$Z}*jcmc#<)`k3wb#>Ap7pCop zRsmOwgN$0Vii%0$tc*y=xUGo|(=U9@9x@-}Gg+)nx&eNWt?}% zX7ad4AG3jUp-5wSl*y!j-X(y9oaa}(u9U zr+eD?a_^W_EIXqIzN9tgeN3kZ%gY-?MXSL z(J#gj#Iyv6qu#o>t#pe-^lG$X71p;jxHxP1{bK!owxNo&wrzn5cW?FB08!#KNDOTw z@Q2cw=U0vx11D#dY>=OyyW^Yysx;QdUF8^go&8^#G1Q)DV6U^u+*82jIR7n$+JGq( z5trV>Vi0Xw>zPAnFZq-mdLT7@Odb*z)+sG2nmk5!R)Dw&g%mF9kz{gI)Z4rNnw#=M zg|zHs&w;?yd9S9J_N!d66CHrJY_z_1mDD%y!0gfVD9=KJ6EW_qw4kFO%1hBvqY_i8 z?youvk3o5Vc|Fhy7S4{gF}fl(vEt*JA>3F+)G!)MoVBO3Y%b$E^KX2nO+_K}^1(z< zUbB#Z2-K}Lp_se;ISGwQEvOZbLcOP-e-Qu2e*wKzJh{$1d!7$JEs zJg<6nZq_(s+Cis14MBq(szCs&20frc?HU25R$G;L zi?$G4_cmO?3AW;fAv;nP@w05O|J9KeI9?b$Q(v?{+mQABlYwCNb(m0r`ulN^z2Nw~ z=6U6rjh*~}ugT@BvYk1x3+X>WfzBKyKM-x=>SW#_2o1N)G+Io+yqiQfMwPp|XWF6J z{AmI^55HDm z>dZI!+6{(AY|VM^DtW|K+fJJB^YaV9ZV=emw=V7C+1S{&!HS<3mN3U|SiN~eZA-$FuTai`JnJ5xNqoAO$ zJq}cIU)Vv8Qza>T{Fjg$QG^M}<;P|~+sg&%O&fP%@U2j4cby2dr_D`Qw*6u!wj|T@ zk~yO!?(73zgUc*5)V*w4bQ`(}d;C%Si>H8{Ev>g{ zYdcLNwM+F8`9P~~bM25;VL|CZ^fzUcF!9Vzd_)vsroELEhbjM?6NjK3z254Eh`9|L zql@G;*CB;LhXabACJxQUI4$@j9a2H{LSI|kf~)cHjW59LD!U5-%BCJiku_M5fvF17 z=QQ^zD|pYzW^uET`pRu}6*2RR?dpu}`rW2)GChf0)rm%alYFl; zVXzh^KDyNqzo3QNc`w~a!arM7I6(bfuU%OYo7l{2e z+GU&|1fV^QCw8bRNET#}zzN>iD4({kudTg%SrSw|=hbsWJW*wrqR&|{Gh;A*u@Fe7 zA{Nd30o9|d(C$59`&rYfxB4hDTNmzS4w4s5tl@9-R`Y*Sxs)*w3}+^DiU`7 zEH84tmZj9=yOBE_vOqpV^!90okWI%mU&UimbSlSyeop%63uS6ch*0p z(K?YYClXoMAJiS75b+sX@7J5A$V7XK)by7UQ1987fVo3So-_8CZBARq0`}8 zPwD$(Sd;SStqXLHotv5Mv{LtgYBFZz^(DaQ0-LvaZ1ogsJqaA;^}DsxXn5q7`qlPs zk^EdR*%G+8jZ{nLVV}ruQbL~7dfc4%<&Tb8&9$yL=K8B(oz0?f-k|VbO@mC;9c|i< zbUw3JMnpw@D}K5Vl*ZJoK6!XnxG%Qfc`wIVDMg@ngv2OrIj_|Jdl+-wHotaIf(`h{ zbs#(H356PggDPkm&xH;(Ua89bNir$D*@it92!*x3u&4@jTCfq&{i{*QNT^=%noQ9GJgt| zR&GUQ1P7-dEwNW$d9J2eT!$A?SV3mp67`v#;mG_?NM36d2g%X;jvR46Vz05@Encd} zb-x^P=5Nm`&e?cSL1Fm>OU=@`UNfVxtZaz);#|fEKT*Qj^Dqt1C()a~kGMv)9})aa z(|DO{{PN43(Oy%U<0s4=F>se=n<}p5vdj7U$9iALN+OqotLJ6B8^7Mkqy?=-_I~ci z&%P(K7Ji;K7hCdoebZK(CFYOQYpY#G#ffohua2j|bS%6#%t4F!uz(fl=~oMJBE#%@ z;oQ*RC?|av^wYYPmE~lwQlGP@MtFibB=Tw?1u<2hIlrs1s1G>AiY%<0r7rzq)iNMS z*)la%Ib09MwBaYCnxo%Z06CynGX@7|*;LxR&!dK@Xje=HZ9));CuV03eYrTgJ02+4W4B#hK#ysZyVv+SQrboNw=98*$$D zzr-XuT=0U{Ft>8ilB#Y%WDZy+lqC z()Ja#lB;ZtQf{1T^R$Bu>P6i2jg7zU)1C7?Sc`f)%~8K)FLxY@fYhkCaK2ew4Tj8) z(s*x`Ch=dKk2mj#PgFU~(-^|>i)b$w(Cu;3#b3JPx3^t&_+N%@=G--8^?1*va_*ef zK=SOSTf2$SK#0|iHk{&y{A^j-$dymv@DnzttFFOWv-Z^2cMoTy>umqIu?=ar2;$wN ze7=in`Z?Z}aZ1qA2X}nOa!E!Y)ooOQO{C|Ddv7!ES*FvNwZ>UUoV`em=uZLB`)7X$xjUY+xviMMHPL_&M=z(v0cUCI?z)JqxFZZ%QP zD>pWq9z8#+JzS2t*9Nu}%CJ@o*|s*Oo7-P|Q1^zmX~D2!zo3XWUwKoI5B*t`@Odt& z(CIF9Y^2?8aeF1j*E(moA&vjlrqL_}4;A9$m?QJz4wsMTi@u~<^?syz`6Ee8yA?YV z>Zg%yH;>jPVWw0z&4a6z`@^c3jI`tfko?-+Uj8(1EF8-|TwGdJwLL{KI^v`si1=dp zke72nUU)HEf>Q1Lx!5E1l4rmZqQTt(f@A}gh#-(`0*4Xh*X=<{WMm3ix(R%3O|)5t zI`{gRokw(AxA)SRfOgLOr|IGvS#b<~{uk5pGoeY*j+@qDOznfB2o$Ti%b{r6{MUyc zB__YZQc|1b&$98|+?;$Cpy)GxBUk%@*j%oyf%^8fU)*93H#Z{smRRHLM%GrGvZRCX zb(L(hX>XpdjW6H1&9dkkQTXQcSj%Dy(c*`aN=;`>m$;hQ%UTQUN0K2RBI5izMcGa} z);f8aJIu@aBtFV{=uo8LLEML^*`vAC0d76{?dp)QnrA z*H0va1Os7-Z#Yv=v(N7JiC=8(rD6zPZlif0Pg>~DyrZXK_Q!FZJG;8@Wjt`|UK6kK zF!uypnRA>j&vH=<$ahc9+S5@m@FAW<6bhqv#!@uk%_^8-?KMobVT$LD!xN&6N)*}n z?n=(tM-2B4rnD3w+UUM4z zC&KY;ZjwE9XkSo#%)_tqpW9avyc3h)`=h~SJKV_N&O6-xJGTv{Ayp7Agx~e>>^tRd zeh8enAeM5LZset#UxS-#o4R%`KaKaafYOtR{z%sG(c#XJxCFK@b;(7Z*l!uU8EOPg zF4ck6Wr%V-y@X8OWt?g z^&PUvqo~CqwM4swXX`Bg0I!+yd-;wZtSu6OLyo)GHn-|$FE4)+9GL2ry#~0f9U^7iJg1~Y;vAH z49=QCe_5d@jXV+unwCA09XKo4eQ~hbOgr=AJL|ZMRGTJ{Zg4>|g=v^+PhYvs$&4`$ z_IE_HlnU)SxGs;*LywV!!)E-4E{<$cyYr7ov@~K&kxwJFa>QMmOh)_55$_ zqnG^cG1`@6b0V5OOTF_JKxRW#Py9m6-JBZER#Ik*f*693^yK2`S*_lutz|nwu*gp( z&d*COHnf{Jw1yp#|4BD@N%piY?@^j;a2BzBv8IyysBPps>h^I=Q%m9p%&0@~*vNk_fm zl?y>e@H3*z)%W}RkxM$$XHF{{%d2r-61nWLa6etq=%V-zL2^_6>rqQ>Q=$ECuR`A{ z?e?gxHo-c#K9)*z+5()|-LtyxM?;32okmAPPCv*L7{;T&c1WD8KGquRQ&K|JA|_WY z?vR)E*PNSIkZhE^q~`Srkb@$E0|syI_|tdn4ohs*kEDC=d|UZ4;EiR>@?~>m%yX4j zaj2N5?TOuTh~D_c$NEsBEuRWP0|iaXFH=vZ-8{w_+%Y*xC+Pz~A=s99h`!M9%QHax zC#9CZKJ0TXzSpt8*`{~WJ1Q1_mH74pEeEaxq=OowvlP9fS-3>o#)hV10><1UdSW** z;ls$}?3@0xTysc+=ysts^vEiMn4q((w={nv-`M?FB@|}G`)T||^NQ9%29Y17*tzWc z9pi)VPr2&Q65(5NOtkl3ZE<|`w7sUDu`jNIdu~-Zf zKenzgUp1?jZFW$sB?&a55Lm6xykOhDE*~ylq}A7N@{(&)MvGNwq#}MiZ$7~-$Q)y| z!klSSd=hBXT-l-3Q@&W`DbD@9dfaa`Xevq^p=#7q3|{TAaJgc{#kZI;~$CUu8nKP#R}mnV%$OspCW zb=?Y;T#%U#Q{TZg*}M zN`}vV*WSf-SP1fCKiK(+sn|K2(D}5&&*I&h7?L#|n`5DDBt?kz+QyTcm&WY}oqW%$ zF?SMPo}d!t<~ddH)`Zqkv{Kj<(YX}1CpEt*Xm4O55M6$&?w-tMJiZWx*pDLOb#Lt% z4B;i5yC0iLc*J5PY0z|r1I4!=89X?;`flY;lBj3dWG$^0j>iw}FVRK-8 z^FueSpt0iB>XVCCcz9NU3Tc!lXpKZiv*wn2z8@ac)WHjut?4dkqQo=SSeB4JmJQE;I=TN-!R*4apXhG4ZbQ@bNNLV%JcK@s>~_VRfsb=B}Ca@~-T zo*G5d=pt~3+l7$P5WaU&M1=RFLPlRHLgsy|y^ZE4D-4veIM?daqZ(DH-S#^`9km6B zOwGF-)i{Y}9SvRRgqDm^*vZGMIM(he%hrXh-Vy9>tq zdP-6!TQ2=|LKq_xb2jIO@YtL%2RR(tJ9tdFcXC7CQG~>b%@IFR%~Fi)@60}Nb<>Tj zKYNZ@HQ>&3nOI zHFIcnL;=l|0Z5p_FG^P|29AZBi60HjAjTftwTwcfc44%m6?-cS#lhtnsLjlFBPmov zU~ry|5hd$6s< zBX2?%9M{3P-aBjI0oMhBC`RNPK|6udfw|jYF55D7ylIXaefbQyS&IAg^$w)Doc89l z!yTw@O#_$UVf!-@yYbNB)>gBZkU6(RZ@=rrJ_r%gd8C`Bl+k-)c2xub{8m6iAqWBL zpSAU*kKs3;u(@jR%h>!pgRl0(jY7!IPW&(9Xs7*ej+@(dEr#NRumvV4540v((ea$M z5naDL)h4goM1B}on8${MabK=Qg;<-4uO>V&L`Po%_deKoINML$${YMGI^H$@A-UDu zhI4b|*zo<}24e`tW$BI_QkJui^$LA0VtZyQ0a#2Kqjrt@Yw!yIveXt%U^j6OS?kmqpJR&gb- zyF2YvGyLmjcQeJX->JN}`7>~QL!6hcrE>U~msZsaA{&YBr-(e@Mu**v`aZ!?u`#dh zwD|{dUf)^pm0IpnJ4UeU%cbgxpv9iu-#incwJ#edv#lsjEhzw+jf-yzug-i-Z_Ix@ ztv}60RyWioyS!sr=J(+Y{57%{$itvJWU3lQ+75iF3;x%u{sxGBA}0-`sDlQG8~HiU z(KeXK#Ng^|o4d~s6JL?Ot2lV?@5nE|zFnQ{FhAV4!Tz&zD=&%2AMRUya~!_fp(zI+ z*UQq|tQaM*ekbSX-L~+=+02^5$k)Dd?9uC)ISK9YleL{)zjqXZcKnVe94FwhtL#@; zXs)gR{#(K*AxgCUL3EW32OWKZM<7yn!B74OM>UaU?eAKKi*sVJRFG&`_A-rKPxY`l zQ{HXI!m>;|?sJIG%W~U&rEb$fSJu$wC0?SEX;)zBh)9Df`aW8v>^NnlCE!8XhXzgz ztR++u>z(V^!tfX_pvpoJ-4ZD{XIb2QeXg9sg0ZlBe}OFDCJhN$8|)SzG1I=0067}f zo~93_`tF5vc}XTweb>9!(Z7?QX9z5*g6#IO;c~ynH^E6_hg4Dd0~b~Ki{3H^zk?Jw z<;S|a=|C`YTK$6dPlF{pC?MlmQ&ww$Yc7%bW0)A!&-|>alGiK~_OY0g-A((6wK0ff z(ooaEH?Rfa7Skb*iYVb3wZjg}Skutas8KftnSw}!w5rRVHbe3@?e8|G*<*j;jIIma z^S|0}{uDuSHFg~9LU_uCb%rW(l8wdHV^gbWy0p`HVkp>mShopxIol)+`-Ob)$_l89 zD#mA`8L2rKgEO48d#N*{bcY`V&LJsj>1tY-e`uuR28tzKI2z+9rUqDX6#ffaw@o`? zcGx@5#mFXWs+@Ku>jtn5_`W6rF7p6RA4Po?Z<352c$5z_w%tyaspw;aiJSWHEOte) zj#1!<2&l&uRmK$kYCDhNU`6PkE+f!g1$S_rjWO~_uz5>gyi-AK-M84KpYD^8V=Q$$y%z=%CK-U!&g)hrqpH)k>rI?nD zWg*cjRad3=&1O%_y??08(!*LrF`s9w~HkBD}F8pOaNPZKIy0(N-_u-fI8+`p63rxg)Q)BowP{QMRJ zN2(Rl6=9;aZ7SD?^YU98$MXX4W+;A29 z5Gjl>x6u9;0JYPn?~lj)z+wMaD;E)k7ffdbg(`-AxW>rN4+uo6Tb_++suh4`chUHo z%jA>BJJS=8*6h}P5O9ytwR#q{ytGtv;>Nid`Uih9zz@I&!3U-`C_}O>g6|KLQc`R@ zJv{|n6oY!-HTM~MSlCUC1O>iGZ)qzotccdKE4^#ln^uj5!%`#j2fHZ$8f0%$Mo$JP z2|7@;9?4ZRh5=F|5Y0X$T1NwBx%1HVD0=Z@xK*lirq)AMqyn74YIJrLa_h%o3(841kg4+G#yk|vsL6Ib+AqFj=56f_dJg2X` zdBox#@o~eVW*|w#w2xRg#Hi6IsJ!d8F!;agfp6D)ntVkU!+>P%!I~#h6+*ODnZEhN zR!J#xbbP$t5G67)@{n9U)-*xk%fNbGOMIUKYB4eSUYp@z@^h4Tt^%sszwB>~6VQTL z#$G=Hd3wZon-(Kqpy-W=DA52=XY|6!9+2#bd4EcSEdFSLneuS;fiwp1(qmZizgsD~ z-Aa5)8hwl~k~om-5cIf%D@HNYxm#|sm91D6m@rQl0%%gaG0e~#F_Kq12E6~k!Jwb9 z2*~9NnQQ4|w2(3cT8MEh)4f3?#zU)Ub}@zlSP;TzH>v{{ zE$C=(XbKjup?f=0=ujL~^0q9DZ7!rorThM!o18_zj zUDiY%`ybkb0PXvC5)1#@|IhjIZh&s*`Mvy&v-!iVbQd|MCwZ@5}U+BR}Kx|W$!*7ZA3e0Flln8T$Yq&9TEDe*;@qN=Jf z)CbAQ6m5-YZwxWqe;W~9Oex^Ld5;Iwoq*m2$W_qyh|1CyW72|&PtVQVb0`R?;TX7e zh*Ihpn0mjqbQD7kvM!ex!2t}bOO;Ut$T`>$`CH1%T>|Ydgc1@H$y~qn zSI!vYwE8Uq@OKmMp+IDDmVs*6r<}-K$i?XO!Tv$e{#w5aH!eMdMZUl#?9*?Dw9W!D zrlXFgFqcJuzQp1~M=LXEYbYrh2#*Latf`p@$HKPF806Q|ZQ}UWiYVa&Q}GMl7Ak9< z@eA_ZD5b-|VR;mrresW)iJtuRU)O+$VgdxXS6!a~LB&`hVc`K`R7MiHF+2P7lc!IE zI^MrTM}JU2Ir})s+;dq{m5vyN{z+kB;aAjq#4(+?Ubwj(2#u(`+w9p*|4IYYk3m4b zb{BFCw5?g0V6ruSt{=oDYD|jJ>X4AFOA-@Enb{u!*_S4y?;$NQ3oC1-xNo-som49Z zE~9F|r$pv2Ni2B3)e^)7c)U_N3^AC%`t9K8Y5|G8*?hVK zoZ$r$H9j8u>rJf?6x5-u$FQ2;`$7ZsyAu|qqzP0HO_8Xv@oaIK?r-Th2EafaGrHKn zSA$sqCU*E*#9IadN=zoNaXTQK)-wE$Co~4$UBLu62-N~GN>sTMb6)+3fk;Iy`vQOV z{RE&UpVub&9B4|7qD*1qZHeTxo1mg&U}aCmW{dv2D+J(z1DKss$jR;Pn;Pi{3OYr~ zR}3u2jo)zqBRdXcFHDk9cB=Y_Y}+C^-E#(|z3|3eXhFx^^^pvNjQXRC0=y?91`Mos+q*+#i3rc%PLV)kB##%89_&LI+=5*}G zm?el&faqJ31Op{g>4k^=>vzm5fcW-W{8EGF78!5J_9!MT%?@2D*F~3T z{kuulM^M91;cf5epS=(K&H)2J#dm<(W6g6!$!)&JFnAw28dgzmF3TD}NEJ3t}<^Pzb|0=nPrv=*Ygq^XyEE;BrlDg;;*4ln(McY(e1w z8`qL)_3s6M(6E7g&f(OCMN(Z7O4NmwZu4HMaTOW;uAcXJ-SbWWG4!@U2L{U=*hFjAlzGc;nb6Cz+3}eAS%41zXMiJSu zCg;3=6Z(l*^j{Dw4dLFrx|ik2mH;lGO4+TF8+{DtvWn8ualRFGGg=OXRpA>k!GYs{ z(o8`4hIHid(9F^C zcW=rf2d5?_jO&0y4vw-Jsw-XkzZ_5is1AbFB%jQ)1|s(9)8;>E<^#kR@S zlY=B=RxqB4V0>H)NKo?QzCAI)K>x~irTbP1OB0%x5 zG{yy{3WeCVZ+5N*`h0)(ocP?WbcfF%( z47?!Iy5sXAxHsw1D;TJb>QVP^wFFXVW)!B^?B&=|8MlzD?kdd@JyX-HO|hAj)V!UB^(0iez%`fLb?BN<|<~2EA@g!#q!1o*r5tU8c}0#175= zdYj;9cFgs2lvFkd*Jy_6>N+WhJW;j#YbLL{@o9_6bVX%51q1~R1J7*56Q)N*-q~V0 zFZ!CUM&|i|DeS$l8DsEFDg&|#az2nF|JrfPH>3|=Jhh&wJK!pUlD=6WL7_O}h=F0~ zK|4~J=U(HcbV2mFf4{AFmo1<~azJ34r6k5yPLMMYy_S92JWrhMfDyNg^Q0svs&OF#}PpO$DV6 z+x!$0bo9qUc@0~P_`Z6)!a)Q0(Tbjt|F_6)&jT_JaQP?0{q4MgA`R*PXQN}VkN~W4 zsnu}Kq|x<$qN9^ZBbi|*(JwK{=K^Pc>HOyIKb*J2o&j)A$CE+$RsH_gd<8{CcHYaz z>p;2NlsVN=DrM8{e!s-kx{7@>J^-)$+Ca*oXBJB9S97)2ArW3H`p&_Awr+IJ|HfCp z%2wAyuyhWaYYFq0Q2)At-G-$qRKM%;jLDwj1`~YIJ($?_>7rdR6}zVNb!i8UjZ0(H zQl4z{vG<(ks;;N!nerLfYyHFV`+O`;9>}&HM0I0`eLMzb{h_& zd}};PsR8^ATmyh1+9W<59~8CdOSPImZ5K39U4S&-fFrKNe|#nquo+_)SxGY8`b?x^ zTl)In_ksp4fZNOAwW%WRPeH3aUVzu{Ix_+d^#a?O8YlOgtCQ*WNXmMRI*n2PRwx)OiWmg z8@d!JWr)u*DJ7ecfn8$@4ukN;C)T?=QdSpQgSG(xmg~jEH+=s!8i$&1Xv2^F6K~d> zwc$~uD^VglB|&(?ozBkA?1HC42VWmsBG4hMapdF8#Ikmt=+>nFZ_XA2W7n0D$jq8r;)jzX8BY zfDu5ABqKF9b0Wee9v>OM3S5y)5;6_p z=i{sK!NX(Hxm={Z{)zv^mc0akH2g}Ax1hG#bVBO6yKKMmN6-+eb6gX;`@fm)ak2kZ z4{Is_H{Nmj{y9wGo~3+$x>(08+?P_q&u8XIT9>-^X>lX)eiwl21TPwjGy)g`up{gq zs>foa;C8YNyi^)b=rf<7v2W)7k*^-7GP`brR?HF-4z=?ky3%gW}STb(pKYI+ktU%f`M!kYTb21g`&~(M`KZ&s^gAM>K%XwV!5vDludH z5w!t$YwYK7#BQsQaubt33p=13j6&V=b{hXnCvC7&%eY?ML7#{S75Une^kgHD_MZR# za7K+Mk}I}!{L?msOKNOAwyZf?0o$bd??#4(a~lmPnwpxNy8zIC;ueE@F|#Ue0laGA z3}GIyqZo&Sr^h&TfVAl+;aT<;)j)eR3&_v^-Z8=^Z{qoQ=BM0jAm{I@7fJnZVQEE8 zO@7R&z%@5*Zqqh>bF8R**Z<~1%fP@k@n&_I(`Yu{xFrD5eN^78BeGNpY;ss2&f z$$`UQLX?=^tJd(go%#Rp_SRuhZr}Sb3`2JbN=t*3N;jxThtgfrEu8}jC?efmN_RJk zB8{}Pba&^w$2jMFzQ6xpuenB%nP)%Gj0|w|6;m6qD`p!*AH%wzi9?#y>JJBHr;W8+6a>~xYEHv^ zFC{}po+}*jDcNTM(lncPi>-Hvv!3S`w|0|G_sk>2kw@}++EJvp8#7ssfEnH z{}9-Xs%$>MT-S>+7ho%zpg#LiX|>bW zfPdj&d_~)xzJ1j^){5)18}RM1ps;$tO>er_O0v^?VPHmGI_cxam*Vud0~v*mhM(Ea zARfININ-|a7zcFO2XK=8R&0`;gXzu0W0HhGqDKrj0xi^{mo9}TpD^A3Mhob~-z+gy zB%G!q66y)mP;}9$+?{h`Vq$-kp<*e1wf$B>2oYEW_+frQfz95&{PFOVWy5^I1Az#_ zq+bzAC8LtI_Quz-nwr%5`2xfel(Oq58w$vhK{yqWk@G1$TXLyg+84Xn+bQ?4X9mn_ z{GpbYu5`>TOWkqW78ZH%VwYuxL~~BS2F1?UWzD_!-IXD?prGRNQV|JBi+9vh1efyH zM674Gzd7hGs&@>b5HWtb>;7#18pjM3W~tJTad`y$!ei`}u_bI8kJ;$j!L>Hd=-Xdk z`X{s*R0ElcbV<{(pyr!7_0L{ox~?|P4SkDwfo?C2g*;Ggkg+qzMyz7gU2mHM%Zu`z zS&ct400J@sp5NW~{sMvZ>|%Vm8-a^|P5-Z@3f{yf;OO?Gq*U9#Ue}+6R1Zfq$s&>d zco_dgqZX{dTeHEXK$iMHo>%$?*zW3W<45^TG-K&knu#oWC4T%dxA=2;693?%DgQv;z7e29wlV! z*?k6E|JM;TiElk;8&Ok>jxwzJP+tbZt#*}8zmxKEe{pD=qMkx5HdVM4W~@!GpVqU- ze~iShH5o4EY?I|(UN+&fmbNj0Y)KY=UmZkv%aoocsMKIbe}ZjYX*R19(?;-|;y>PH z4}sgy6mA|zgIMBav!n?qK#Dd_rN&}g#VVhEZR~UOML3i)7~B%2iW_`k^0(FO0NAE&Yg>HL*_CNAei7!g!M(UzV>@IO$3&0| z3J&)V4vGLo6nM;0+9ahqX%psaj@wc`SgR~vB7FHAK=yxjM`}sr8Li6BYx@lyGBp*8 zs35btYB8`Eyr)FE{Z^tXMT{LMfq8(ORLKq|#CngZbbkIW8F%i?+kKYJfG-7`ORi%R zhv-N)m%;Zpo~htH!cTZd_n@E1GW6fLqZlL>UJ#EsS9?gtG*y^qadW@6o)~Z>6XxSM zz1FvW{P?jNUM2F~Ai``bU1rxYIL_J2QB>2{*9Vt1P%_*_r5PSDz;4rj|KS6}q+m%` z^>dyd6Gz0=9`YF@yvtg&t`FokjOG6`=k6;~pIX`>*M-KKaq~3me+xXUn#(k3hI=NU zXC6k23BQp}06s3YWLQg@#-s1z&QvPxO3Z?!M(LtB?(W>9E!i`7$XIns#SnipBmX{3 zcAVdxtMl)!Uf8swCqBq1hq*J5x;h^wMB!IM8{ZhJE1@U^8wqN4XYMG69b%m=qg3wz z(pUgYLtr|ixbZ-gnCSnB{Sj}jL>Q&SVL~KmN*sv2Tp!ZK4lW_R2VpsiMNAg=L1)lO za)<5@Q-vgxPuEoPtK>e-ynech`-F+eQ4|9M4#S}guC}T?|2v)tuy-U%iiqB`W3FDV zyybI{bau$PGXQFq(K{bdVj$3Rwo2n|Gw)hF;_ATg7bQ(}0yVE*N`9>fjF|sf^J_He zJI6AbiR$+R8HL)?)654@u?MqwqVNBJLvC!z3}+XjTOvllPt52p})g$565l`a2yM)=36^$=|}ZkLe$Pe2cEcpwDy+F@pZ zwCbNJ{*S>5R`<(*?!7G&fj_7p;jUBoX~+BW(+n9c(S3KRn*Qt~6EaKl@H7rywb;f3F3$xXfx^iM09C(`yYtA8X|GwUf7n#0z07xL8MF1? zI{f^kj{NLX%|YH$D3z~GXpx4#g+9MS;}U8cVxdd*Je;*4jCh63T2fpyJ6m1a{aWy9 z0Kaapg z^B^$Ij`h1CyOoct>7D!C$7xR_g?-K@jHCLTI_>M%C$?o{7LR!z0k9N`BCy=9^=sD< zgdXN_5ar`G?~Z8ODpbc1FSVOK(}`d3hXC+^1n^kj*j?1R#J*tN=q~YR4?`A*eGylh z8|09yh|ILU=%5YRT9!%*Kze<~4^y7E4VZ^D9ZQF#`{}UrIrtxOuvYpCRS#QQCY7X`){`7bA;5+S&9A+bNWQC+oXKb*T=x^9~0DHQgL<|yCs5+ z0To78Qw{ROcr;qV&!=Q^lsY&WPv*XO zx;&UTHv6S`zh{C57}`x!_|Z&r=<$~{PT$BPmRZl<}7^#G(AJn~&S zp}T%!UFZ)_AVc>4kFs~oaD=-F+N3G}cY;(h;CXF1-DqjgM4a3fGAQnN#c=1PI4qbS zd1C*WeY(Cy20ppD>>E=seWM`FTxr4gnb3+Ze^QGUl5e9&>GzF zpwpnf&$MfZ8iZS^2K7fO>fHUc{=jeAX@L!cSy&CyurdaHDcl$h~q~ULJj-< zb?O0h&~GVX#((gYdE#Ik46hT+25o6&BauK3IzGIep9bE4Y(p^K(;nW)U@Z4v&Mau2 z4H-m+A!^SP{UJGz`PtOh=UJ)9Pdkidi#`;$y`*yZN8V-tAHJ@YjlklBI|>5Kcoamn zhnp7YxEQCI%zOhSh2PL{-0*)pb|9*xg|Cjxc7fk_`BEZXFaXl*21RXz{&VBX@GHg` z{RvX$@q)218Y@haQe3BON5@U7oANWhC_3A$G+d(6qG&BH9f&Lql*PuR7}p3jzcWO3 zAb+6tyJx7MAv->(njik{48Tm#0j|k)t=Llb^##ZHyo=M3Fs92y%IKu?BdUhQ!%jiA zjc;TeO%)S>ZWCi_W>!^y4s<4J4#I9*ow;kq#*CbM6uoGL{OPgPBk&*|S2Q2|e?7@G z8&HQp-TB`CVl&YURL`nuYFdnpjHrRqW#Ca)_D$HFsr}HH$e9n_RJV#kv2hK(1|`4U zAm#AslLRX>Ta}2ZDLlR#&F)02@JEo~X@jv_RB*v9E-2-KgXud$#lydHIO##q;kgqS zEHH4n0cq7)O|9ChvcREu2C5aSP5f{;z*kp#oh@BVOhMtj)hVm5FfPW=;|y=^xI9)* zL+!tJ-wVI{gYP-EzZwu`Nq^#DJMi=b`y?<>f`mv(vZ=Uu1|xsH*Ro!%J=uUAHkj@T zG##2T(adW>ws2iIJ*|)@@b*iae(S$3OA|c%Z^s`w{7YN|hz@uy=uDr-I0i@^;h8r( z&U(?efE*%5b+oH6C*H$rS=o^3aKKIxyEqX>-4F2I@cuyZG+`*f|F`QL?g(yBWjkDn z8h*#Hm$!BaFY9I?;#s<3B4X(t+{n5Y8v*<%iE*xF4PxQ?3c$2RM{6iMD08+MGT3KZyAwdm&Bfx2Y>4rX32gqTMowNU-f#?uYs!BNO>4 zwYD+p;|QOx_}uoAUPx`SW@33@;@> z2QvnmUvzSPqyX+$Hy@9i$MbS^0H?_HWZHEy?fQ`MqUGj7 zH14sBOZCy#GMuGaS-89+c%U)f&lz6JY<& zEwpxKV3QR%)$DG$fuirJC=K;x;JH`5)j-WcAJZQar{`heHUv=7p(Y=I>1}$>Ug(`($$2`$kqR?WPx}w5N$3Ae zXVCaR}uTf^!4rFfYVm(!utx~!!#U?YO}Ra&-&~g=)W4f0CEDqWHoR_F9%R? zZ(B2_+GlP7Q2ghTM`mk#%gvRYCjg*r_7^(=Ec&rd50lDk7sxiXA=~OAOWOi)n77?< z*89YHXKn2KXaf$!RS+r)eFy3bq&}iVmZh2%siifu?vvGaliMDu$qrv{|4b#(Qf{Js z(VJ*mvC>mrq+Knr@}OU62$1Wx63kM|fV{v2z`FzXZK|wFnnoY?%vROXQ5y#{1=#|>d1mhikf8rzx+FxHQ-Ef6nB$>oI&4(C0aHnwV+&{7Q5&jI7q z^kvyHn^k_&TPx&*R{x$><+tkt(VHQ7>BLxWGdmi=l*0wZPu1d~_TdI~ttXrN-5fQ$P@~~N6)qE zjB?gAaywr6lga=vaCY;g<)+EA7~%D@-+&jErsg)A&wuloa5c37V*^Hl8FNK3=wKJCEVYeG{U8nF%(yx{-%aaqKNP&U(CM=PPKgWcFjC z%F1SQyzEzQ0$|1#BW+U{W~iaA^7*-x$m{$W2>y2t6-BQqE#@DmT{dSTk$xA)<=!1x zNCy}pQ7tKcPk;Jh7S+|a;`((K?K;QTp=7*$dOl~S4M2`&6QmgIH>@antHP5eFZIvB zkCiu9vp3G-L1TKJy@9xPsbk_B5&vUVVH&`s8s+WkB|)0YE*76DCiocTOuJ0>o*!-1 z4BeF%;`2Nv9}AwbFl;B=DguV9|HrCw*7Tysojau_odga0U98iKuj@VZw6yL-Xm@{X zR`!9H&H#+KpY1vAtiHD>7~*MNqDwWOD~q8-Pns#uS?^;*JaYHN2q_&Uu??^_jRUw} zl7w`C?yZQ0w)0XLmVxGjM^f^n3;&*4& zfk&?9)J$`)|7<juTqK{%(dRyFqdmurZBgOhwa#u0x-X3 zoc;W<_Qo2<^>T00bpKul)eeuUL1iYtsn3$%B5; zov+udV(Cwsk2C5m-9C-ATs!lA&)^xrLz-6G14z4FSyLc3)8k_tH$d#)@ zNc0<@Av_U}qJ3|!$s5&uEo1aV?joK<(y~adKX_WY6L=jE4zEi#`vt#cOCT*A#s%TN>x%j#mnhuw}h*L3LrPP-<5c)Ti`&#Feq$n zOj~mFc<$5FmgcD#8ns{-3JM+Fk`l5ZRk9R`5 zVZpDZ_viMOoY45F$92~ws3)_l&!kiw3`g>gryzQGoHf)H!#b_qaxuM=N%C61WVWNb?A@TqWelaa%}H$l^j@7gktBQ>1jzyD`K0`dRlUkRtHEq( z7ht~mU?RS27-1|nfs{;Q!oK^FVJ4Ax%yLVNm1nkTr$JDwwLQH zBu%KFgoMPYZ9yd95M_Fd(k$ioVlgac#^=K3Ft#!(yg2u+FW}uX*G~t=1 z81%Fdq$ZojT6}#^a`i0-W`P#Qc2x^&KpJi90N^dun(*^OFLT^9#D!l&> z@#$%_-MW^vN$WHVg|`^;{pQG?Q?L^SJ)yO!^b8!%FE z4lsnVlm`YX;SqhK=fiu`?gX6&Y%M%SjSnT;WTQp85u@22iNY=VEsOF(x#;Ja9b2x4 zwWnwJ45;eUSwo-ujt=RWJFu-GIrDf%Jz>3a#erTPwtlf+8>>yq;C1n$+f0g$`a*~EY+wC zOWHE6l2T^X44LiYfA<~-wAF?ul9B-<$g_{%?n}4~iqIbqkfqguethI?H+UH+nfN+nK zU4b&(k!b?^K|MMIm;W%ly4>73;l<`y?Nw&(8FD4emgMvx?Pdy|$P@heif~F737)@@9O7$J@JT?kn|4l&EE$Av6V5Ovu^$RI`Yf6Q4ETXlGq7%v0%{d_YFn4kEMpxqk0rTl3jM z*GbID=#Qz#1p(~o0aN_>Hya5u<@z}%q2)&pT1TzFn--yd}ox1D%n7fh8_jq2%Y znM>{Pvczi}XbAmqF`tgv04gjeY^8aU`8GAW4}-)Lj?jHxZkTzW9L^TqU#iHKG%t*< zhWL6Ll=YJ8n|mtWUiP@%VaZEZ?}$bJw9=fTUs0cL@_cZuVYue&Kt#GE$;XI7SoZa^0e1sZk{mRHHR@=$R(`P-ZyE`}WCb9^kS zZw}Zwk?AKNk4tt``T2WEbiB^y^9VQY`iWJv_Ql2Z)yX;F0Ir18P4u>#jIV8mZf6EaS)at&zl9%?nk7 z^tz8?YCRzjEF@&O@Smb1g6fcV!*sm0drDM~I)Ubzir`sx)KJ}>=zo$rtKokBNc{Zu z&EsA*AHIb?95!hAzJ$KUk9V7hRP-LH2p$xwgLjuyBz-1loj#BHvFpo<_l30m_^YAt zxOPD~M`4C6NuO_mKLBqqsJkkFQKbXgr_^a0T z1|#Q|4PSei(?Pz14c++!@L(;RR+-H?@ZhvvoV%*Ol5M}=sEMKT*f>l~yK$Z?sD#)A zL{DEYNZtOEcDsl!Is*y*Lc7^!hv_JnpPycX3TPogkScywtw(fk50c7J%%wZVEPtu~L4$t-cj=QRjJ~4UMGX^f)x1taLEe(bV@o z8aYnr_xdnx<8zgy53e(}u~hmXV`*tw{w?h}v${ZZR#t~FzO(deLO!bt@Ek!)AYuB@mbH=xI1Vgn}>~ z)VC*V6p$e+2<<_;yZd)`DdRZww>?PSk@J^>I{^HwNq9ycXtvq7PLHb90&y9ePp{B{ zT;;n_wYA*~IO}8+kP$wrs;dKqy{+Zyi^V8KYk(`%uol;TLczW>{IhY;`AeV2SEs24 z%dCY%AX*deQQ-wN1&^yjnqS+^6~aTs$L>}RW)G6K(81`l}e+__WlWj$YX0jlBS$~z+bf5&`yveat!VmknM)KvCv z0@Q;B12JV~d~ukDUt^FMFPz!QusTyo#;pUwab3@o6j$rLNO~TpWjVq5U|MTHCi!W} zu2I^$+@EF_#i)pWr`h;d;19y>`hxi&N{?JCtRQuq)ZC0K5DC_Qjhzt%oHfCR?>&H6 zX|b}f`~Kp$%}@PfMcQTnUMS2Hs{iEM!`1OB?y<=Cm^rxaB3$xpW7Kz(t7GDDZAiPv zNl8!77GCcQSAy$OWwlxAiurl(gIt0UAj?UeR;9kA>%T>|>6U>jiyaa#;GEBd6i#6NRm9v$-NK1w{g9_`&Gz|?iy4R1!^lRXxpphE@q`2xoshoq0?Hz55HI&1Pn?Ef490G8NEg%La z6|}6o09D0{$8419g&KxoLi@pjJ{WYXqJY<>xgiGA8o2&40hn?KCLOShsKnh*AwVQ7j*m|IA=I6`+7yUcNuIV(Eg1^{uzp zdG?~5n2zaNjsGR&#QRD2p(Kc{wu$F_bx%A_-ZsJI9gl&oK~l8a#R}TVzi6-~s*gnWFI0bHRaW`7?{`mK(VjSHZ{%wg(r`3&-?=1`dy{i^k%o!Okutk(2fC0Cc@TMmXKR) zk!hEUjESECmDQ=q5y$tWX4YpiQO?%~@cpV1E&+RQsblO$V~)}R=m28wbI!9WI#J9_ zA9;FtH7;}N$&WhR=(vB0aRhvp&c)6sQ$RrUw+Wl8HiVvh2Jl$cj(mYC%6rK(*oaqw z3IaPcV7Q&~s2Q*%m%zMvpvmoVvtCfL2)xs1FyWMEeZ9{Qzk3_%Zk<$Zt#TY+Gl)6Nj#3yT`~lX+#zVe{s3=-Mq)gNS7_LW^;DZOxK6Xe# zqIl8N+M6N<2URr7-QH;H+sS2upj6LY3fbxbYdaHJL8k_@=)&QHJH)Uz#j5FYe$1k3 zeagIOa#IGk5ocWrBWs8@2a<&36XFL9=K&9n-fBeVae>mp8 z?t01~f$tcSxam&;RlA!@OGM=Mv-!aZ(>79VdU{yKb54Jk^!^ODvM#VOFp1Vrm2DuC zK37+_8jUw0gAFbL)N+I!b(!>9M1g;aXyqz&p}CskgjTQgZ9L#WcGhC0Cp1t(5#&jV+Q!MB$>RMbwbl8Qz1cXiu{<>x`h(>yULK@>8;PIIG zaie(-!uw}nj3^BF3vbFketxra>t47pGP2er=9aC6rbU#Cp<`(08`a$`AX7cI z@diWfp*Cy$X(D>M6=g0}PiCk}YjOwlc0pD#WCNuZ-TFuQV-PW9YmeEgq))t1#j4dC z8g$j)*Qqqm7##Ww1~*U+I4d-isv7<{yxX{ua)F&>A)4DCu^4OB2+2Z&Oh_8tkYn^u(v ztABW-gm&A3jmm)nj!we~$|=qP5EL1_0b5uZ2`pN|)X=|$JBz3^_O<7=qVIW>T5|ltnoNdaG-R2S=t=4GjBo^W_)9DuZBBF3 zO`O^OxlY=5ss3F;@PC|WN?dqYUHEEWpDBvcv&NPT5N648G(Epq{4A>9mB=$(Phce+ zvaqXeS7cCii(!(=`!HhiUg-oj397463e&uh5wcwK-jmy+KZz0^Iz*;wv;|QtUltMT zA2dF#G~6Gu3N^T|qoR^Y zj8{Bn>`W2m3)ERC-@fXV`Ce|hBx6&JQxL$I>L#Jhp)qv6Uzg)^LeG)?dLHc-Zs~xZ zBZBiIEcC)`ZRCPgB?808Gc2`=g>Mm|5w8f^Q#9Bxr&8=T$|R0XhKsa$Ykp9VBCJ25 zK_s9CFPIXI*kiHWL+FM$1PAgQYgb|S4T4vH9J}U3dy9ZpLMlKqDjD2q(C+k1{XDQH zZpUxUceFwtGd3Narn~L_>G_>W0 zD=`?ZgMavqyetrH<_qK6c+8HjU=6^Sjpu5WFc8)<1|c}AIP6nTC*qtbGboKcT} zkF7s}Im-@K*<&>1VQW7X|$Zw2b*Q zy0!A69~2{^6|I~l{4$q+jOGk9b8FhGdPW`6+1V+tM$d)b*l$WdvWVGY%Y*>D`9fU7 zk+bg|lp|436j=+FwG?+a6N32p@8)|F6jpwo_5AWSCN#43kc{<{+FImL&YO7%q4??B zhA0}zz}4WLQ72CBn&NNgi66N+y!yJkqkxiFr&*f!vJf&Wgo(U$fJLM8N&VZm7~Xu^ znlc6_$V|89qHtgms8^gyU+CYSJaf;rofLn0@eJHlj}*Hqr`mH95EM<}yrC6$0DV)!m)MgdC27 zh=X6zhBigNJMFnl!+y=|MxqA|DgceTjlMXlUScU?OCa=wOku>}6I zOVQ|6Iy*CA)_2xh<+(<-dP6UL2Ze9)%zd1Q?`27dJ$W20myW_~+!njn?NN%~)>R9R zGU#PvSHU@^hhga3yFWjtbb>mKJftr7`*FPJTyFrR<6G3b@w24GuN}%v-W)s`#8l zWKBtlx?Th-j;k!Pj!bE)3XmoogBI0^W~O# zc{bJ#%WEE#jEDrKys&Z7hr^~SI=-Q@nH-)a za;Edn;xk{c^h2jO5*t3`IIYrJ4+Xm{nCeJO>5i#P zPU{X$aWQT1*h~~|cVjh8iaD9`#)UOFX(0Q>$hbeQyEm&ux8WkynWB#65;we1cE)<# zwSUyGo#7(`UXkz*pWECHToU2Jh!%A+nne^8DlLZ7A!qd$@j ziq=>gq1RLpE>7|iv1@2N++(O)x||tgrd3w&Nn0pCwzjB^mj5DFd-yzl#Azg7RbcR< zJg57m5JKTS;}TEbr?OEFzR~y=?)P--pKC*@5$00;g|#-^rW`t6t3J4z-m%w&C5SA# zn<|9)g_eFY?<}V&OfM^kdvJ4%kSEN3s?EL%v+P+YFFKJKj}D)0_XGkby$_lhbJF^F z!Bg5%qOeieJ88;CTGTWm-wQihi65^~FyJk0uB=*BI+)(CHNZ_=fN~EE=_@?TdpZ<= zTtUM-Wgk$iD!UD#d4A-l`h`;*kt4SjAv}AL?yBzw9tHS86AAptdNR3Rd!#Gmh&>Y# z0>*oQ2V(Yajlt?5#5C2BKbe!IL(MkOhIS>bAdhqxS891)0%3Cf=VNqiNa=WMh+{MY zunCWU`wix)2!+UU*6(hL*kWZFc55o!jf7;u6}2EMDO0l7v+86^*@l_|?}>eU&AA7( z5}S3UN;qfTxVV4$V81-E@wa};SmaE3;Q>KbCI))-MHBsBBN5P{VPdnkc`ACkjK~+= zRT2a(qS14B=| zV=sOPjI+RtR6Xu3jB{H^E#+Uw0HWAx$ffkrg)aS`c$)Gg1UC1;?9G2J>jKRsMQdO;el1OHjlMcd=7A5bx&i;i zVteb!pHSi7Pm4h^9{at84Xopif{FIqz4_0(zYO?g`uq#S|9<1ozwxx-4HaD**!;iW zdrF-P|KAGzycsR#+qm7Q;l&bBp{yWHg!IWNqz+7mOWdSswd8%?)@! z;At7tp%3zXSmhNJN9~oqH`n)FrZmp~95MWL~+HZ{`>piWYB31CH0Kq6Ilv`~|9 z93shd0*^~CwqFq{gd=0#dZpB>LEJ!?Zq&N%iZH48Z+GA;q<{GE1gPcmPj_8+kY~-_ z0FYZ&S2yLFoQ#aMr1?C5%Il!l=5Cqyg^T<3*~(I82z%rkLdI%l4n>j{AZlFh-u=S; zvaEw%K;ZJ7udIwrrHVm&F#dgj(<%01Z7lRoqaGa{)!T3xj=LI8GZ%V=KtJt_Wq)Dd zLi~5$f@$-RK0GFdeuLL~pk!h~>qnV!(B+2y%^=|>9EEV)D5~LU`W8g1U2Po!cqrm8 z`V+Q3t2WbnZ&2NYe@yj~VPRqMZs2^Rvh$-tYRm+bnbN4x-l%8QT^J0ci>qjAGCQsG z9{9HPL_jCRz|iuvF68{TuYby>ONCK7g6g!+7fVJ1WyVrxpmi4D3#D|RfEr9Up_L7E z-{qw|>k5IbZ7z$k@1yTDKt6*vMg;9>sY_H@G%43#R}I2scocVcX0v{(zk6;Qxx z|Dhl^H=N7(>$gQR#tq|s(UV4U01cia>wD17ESe71H-CPO;C|=J7j(vVL$iCfI(Ol@ zzOME+gvf1Ma~)yz4D|Hqa+5U8(JkVdYKI$Rip`h%vTZCk^1Rb@7I}G*sj2L|Gj3Vr zeD1AIE4C6aDEhkzXp0t3HFNG)Il%m*IV= z?j|NC-;haks4L8Pt3H|MHI4FYJ|Cq4l$N6Qj*blyf2u@kKX^n~;R^sow=;mwB_DB7 zH5!ghc^hn_9$b$bhb0uI>@-(SPw?1XbZFvL2kf3@uvBFot6LQ@_?R(Mo-W8 z%GrG_Ck{&Jnf+Q!n#;h?C_9=n{C$eR+Bq&Mr`Wrr)?(cTlkB&`yQ@@`G4F6FOuB}d z9rwPFbc0&aB4SE-{peBEa!c%l)r6nV9Rch3d+{w|4IzH?M&eW8gpd6>N!%__QOE9m zaXd10i$kxF82>;=?yBiemulqQ)2@~~iLyYQ@wREONUFIqW(!G^Zl!=R9erLAO=6No zN84BVP<{8zs0}AaS1zie!?_|Edn*i4f?y)_#TMM0wPHdzhhFKkqa<|m-s-HMR(4z= z4rr>~H;;t7SGXH4oyZI2bi!=%^Yetm>nhQdVwCd4b!k|i_xBw&tDB$lkbM2d!A>=t zkJ7KzsLf-?8t&9#BFm!j^-DpU>&5CN7h?J&O2m~bEoF9AUZT#f6l}*6RmsoA8KiMW zIiy@D7?_yCqoTaL>Lta-uczeg?88I2X}lW?Ak{l=GObz%^3w zWQ|?g%Z<@q3k$H2v6=)SeN7+}VbHeUI7rju% z@BAF)hi|UTTN#$6fuCCZZnau6tkM2iY9fETMR zDkG{fDtgNF{JM@>BhfaM~C6eW)L5ofgnU9`F>HJ6rs8@1R@x)z^GecQqza)d_t{?NRtFJ%!T zF^ZJt?z2_G)#-*BHLT6)hOs=-T99o@eC`F35RvimOrUVu+_AV0>AI(&&QVW2%IZO0 z%?B&_v-nn{Oj{@+k=$GcY3nPBSRyr;3bG6#ZE+W-)6@|Kp^w}<)PgxLwH$;&!>X&) z*=bf>(ZkY4aH@sBbhD~Z)mXMT86MFf+C7xw#(kN#_M5GmSr-;$`|I*<_MuL2%9Um0 z#>r@@RkZj6{fR*r{fmzYmb=lt=;I1p21JN%qR;hM&p?ExEuLUKsme@XeJlhU?D-^77wBT>3LH z7uED2cpH4@;(iji^nr!VEnjcx>*9CzmZCsI)5q_urwQp*yRkd&9k5pMAm&hCw#pIh=rjRSiIFHk~Y{gU8=|0O=Uolym zW|LPLt{7%Q>+wuq|K=r~&IYYsvKxwX-I-~U<^kdZbi}zTluX+bkx;KSfr*1=bjBJkirSAdmGg;tB7)A?;Qpo2#SBjc^YB8)E%8l z^m)?%m!H;(1oPnDkVMFP`__Y*xgd6*02f`GFF78MZCa3?X=nqXpqjvB|hE zE@DF(Tkz%(#A(W332h=5Uy-gw#H2^x+ej$@bfa1CdtAk3pPe-?HyFNW{;#_PJ;>&nwaZbbQ3G$P2-rfCGAB?`3K&5-Pw z=`}}N4W1WxUD`^@4M=*402z%%u*Js+Zr4g)Y#XcI@V1w>KNY(Dg5#5su>ZMaPDpIu9u`%h081j z)_B{+SWj!SIK`5H2sQZ~WjIY1%FE$2iqX3+%9s{(mmH7KTOU&1*V^JUfR>5DvH8ya z{;?O^;H0z<@1G%hDVqnM(-kQBiP!D-sCyn=R|K~v%!m9^neSq9X84Eu61cd6j z{tp$Lo@IRvdgm;@mofj9@x2$Th?|mcS8uPAr${&|QZIY-B<0iM9>GeqN7+WL8fb-+ zE>B@&guG>Dv-* zf=rhqCDvviJpPK?9}AA@o_OnLi?TJXeawE7>PbFHWb}Fx?e690?8|G^?YK^syDh3y z(sjKlBY{Eh3>y4UK%1^`bFIZc(~i^lwCHi>iU!+au9rOsX#cv zFwTaLy1%(WK_sE!+4;*iag%*Jcg-l0GIUYy=C?(-E0(MNc5?7##9r@3H1nf7wvA(* zt?{Si7+XfeW8KEfy(h$v_QE}mFYosK)dC0{_%LX#MDu z@BCf|S;p-LF7IYowm@P!uCY<5sduLF>|ik>FYKwM%-iSB)p%rPOYHQ8-LJsOQA>cW zKlJ^q><$*8r~ZLublFmOt;530@D^~?qTY7%h3$`W(-5Jn!yy44e|pWRuxpkU>96Zz zIrDygu5e+VrDUhR?~}y-h&mXY5goBkZ0Pr}uK`QLk{-KSvfU-HBcBc9=U3PQpes(K ze2$;4iB1#kzvwEiyJ*#IlBnC-Uhg)JkftsqMtVY1dLs7Pi^ndGC|?7zfcmk!RVN28 zxaZoa;7`yUr%w)(xN(_V|{ooOS544@e4isfP_}MY1dG{=(agvwWJAUc2Jn0*Ys~ zfIX=T_x9a^qd}(YkG>1f$9?Nh_B`n2b-webcK~yqW!C4S$O}YvjzF4!BOX-=OAJq* zihykBW$=#G8H1m$mp`py%3j1x^2$8>JkK!R=<&hl z#KgqqYFpdo6?F33_4}(nBl0EY9wygso65~io8`#;b^^Mp;{%71XPgJ4!|rPW_;E)#rXlZTi~d1KLBh8y`7>P->QzhSlS_z^1i5 zvK8CclW~o5n#DWgEPLtu(?O3RYRRDnShfs-%kdixHB0L1>Vh%Y#qQMa{WlhGEgWo3 z)>y?+Tc54wQTBopYsCdyOY6MCt8AH-4&de-A=1tWm{VJHGF{GLjO6D)?C9uTj0KtL^VvtYg_)!tpgp-r$|uy; zzM$3(q9SiJpx^E5*&-;>vcIzU(WMwMi3!ca)WG@^(?Z{gsd!b&-AQFfiWZ!Em-Bl6 z^M?S(-rMex#Zy@3!g!pUJ>!^=?dtDNABn`&NZU^~e+(HHH!m(RN8wl|*)-~W88Vql z?3>*0ecO1Xb?`%#*>B;Lthcw9TUw}@H?E`(ZR^{!Z8A1#*)5ymdx|TGNw3_yagL5( zQdK@T14TG_*G!Y(n0LO{0J`PN*E@{9`|7;Ae1fgigKe=> zXGSZzU#1Z66I4Qv%RmQJrpt0M)YmRHx}CJ{=V4zYX@}S&8ZZ4a%Rhnd953W5=Cwgw zkM-H*ML@aJ?2EeVO_4~v6(t2eiKtGBQFm3w1~q5tEPxO^Hvvw1>%;PI|@{ zD$E88WU=xfdjOJ|rHn9~PA!BZ79x zlBrZ@at<<#HJBfuIOB<}$)=ukysR<}1do_|5f;8dcU;hz;Z3wY+?78{7uD3nz}l3~ z5!hUi5(_a-{Q+83V!n@B11UKWUpT0M|s0T zyf0jepijvN$IEJTmP^K2KYP4w|N7O^H2Vy@oGUh+TiApM>B*43iWHqvnSu=!GWc4VAgd5Lo%E^V`zYn6{G}?z zu}(D>RER`ULZlxBx)}3rS7p`xM$amf?5}IHNg78+(*Q za!F3+Z(jfQ?Xo-yz@GXpEaXS#7%_Z4%q0VHGqfLH&rSXM=9oiKthCQ&bumHdj zaSzw`F>ShCORC(#H>Zy|OTe|^^6c}-ed+?a^QHEuMn;*} zO>h1`w!Q)?s;+H&W*E9tkQ%y6rNcowC1gN4BqSu1l5mEQmTtsBIt8UAl|~SiZlt>< zB>pq{ywCT3-}={@HA_9mGw1BR&%W!ru3L{l&9+j}I|$}k5VOl5r$0a-R#!;$!DwMl}?qa zqGqd=G|aaMj^5_Ryb1FWpg`#)5#+O-aTuN2vHCh~X)?IDxH!nrS`Qc^c`5Zhyy{ZG zk0xN?FHVYG*@}Pq>TBE)`)PvCzbjm|O&nr&>*rFE730$7^-ju^;JQF>#Qp=4)0aao zj@>XO5gh9pKGfv3JRdd#0*bF^U{H*6Tr(N-IbNPB8S9DK0SrG5>G?i(j*6RM;VqO8 z%#B^$3BL#Ye!DjJ=uzGcBgl3EEmE(I8)x>~u8?T=HGjZU3alyPmH)&e>}21d%D=P| zw*B~)qpza!p2drJfLhHmMtYE>W)rM$NeHLhx$2~A`pxf zzl{vk%A-R7Z+D==^6BOiqiM%2ro!`Dtt+)go2Tbo-apGvMhy32>t{o87pwZCYK>RGV(_Xt-A{196$zF*MPb_mC>+2r3)k3m%l2T%_B z`-_VFDEa1uN4|j#UrZiy7cymjZZBZ=nxu+sDxH{6oRL7+o@NM?lw$IkFtKwWo??I z4F5m(NrDmtUU%D{Tg<6Ck;J45(+Yi#fEwPrdS<~(R3T*rP{hp5fi^7rhm ziH8q3x_@bq*qJ;;(BplvsD#g6&tnxoh!5lW$3Tz?Lc5676Pd+Q32K#Vak>dp&p%}U z=NJ)0UZ0583DwvrmHwegRhc=F7rzR1%vN0VV2V0mKlICMaUoiMw#H}xqN}Q;6iP6k zMD}g_(c<5ChBXQfLw}H9{kcQ!U{3MC{_sQ-Utmm8D36_aE#ze&{!uSM~8|}J!-^X;`3RDcrwzS-J z25dI+8b8hb#Y3eUe;NB!hP##erGyjpo0XBotlx_jnx3zw)l@-Wm8^cK> zC@kEJtiyaFi|-pj{MQS{4lyPhY`MWtcwYB5YCn9}F|YPRdB4WxinlIiWMvsM!Ishx zw)4E6Aknbpj+}=Sl$5329HRzmQ(&uLH5eQ!yb_k|#;A#tWa_Qmiby@`THVaAEkL+D z-RX(yLm_gPLfMB{Q7|5yn@7w?8|9q|@f3XF6bwWYK%!QVldp9DQJwO3B)(s|&Nhtm7^R*S5$Ows z*PV&=^$eu`R0w;t{ykq0Jq-iMN9;U2YMJ{$kzzj~C1Y}p0E*oa3B+A*gq+4IMvi6k zD!Tiw@wu{K4b9I*)pyTrV;XVB80Gwp-Wa0~qGYY@e7`B(P%AUmmFyGI7%jID;-gLl zB)QcLJS72K`%DV>1gjdHg4aXuf=RD3cu%`0kTLHX$0*ND=khe?34?lnPiQhA8)N|dc#LN&&Mn<@$*sDZtJzV zy1M!!fFXVUQ79mqu*t>b=`snkm}ct0KXvpV1+?p!V*1%$yiJkRrZ}U4O?l_rqQ@HI(qihMKdwdgLHJ)=NFn zcF}^Pl&zWx*+I{p>4(g}I4qpxD&EwstoDa72XLQF9dK}QsVGO&vaGMKzxO?~JKOs3 zwfdm_xmJrh>C+xSX`%mwur)P0ny6f{?a>8Bq#6Z77X}J%rxK~#6O-Xegx4x_jmU5@dS`f;L9B$45Hq6_`z;Hf}gWZP2CEF(gO#L*p|ZA zE1!;O2ekB)cOr$(j1S)j3hMD)Utg}YmRzt)0try|Mb$Ia+h$r0Rtq5P_BJrqk!_KR zuW!SHu?M9y5Q=4>AQ=BjT?BNtwit^o2ukDreBPdDx!f3LY&Ms}d59 z5<=S5(bY5C><9cG!IW2ydc-JEtXB{FC5jNM_Ltr%>|7lEK#`|tmN63D9*o;Rb?tBR z)ae1T|Auo|y>%x+Z(m=+he4_S^?RTwlFdF6etNI%$^2vzwwI*dD-he*ss}v?UwE`e zZ$^z%$KCe5Q zNyW-#@9Q6U${Vah(=jEdY-}+#YTD2f`!_x#Glk4ZbrnN$AK6WWy#`h}G{--Uo{Xp!cmFFKBoJ>NezOg=%TW7ZAW|uR_EzB~5#7(M zM;llht(PjAU#s+7poyE@%fSw>qiqiG-yHcYbGcGswX8TD)U~8?>z@E%e?rdcnHgt7 z2QvZjv2?XPG%7g52-^~y!Bd>l;%UrBZ|^cqDSezTN75g+b}XqlmGN~8%5Ky?nJJk- z`*UNJBw>TBskSDYHTWpHVwqjvd;KP%yavkk%>zrCww1Qj^C zZ4QaB4NITb`JL=YX_f0`Fi7~oKM0H997V8Q`%ZPY)RyOalzYzG{|SUq^WP@*nC}ti~#nQ`{&B!ZEYAj5?o!8lVkq7%5}zsBlkQX zyS!c4JLMECoW!TG8IHn#~t(2I9c779gUR()>t78wIYH zh1WXIyiEpkvq?%mBs&2KE#5=k!AR z?8uBiH8-iXsC>7#roWX4h|;@E@N*Ue-{fb0-DQ(Rp@Z7U^ix0M1@XikpsFq<6nuac ze6#d7mbP_1Q=*l31$Zzj)*p$jxNyZPTjeG0{?Zy`3nP1Lph-7D!v|R`yt|Nu|NX1d zL#yW6{IB5=Za|yS-U6-6>L9QPF4-=)ih9VvKfW0&5d&FbY8{2Tbx}(DGX!*-Z3V$4Tr-SuWIP!&4QDX#?-{A zE#MDpah1OOdOnK(;gwwpXTN)umLV=ciY&-IFu@|th-B@%3$;6RV?hRirWQDOmqB?o zXB$xh*H+%3aOqU=>9EbzT}{gYuK?vUR73+G!bZ)qCZ&>UWQ}byGz~P0-=YRO^xfJT zGNnTh64n#LFRY|qra>-aC6%@1&K|ukT-V>qp4Xu3`k5n1BXBIzcUa>xn^a2w=|5In z9VW_R{5`?uk)dvAp!wNiI`uG4rSP981$H$M3dxH@R!Opt-B)@u&!n(`@&Ht=VvkSx zEZnn(W@RV5ELPTl{A{*oMZn}yXCL)>=|^g>54YO~UrXEWCv;AVS^;336%QBXxeWi; zbPQ*f{MeQG*#?zLgkLsTx|v@z9VVswG-eKEjyP`S&G_c@<5F&sBFHMfo64b`vEa#%2(vN3r0cjHE} z44K8K{h~5hpYL(ADDZ#@XQlJ~ak}BBWO!+ODBqrYk~#+TY!BB_`b03dIUudhvscrt zc*7l3N#M+Smbufjoy=#F#A;bEc`o$M+qLt4quV77>33M?8L2=37C|qC zG-hngB=KEwqVHD#zpfNT)BBuSlN5nf>D7h^;l3e4+!am7){t_FCA=JF#I?dH#7#`* zGc+Ta*ww^F0)nD`r|=~PIBQA3)3?~>-=t4B>I>6$hSU)4nOFP7I}gn9-nJicx*D$n zA@Fr*q8X(T9;UUicaGbB1K1Jf?E;h!&bSwui)o90Lt8>F02Uj}>pqfJJ{gN%|AGBj z7k8Mxf?9_?D{%6$l)%q>Z39>V4nJWMx2yd3N@4>XGV7L&OWI=pF?Z~7$xK4ONeq}L z(m^IA*w0?GhYQ%~{yoS(Bn1KM6QQM!dG}!E)FZ?|JTOm%l}3bkfan2~m(Vu`|4F%VDjl z6XWS$HU!KEDpj1Ovgyq@^PcQ0m3M`IdnvibKPfoWFAlPNrkCReL%WbS&Wd)|mMb_D z57K}dz@(Nn{nsCt#s&E5Vaaxj*Sp1_w6f0$EC%I@^zFyloZr)&u@VapIY})JMaLRi z42!ob8mL5HF;40%8y36Be2=SI?g5fBZd9XcpUbGtM+@ApKx})pl zic5o*(os;90|>O0bE+;(t|37!_$+8wM_))Rx5IHLbxPziv9z%9-|c z_r8n{I%e3B2jWZk6rN?1b2d44Xy?J?i(mcByK;>Vi%vVP{w?NG90tAPIj>*0x?h+a zC$s(tV?!AhxFqQYP}&}$83J~37h6cvIzfT7!tUk8E^+MCYg}>a;Adi-Jr%!a{2KA6 zmFB#~+r`HGq~QvD(L{kem-3=hKe-F|JX`)hH`{Et1~lm5GJ%;V>AbgC*Pzjr7rF~F+8 zu_wD{ObpGI)xdCvfQ?cM0ycV=6Y6ih~vTv#ILXE3P z{_{=+LLmjTLret1TzVn_2|tTz9a;tLdjB~Jhg3i*7IX8o=6I%>6O<_U9c>OUEsPl<5#x1V2wMq?a5f7)|~I%)lrmJVbkBkKfh z6g-1Sb-@D^lGr)XTOfWA$?Y1;OuJv&3 z)BgtRQbW{*&=B@9Ni0&w8$CM33;=)X@8#q<$>5!!G8~2s9~uAu;r z83v04A0kC}$-ANl{!I*rlR+3t6;)N<)BQco<b;O-6K z;kN$IJ^UwY7>8^Ij#<9DvUB?Dru?rbMU#Vhac(Zm-}wLBOyv#W*w)CLv%o(&tN*_E z9agfbHTr*kZ#*0RnH*>ny7EJpT>@cYA>`xjedW-%ayNZb%iJe6GScx`{MqcOLoWNp zzkVTb|MR%OP@IBUKVXIcrOE;_fV)3@-E_B5wU2ir(*+USp=z%EK}{xm8bK@I`n6d8 zG1CvU;A-?>9IBb3<~x!A5?kc+7bo;*x+XEBC$_NIi5VO+NvJS+cVB?<+n!FFAIpoo zB)#szQvK;Re$SmBIY;mQICfX~*wb@0!E`un?q+0u?H{a3o56qC*y-30wUM3QXU+a! zGb0KNhH=cR8<^>aKOz@ixvZ(FNuV<1vyTanf0w$O7(MKhey6c??k0Wi+o?SEMpE|= zAB*l??JwidO$n(Vyx+j~jCl2`z@yFeD4qO&M)VE`yc4d^iJ>CEA+?;KQq*Lllv;R2 zeBwEx|BI5eDCrAc+-dMjz4`mQ?-JzAO7cveC+QJ%sG6FZW?IwL^M!T)ozY2UP~=h& zRUxGmdYnml{3DPiVhjw{Ex}3hemrol-F%&x*pJuh>1xURy@&@QBl!zS%E6!mv-BUbaT=-*Ik!^a@>)oENn)4xJ2GAft{V*!3jLv$<5CS70LXw zA6aR%absIP6yp|G5|k_c;NMWB`b6dh@@OMr>%{AFty(6T%RDB(q(I&z?RYnve>HZq zjQ@tCK@e@2b!OjESLJC8w22*_NiQTXLWh|+MJccmJ`gCS?QwB%NLtYG=-%Rf8moCd zM{;Jj&eWO%)X=Udcm3uDXa#f1}e4Rjzzf&Fyt@_V1Cqt-(ZHNZ3PNc zl@zCqh}pTgG#njEvdg^HV~?_Ro&I#5ysD#VAs;|G9C;F&=x zZx-nj1Tm$1-jaI9!rceXrBaQqNOI>@b@8mxBN!gJO{29o%USl2mK@C|PwPoj`X_DN z4g+}CX=(KB1mt-+IS+8~`EeQSjW-dN1_VoV}3Q2ge2SqrAyGE(GWdYZg}S=1H#s^lwL*l9ta>eL##CC*NTLq>Dx>G z#*?S1Na^LaZxoXS3~0T1ZnZ#+!-aBVn@x zKejbHJHAypI9SoVKOy3yVZty$Vgs!OFq4Be!vk>kr|01&@)~_$UhK(y!iU_LrE~fk z1dL)dn~p$7edfYbX34TEp^+4`qUsoC;AIDPd=oNw73PRP*HD57=;Oqx zwInI9WS8T_#eZmdID9x&b#O&kpr47PCNFqd6CCIzxETvzrd zuB%RgnCB_>qEES^Vom3n{d z$~knJ2Uo|5h-mRAFNenVV@BpTB~)0w=YWPLd({JikihQuFEJ+$;Xqn=anV$#hq$HK z>L{CHDhDiIb9IOd8?fO|o$fngOm3wp*l-y+F?DlFRUVC}s;U~-(V+;8eL!L5K5!8) z*70lFb+ivP3P8uTXSJ?Qa2N$6iJz^tGxo~7p%4y$@) zaF@QPQoDa&zY1HgdXp|0@~c5*{QD3K@T6-;*iL_Jo0V2u2?MeQN!~nZ)3W!MPu7Nj znkHj)L14-c6FIoX97;hQXpwz~BC5)f?e?yhWWc(=S9IHm8Z%UQ-jFU{q}`i^*~GHD zf8M27%GwW&ociX-)1klO?MgIgusF0 z>lhjq&U`Jaa)?$!y*s-t3yk9w#)%ROZ2H&v{Nq(%ND?|Eib|qIhFK))gmt&WLLVmS zkx2v*egFA|Fi!Ix1PlY76NJ8{K@kpxsK|D(<%l{5o8i-r;e4!pvyHh0lnxs*qP2=i zqJ%b%jUY&;RA5s9@(UOqWWafb00+trpH>MDV9cC>s$v-5M&A2^mI)`H1n9kF5EX=36i7Gc2h?5nIm<&Xq7Q;oZtNK@jmIj{HhQ)%nGYN zrrp$l1ag8xK}=mr95uIIcosWP8FDm8@_f&?Ntl%49{idwB29ruf_mVe2nsV=;!*iIVDU#e!7 zNBbMqWM0tugej7t-YOUz?D|eEGv6j${sh2^66p$Y6(5i(>TrGFVINY5=-|!QJh}v@ zw>Z3GFUFm(3{3aXL}ajcbXVUpG*?KUE^Ik{}3B*^SmHbVdS9- zg#$I}P^(w}`(5`)%Y7Qc?#((|#aDNQn7Pq}gG~Z() zPo`JXmLJu#>sw0(?IIaW$(DTl-a#QkjJazG3B091aH7mp8R%D4Het?NXD*}DFy!7j zZ0?O}HlZ(UdKBof0H1hYcn{$!Swyu=l6uom-)gg%c3u|P-`S90a&zfl2CANBNtV>f z_kg|2Z8oC$(ebOB#pUHsxpB9KeQJ}T&f1`_cZ~=EVdg$@ji{xI#OLaIkg(5Utpd&y z7a{fB4Q~DwUyO~5vcp74_@sFIU3`Voln+yf;^w9C$C02>WfAU@^73Q>KR(+B*VGv((88;3JPQ3s~8?(Z*n^i_UQ8Lrge z{e@M8ni?y9ywsTz|BgbN=F1GVPx|bZgb=#~%c!pR@xU!)Ag?~IjygBds{$eBZLtpN z&KAr_yUu-B7UOJV4y5;%%iLi{?#E`cyTO(oeIjMI#D!dZtWyyB5Ys{?{DP8!`TU;1 z!FN3oGT(qcnt#EUfD3d-0ZECro8O}Xd>#S75L)&>vCqYry8rWOlLrFkS$vEw{y!f} z8_eU^A5J{`AJgz}u!purp8Oj;|JNVJ(*VmTw&O4MUsPySmF{E8gCSY6guPfx!&+&Py_3L=sX7?#p3ZpOI*~?tnxn1jT`+y z-vV2p|Ki|Vh5QOjY`Y@TQ0SI6Y;*@S9tdpQzqoV&Hmsq!Z$86#tHsXwvd}W1@qS`d z49LA30tk^+nK!_?2>5-ROgPvI*Opj-uDbiTc|8DH1Y|!0>grclC#~XAW8YH&@#uRk zm%mINR$8LACYn`VlF;+sFm*!#3Rs>A2?^8Rm#zGmoB>HhK>!Gykdjg`weUpeMlJE9 z&OhE&2tE*n5M)PS_{O$Cj{%Q9p2GTafg~JK*;BM-jWkf{EEI_hNlT-x8w>f~ZaI1k zDBL9un27Z+#>xtQ`xY~%=(Oo5N;9^Y(-G;(g0cj%FEFFLha|unP=Q>ZeSNV6$U*;D zC5(!c;l-&;H~{(>ZaxdfB~ExQx5wB@99E0?_9jF2v{wm?c>+lnS*{aT5jtYWDqrd>=a-+vnr&NdLK(fR8c& z57Yy|X!HWbS}Z-B+Zci!;G{4@0CmDq^>tFY)3#h|%d3ksy^?H0)hKG$9ze#I1<*fC z00Kg(n0=i@;33O_29zs2=D;AhQFdOLT>EHT9c);93QEUKlF@_&vJDxztkgOGO&9pw zpD(Yw%BeX8$f$7u-Mb&iywCr#HUhzgzgaZf#fZJ>a1=w-fAreseb;)`*z{|xwXu10 zDtPtHixva{T2Oj8&%gT><9nZ8isF)rl#{s^7ViePrW<}tZB1r90pyDzPrpq(wX2g9 zvitkGkex%(crP)+V2J>*s*W1yIvjhvU;3`4@!MHS62vaQ<(rbrg?08oi`@@Bv*v+! z*F6`V=Ntw#g-Q4qj7X6{`MC`2$ej_fvHXWuVpmh-Rw zVKBdJXuiL0?DSJ8*#I<+tIX_I20$qko*!+k%MkO0Fkh~zK=MIB+@U2tHx*x62YfEB z$mxmo1j4~^O~}N=1cXD>lweQOgIY7>y5P&=g1R^$skpjMOfS|W(#^nz5r=!-y=T?x z@#7;Ph>v~t^WkW*K9}!a4sOlI*J`JUZ!Q#H5;5@uEjm`>4jC%~eFlwfPj+Sw+JZ^f z4lG~B`p44C8e1;U$JCc@MpE-F8697Eo_xAJ!TZMlAi~sVArvx3m^72Gaiam~$B~Cd zx1|#aLVGp=^w5uPZ3(NasU~;-LSv{~GN2>rK&b2%6&abJo2%R#a5BebYr9;bww&5W z{N3od8R%@$SL2@5f}>ek1ZP3HVZ2I-BZ^H`2)U-^WcA}lPeTjOnyi3dl9L|u`8O;a zMb;AdC}b{J@Z5%t69^)%+>kW}2bwY~Z!D;d5Rk92~XQkGtN3{(E3w*`qxLjEJDD4vHT7 zljBSFj29mi%7banzE}3#tF4F;{P`!JSjmF|8X{=dP0uLO zSGmqBgX+XDR_J+nc*vnqp@bRa+~k0Mw@foz+jeWcnegLgK*Mio;@S^!H%wCsDr}l- z4kUYkISCj6z*lj5;_NU;}kVnI=H3S6WB%hYc7E^m)k!l|QB&Cg02m_R$89}Zp>O7PyT&@vXzE+-<;qU7;$0!D_&`wh z8;v~e6bk&KJEHgL84b5d3J;+wHzN9D-to;qPQd4PI~rpjcI-L_j27UJXaWLS-JcnD z0s43Ig~A!uPpjyjFSC&mZ+9C@7PgPmWZ1kwP%vRLH4&3A`ObJ%Zs4iC*O#R9+jbl6 zpZ#`QxB!*L@1?hUNU?GTGR64_BOW)trX89ox=>i)ixd;m6(pqfmv6J4N245-xaAM( zmNzAEX}`cLpMyot0sz&yW*TB}4GZbMHhTUp;H#NP`a0@T}O)Pc?TW@w?1UP36L<3EaK}5I*^-dOn|T&VM3g+@Y3; zSe4;i@VTOG0BF_AK+EcQOm`S5IV`FhZoO@U(&DhLHWSz88TKv^;@nJtU#w)6BYQL4 z_NZJBooT;g6DwEn=BbV%rbq2yno0r5NZ&G6`UkqBdVLRobc(0AZE zd2`oSr02gy`$m3ZG!un1ugVV$sSUwctYv*{acnr8%EGiEdh^uF*89gt>(4Duy=xS( z`qZ`5;y>;*mVPUVZy3xx0aS2RKO)+U7l7yZR?23H1n2Vn5Ph?)+*R|oF^^nfE}*sL~eR% z7=7z@q(sPCdN@)WF;5?U^s9+2UdP=$OOe~1sWY4f(0Y=9r2aA>;pvz(ZDeX>7^n%k z??vmE>&2WHR)T8XAh-AA2r!73^V# ztk-X9n_0-Cyg$53Zr^Tj@O-s_>3apC4r|-3_?{y&;B>lA zhtiwJXqsNd0UOed=e9Fhw|8l&8gM`%9^ji7qfgk<-fn<-c%>X)y7nC&?F`I=SE6WT zETJiNG&BX%;B?!JPC?QYHF+|h-6PE|ky?z+hgq6&LJ zS2+=A2&L8)>n(%T)Y~me4B4Lr6y65Q1Iky)i7Ewx1a6Rf~I5;_L~s^ z(Q4t)*jY#5wvJ@9S$?(zSKtWO?T(gzBtRq;mScZ88*Mt-7}2n(wGREwnCC2Ku@kp9;|IBKbPh6$e{C>q4jUlxmpwS zH56#`RcOp33`p8AUgK0)yZO>raGuPn+u0S&p6`)U8=V}Bn~w?FVuYQ&Z0O71t|(Yr zGbnce&SLQprYtcDQl#Z+f_9pOrN_27CSKYy=M>sBa~H3Arn_~A@vLi`YN@Q1$W zdH?YFO>K-4iENwO>+w%XyMU;{Q*X7>SeCbUp;$~Ea!67e4T_CmTp}En&alWCv3w(J zuOL9_eBImPN}*Dzr@4<2tef#qQTB=pSub5l1COS=DEO>-4XDOC8^{CDyVNk|=SB^w z#G~=Q?w+5r0nlziGX{c3Td!>G-5R2u(Rw0z|csO|`~jHZw-B%UxSrZVHsOMYjbJCqC^ctx^YC zgI_he?#gXi$y#}>de3p|-nP(TGR51Q_A_X>`JCcIjgXgsaFBbb`v>9fFMisuY;2jU z$ePG)icdobuC%D86MT@^+Xy&qG&eB!dCDwi#r1-_SRf9L67vFZ0u6C(-l51 zZzS-^7BWTmsb(K|{FL9jtrm9;s|J8FK5GozIZrfv8-u$Ifb7wS%r9)J?ZLvXXMrDp zIVG#j!KfeJ9r~`&K-p%ofAe^+6=*btZL+DAC7~U7D`LXxIy|cS^g*off_UPL`;8$6d0ZMapzk$9}Jq8<+FUj5L4!0>5i+nmyHbE&ALKcTZe`9Ey~_58_OpG*N=iS1wxs2Be@kbXdZbKC5F`qe~yOgzv<)%Nsez>=}p!6ze#ApXwaf=np%UMM{2*0J-Rx2?unp5tW72(SKqd?3|uR1Lm`Hf&st zf;__^y{}jP8=U^Qu0nb7AW#7C=)KF2z5IuFUstmBUpO** zY3q94W$ON~t#`R4>2y6l4LNg3yS^}N;zdT>dj$$q+5i1gWMg{ycb*)lS%p-tch5*7 zH7m-J;`DUNMNijDhcjlQEa-N@nh3!Q!r%$4o&M*xlge}>;=0PDec!$oSsvjF;7p%j z#{$j;G!FhWQE$u|@OQIrmqIGmWdcq_o1Nsce~EZ$h3(pPcFagV{gS8$T9xGi(DkIy z4^JU2^p6Ga0{G2fKXdc!vyJ^3vuj`gs37A7C6bSMVpl4bC4sbCEhT(NqAX+508NXe zpj2;2Nbpb_+#5XqQUSE?%p}w4bG#7Sn$Rx<*w)#&HWW8C8xe$QmMQd4Y;{G*p2&8c zW0Hk`KX5)g%S=%tc@9n~|7??ZB_j(6+e4$x2M9#p&UcdU;xkrPOrtT@%WCKtsHL>I zPUZcSl&vwE3?rS#z4zY7DB&D?a6^FJQYOzVh#m9qRSN@Nc(sj@!2e`E_qf1iX>2bE zaC*I{Y(zkYL1(3r?CM%bAhGQY2`B&d@qR_ttt}%L5wzJRYY&GD;9U0IE9%&p*)sS| z$Ml0wQ}aL6=l{;L#;XC9)L2abP^17lFNY%_ z0;D`u0bxUJ>r-g8&De(lb~IM0=MEX0G$EhjC6$h9TP<|MV7?tt-OYdcy!z^6cpf^q zc6(UtPS)qQf3yL~2BAM~Hu6s03?#%+o?Ivdr38BCa_JOdF-QmmPM?xMoO0hblC#J- zUP~+XUL&@Nf234SvJZJdqDE=dl_OP+Jv2L9bjQ7AuR4VGa^Nk5$1Ue*O~ zXPP1&3cVSe;0*R~AgE>FHdh+>QIXO9V3JQuxZevnv zuLR?{=C0&ZTqI~CwCpm0x&SIUGio`aE=5h#EP`m=xcr)A?*!ROcoCqpr2!jl(X#JX zYYT)5gCh~@;IwF~nmkuT1pj1{MayOeFBj3a^Ie#8)iz$!{+X00eoYY+D_Ja9xRCK-YUDV(%Yw+bUIE; zuV^sj1K;aM+8yZ(hDJua-U4S>dJ)<%q*e*kEq*i78(|lz-5h<>RT% zE;lG+gk>TGP}8qCb>1m=iPjJ&B+v@1yK~$s`(dSaUvDl^9~ztk&8PYd52wMp;{1A{ z3X{Q0Q=_<7wV3Weg>7IkvTgTQ#^?98)qK~(3>q~a#dA+#lroX*9a9Tf(wfm&58{sV)nS2ER~iUlUH_l)($ zy91aCyI$;18skJM&=O|YTY*a3%07nxdz;mF-7-%6(?ld4S$iu*DF>O1aAWrab+2Ql z;`^nBi?6tgi|hJ@KfQUDZpclh^BG=~fq3`xaX8aan6fSoRQj?1xq^yeDRf_b5>RjSv)Z&%}ZNIL%l^)AG#ZwjL%{K#Y zzQewvYWDnoM9I6;>#b&7WR{_?sDHuO>us&?>c$2dwfs{a@qXBuj+&L=RV*SA4lq_u z$Wrblt%AeLT~hXo0*I>; zUuijMUT(@wKan}{9mM)N|Kt+|yrqN~`-KkTjr)g@ckPch97wqf7U(8P3{z}EOyZ)3 z)AzePd+I2UixIdbLd>@jxcT26NSpWTYILCnrFELM1?W_sy(tWn!FfbPOU=OVNATtR z>q~UaG8)h5bDR7^6|Ml}11b3O*&1%wG+8`<@mkHHO?m(!oHxpRTSR>2#)!D~>$O`8gr8ZV!4RW`u?ac|pUrZEM300=b zV4FL0u@Wt(mb4kFERwB!cy$u|e2;MU+$z=`Kn9_5&VGCDLuK-Bc`1t>IOfirs^%{U z$@adQ%PV~tZMWCiYN79RE>SF?q@REKCiI>s(OJ76z9CWeWJ6D=@Acx_uhDojc&>2`SC`8{1LAF{U^WRz>Bp~@WZ%z_mhjf{ZT(a`*qog`;B<-Os7bRr_c?HNB65)c>_jV3 zAy{Wtri2bb2HrW!MYDd&8+%2c@1^mGc1S^7d6e7I^8JffEYK|aEcMSwBJg>c^nUXB zm(rOKah>iV^h)luAkw%Hlg!{D=v0-NrLEU z#ORMx_`+I6n1+qdW2#62`xjC3+d1y{vdJ=ZE9o5Mf@cZ0ydk+MX9G_IPmW9j(-OCt zqd?@9FTuY>D)AT!@g%yyt6}o&>K(nb*wrR_3(AjUu)5d7mT1jt57TMRyra#}{rgYz z?N{qX2^ey3jB8J^3V+6vn8V-jy4amLJ-ID&l=Sh3f31B{&d&o{>x{SLu>P5?P%0)< z#qV$WZoQNl4Ux1rM}C=I^^LYJ)kVKBWX5;qF}&1p(bC6R0C72(+1-9UcgU3RV8BfhKUBCzM`mZVIz*$*0?!>J^9VL^uteJjTg*Z)W`Lrc&pyl} z;A_gchAuO2)eV%t(EB??d%c#GGtI=0MiU+Nk*1_r-ZOrbZ4_&E0P$;TCH!Ji0*u>uc@Thfijlqf`Grl`L;%)i?5b@t?JGNAoHT~r}K}zgv zvHQfbY>Bd6`fd5ORnM0i$8zI#SVippZW74Ei|x#kqT^_Vv$2$3;amiV4M zi!u?jS!t^8>uMqA9rtqVy{fr`c#y4LOfLDNY0*KxCCE!EvscGKO!-@lO7-;IgAtQ(ylj;x;*ezwjkbA+*WdXyFDj?{}0 zkLOyyK|Va}?}sC^FaM_I^uu-zTtiFVO3Z$FpWU_i=&hFx&K5WNWw&U94T)h2q%b`3 zUhjMF{GNH*=oL4)C?i;Ac$8&DfoX#3ugy?+_gXGznq|rRQ@ps13Av{M`B?Ev{;tX?F$(KO=VMz?;RAJYcNYS zPn)|x4Nfz^T9YA=&YXe)SalJ>Ou)}MpWqT0;N*fN63b+ln?Av@!r(*40%bSo;(&tZ zSk*8h-0*`6)UZ_tr8<%fT?qItYmR5MPhS_17I}NsF1wybp(5H5lTJva3ev+o((Jp# z2z+$4QZ*#WW=v8}_AvYe6kGa{?g@?4YV@gNinw~?r!5#F8O3M9#h=pV?)6qH{q zy_cF2M3t@s57>Ns)Lk5fr9PC>$IR=sGOd~qRbj{~E2ol$nb6(K`jWnkFFFr{w7~h* zoMYKx3tl1RK*uezCm!qz?PCkKgQz!syoIbqLaC0Z!#E7y9oyI?G{y0=6xi^VO-LtM z`FzA1|2*@N-#o~^s0yKN2jioXv6LL#lo}J>XF%N$H77b;m8l7F!IIX$7aG4qVfdw#fHgb zN61HF0KrvnxSR*U$oSU;=MM5B<*y&wp7tV@s9g*w9%TbLg!EP%L~<(&)fdx36nfK0 zF4k1ZwftsbIG08K5-T1o8!ViBCU5NB3}rPAp;l9Rj2Pm5hWCQ#=;$l0m?>_NaTlQR zX%)nW8``~}7*SZ}C@U%R`+X{K1sBFZQz!tYyFfm&+AH=SENCuqMkb3oz&k}Hm5JIBGa6OoaQ zZ8ze6sjqr3LZ-#B{&)i(<)rYU8f;Zu8`a(Jo#$$_(uG9P2m#Crn!Is)l;aU<(yM6{ z72MX7NLGK%(DbThCP?Uf!1BCJ;T$L$jSUO1K=g21?~<` z1<6jbvqwJ2%P5*!t8NM_`bl&s468g|_($d2QHe@I@2sHQ)VR>aivQ}E^CD|W3se)l z;KvLE(w3jp+@yoJ`X<-#{*#qS82r6W2TF{n;n@wtl{QvK{l)arol@urRKSeb4M!up zPRxzFIY!AuXgdI%GioX|Tl43zq@GCW$MWT7OXc{b z3#(wn5L$7!EU>cVbZ!Cv`t=Is#>Epf1tX=C&h;K~@tG0WP%EW?Ecfj|H2hD+8N8 zk?ciwCkyAh@I)LY0Os27kgs>0GaGK=oREZv0VZ0456WAmD1EOFffe4Dg0h@tB!U+S zKpngJsSSd3{vj=mfG$d}53^4<5FWEPq@4!&Q5HDY=Ln$FCQ&w(Yx4JbNcj6akn#9M zK+cB=wjew9Onman8`*$l-wKe79FT1vkzcdIRhVADxZS(yVKvZDFAGAUKR);!KXQrn4U6Uh{yQEE*9rq-Sj&CJmQ0f!jDsxyIcwx zIp(T#KU&+O#k~X&MTYm4ey_RmsNl&1seX1IHzcVq;fa+@$K0&LoW!QUpNF5-#gk#% z7Cq7>D2fdd+>r%tKN8+<(!FlIFz;n=$4`w|!=y3lX&~fs^;D;WXX=04mAvZu0Dbpc z1DcUkF#hf-scLijKVMgegatjqnPeVAM}&mYQVtR|H|bjOsQ=d`stbpa0JJV7T_Ji0 z{Dh+vj(UArDM&*D9gf6vNse~5(svOIwY6%va?eiLx}uM_Ue!Fy+Y_g-Z1f6!bDx4E z9;Um1pgRe($&_Pn5pL!aNpWN_COVabZ4oc&jP=G{n~mVbjC1edm1uZ$+$YQgqWM!9 zD_odcReOHJ_Z|5EI(Y!BBo|4GK>8S0yXi;6s%)qqx2BLc0g}Q1N4Jq=5xsZ}3r9Xk z%E^Ed4de8TC8>{YHJ}pD_Fe5Wu?j^I4CcwRpv9kYhCyHb}t^QzL@Y*~3Y|WSy=Us+(P5+aoHv!cWrvH<2Sg zDnk=m=gLk>9=qZw_Nki#ylgETU0jp&uLI7(UqKxgAFu&h(xYc<;}^m7fQZMW)uxu> zdt>S371)ytwUxtQyQY|*&(~%KB5rjC60>$}Ulx_u5cb}^KnqZiN8qKMB5w;fI#*4dMq-?lYsq>Bq{Z?ZT0>J?EJ za3lgZ3gKB7=Yo2tqy~CltC&DKOyn-K5ow~Z^0~$vrsqJ}jA{FE+QhyqQbusH?*#xe z@zd>{W&?KnHl`M9VGJz&y=dC_SDL&53}V`;cv}iL86!()u9dM zkK*ewslXL5V=)-;v`-jFtoY6RVxGUh!&-a}K;F%sP$O?h3rs+2z_{oU-KTFOBNCa| z5ws#$Jn$45Mkiw-TWn%sUf+cN=u&5AmZ%4pfB;FtoO#uo&HrxrKiS3qEmeqQVbYMK zFA~iXHtqL;3-50;Mw6USE@Ls&u1fj#IA=qO!g#4ugS8R4?b)8w+u(Zcs8U?D{x!sN zC>T@kVK2?u_4`cCnRVW@-VI5dG159y=cxBtLJ?@XdeIj=IILySL4JObT!hmbroWV?pRY=qan_asZnccg(gnNrz73pBOi;oFv1Nu^Mv&d-M^mf5$JBpn<$dR z#4ab(nXtEUk;=_3LHp8o9)FcR5M+xj)W#sAv3b5N6eA>70GE5d+4Kq)V((8P7x{69 zpB3`n#_c2(A;|Ej%O^x&iY=RAdoxix8-(LnR0Ia_j;9{~+KhMd#l>ecinCk0m*a7F zGNx#DSP$eabCDT7A|dRB$i5^ftVbU4rmJqV2&~67S%Knr8FGOVFAvj=Xax19+j4P( z+eKwgtIg;T5OT1P_$M3V|Jf7oXsV4;xn5^q#O|wuGMBj*fsOIIAh;ICE?B)3i_;n% zeBZpHm|=49V+*G&ezBV zz=E4!TJvTDt@4stVtUSh-4X{nin7_NbRrH!Sv%uGG>YKPkvdqRml8ZLn^ z&y22y_oGX3{t^)F{EeFfqWuA;r=4TZCixC}+3 zTPJFp9ryppU}QHoie>r8yYXXz%^S=#={aF%gM}ReJAQmBC^R{uH_|mvX7j>8+i48P4Gk{nr}&vOegU&^9~A3xddR zJ#AjT=)(-e0<~T8c#n8E34ndJq8TP2#$Y6gN6#lF{}V~2VNheWkwkE!53F8k9c=@~ zp-GX&e_DIaJB$Hsi>KzmiRF>S_Lj8Lf}eOHhe&j-0*zh*n$j*Or)$+>_LPmNO>TQh z{%VeLwu^aFbz;VKZX;}K(wZy7?tX_-C@v+^lmj;@&--pfCk0R4pnMb`HE8U!DFPo+ zO|l`68|Y`dZ@{^J@)eRYXrRauJTV*L2TTcb`0sJ=FS7ju$g9x4PpU@@LlaBS%g@H! z67xPbkOQ(Hnry^}s9g0v1(IU$5ggvO#2eL#)E#B8HAsAj5!<74Ptb0*X$2dc%E@07 zghcP9FpUmWO|wi%S84R@PquSyay625hQu(>293$rqDkXP;8lIUBV-Tc=8COSJO4(S zupQ{-P{fzG2*&I^zuSfg<_|&8vh`Nel_V`)Ix@?%7f+dE|6Qp8SpR=VWr1a}&)B+1q``0QXK8d1XY z11g_s9@(`WUIbTO?y8DPIjUY~xCpf3HhqafE|S(`3c3dlnWLl)THoI_eaE|^tvPB% za;S+C@;VQE*Epm5qTc3f;e@i-wb}Zi4{=`Q7-3lS)o)^}=hHP9=J0&5Mkf5hKJ&B+ z$&v!Igfqlwlxm^UY?r^s)x+sJBi9wNhnT0ffpXAh>a>>RM^qMBUy`OC-IYvG1_I59 zs=hLsVm%C?(ah}2<&&Tw-w)Ar)On!tP^Jy=7~p(nB=8ZkXcg!GyrgqZN&#lAoU$eY zTQ>gchBQ##!&z3FZq!J;&Jw~;32vkLgLBqp+#t#bH9>b?L6HzNt@ZUwviIfk`YZl^ zp5IxyhNQXoeRu#^r`we_LWDX%{9z~c;QV}q^i%doCWq_d>k#9VN5?g!`G^*@2~QxI z_~Q!VdS(i-&qPAFAFz%6cUJq)<^*Rl!r+Xf&M2FFUS$?NMR6`$8ajcE%LJKpUlFQz zZ9^4UDQ?y&xai*V{i8xo7|W5RD9{UNaaDX7Sx!B>Vjpji09PS;)}4#SwvZd z>w;^1R}<-G;Bei}b|U|A4HY^9;lev1ok9NaLob-U3N?q<-|lzqFG*zRiD-xs6uC4^ zAbMdPwjk@pz2;nL)JL2^^h$`M8XBMTT*ud7@~<7jht)}&)W0)uueBEok`5xvKt_o3 z6j{-yenOg~jOsR6gn0z}qQjKV1x%HBa~~a$qE1jtl?;DFo)T=Vtn>)zq3oVSkslr& z9{0B`J@LNcIvKhj76L^!VxD@A6TM8Ae>qrNTlWg>=Z5mAN(5Zpecyb8ZU zK+`B3ofj|}ta>2=8uF57TFiH=K@z{_NK7Ggr}Em`k-OLJb@`_m)WjTo?IU#Ul!f5F zch6^DJv=WqUiyU?%Y+n7rUM9_6;)}rhFIn}_tmm}*^1WWUp&Z9XD(F7U=M6&pA@o|)L(!NKT7Q}Hl8;R5@XI&0*MTUr zTCd6x6Id`F(DfNNSfan?4M#c!9P3A=IJY!6#t|b}*tZ@w2yEuO-8vP)8RxA)AkUU( z`?&#A)*#m=3Ucy2(X&B_-`Lb|Hi{{+eFL|AxJQf88Sk<@-=nZ?F$fI(eyXJ>luX1k zi(^MW$obfM-M7#=jkAvzW|vf;X}qQruPv_X zYS_L`Dv_A0z&Fjc2IJ394%>P6gF&8C#p@AftTE>M_DxS6#!I(v*q1%-E|W2+0`qpz z4`&)Hf$htHm2@?5cX@LmUK1L;I*L^7PuP?KmY4i9V_ADeCnmV^V8t0csou<=uiDwPg=+;~ zsJrt(EL5FN6e=8b7xl8J69&b&HX+BD{2lBi#o%r^VUhME8EdLP0i*2MP9CP z6u66mHD5h$Zc`NyvCw%2fp<&UJDi?3!-`qiWWfkw$*WsLL(w}tg3cw74YpOLbgu1TBqY z9Fnu&T_-|#k8-FR(iD$Lm-pBvu3!SDOz%9TZSEAuNMKKFv92669uam1hq3DSF-lInEq&CN)Pz8uK;n-wO)0Ju6s+<1wLfEc4lmypDBnh=3Gx@MT5&B@7$ zLd=*$=9v#Muk7dQocVY1QDw|2 zq6&;5wMo&3BegYn2?%djJ|0Hc8^W>4;lHAG{-N#cKIM|{bynQZ5=5w4gH$q8e>0sP zM~~y-9z!E~K&S%HyoAv5wiY2y$&hpm0e#`{)BED0jqA@K$Q1SO80lZ7KB*#{f?d6z zGvbjnn?i@5BJKXxNyZ#Dz=c$x?m+Be_W+uVqCh#2z$=HO6_w)rYoskarf2f=fPQ5A zXZIn{#uUh(g2=I8l!P)z$(4rbb0^$R0P&{d?TiV9|2+G}gDdUZHG+w)WFjaea0$&C zRWe~X%$s{W&z}GRF+u^j5OFO8k`~RkD24aIE#~Uy>I6_u%>GU)neCkvj<4W^@j*i4 zbf9wMao+EY(&l!Ea$`@mY<)N7jR>$dQMgz$T*JQZ7mxz7!`-VQS@%f|Gx@U(v*Fjk zb3X<9oRa46HAsc%OjgQOO@)WA+z60E@dff~&n}0yqfi-TK{4zXngBgtQJ>ADB=#8- z!)1irrL@vRgZugI>ybLOv9dOfH%qvi4kwEm8U}J(i_H*UB*j)CWE9Ix2-OBGVBW1X zZ^W*_{KLB7>Yyl*Sf`@rWx0p%{6;=Q99w;8RwpBp+#ksb)a4mx*Uk%z@{ChO`^VKZ z)2Ocvz*NPW;3Y{n8?dt(=_1UBX%Xifvvt5%+rv)6tj%EvgI97@$%|4xMFdd{pPE~q z-HKy?4b{peyHnZ@OBlPLCU{;btMdFJ6#-48HYE(6m=*4F=1fcOXVU7M6&msJ+-r2* zRVw=-D$#Vt^lEa?>2!``2VEeG!V~Tj*W`5$P1~(0orLU9FG(^wY1!ia1HKiwsMQg;?JFKe7PyLs8!Zm zjyGlzl$2uwD?-y6S;o1##4zlb$aW68Yj3#t9u@C#KC^R%eF&)JI)*V?9Z2rvaVS9Q zj9r3l)Swe)fqymmKxRuLd&JMxMKxBZi~#K>zWlSPP}C6jXMvl4AClRG)W&u~M<;wE zp}@IA`jvVtiy=p#mB>{?$_5!nntX4T#JU~nDUxxO=$g@8L6kANwAvSSJ?g5t!D32 z-SLlOkAmdZQd?qNRj1ky8DlXXqz7>NPS}E1NE5;Xz^WW((_tmDZ&AhHs*Ji4cZBx^ z;X10ZSEXZ5u55?>;AE z74EJ7`D6R7sez3n%B$yTHGT3R_@a(#JzeBrx+dsXV=|%XXP4m)h~qRpsoEdp^p3r$ zsctwpmhlMmlB5`1os5-Rzo*AIf)FX{;mlKjyS83u@%)IkGwFKfnw^(%$xmS69fW3I zz*Qs2L`fDJ-A}2Vw&g4)jap1W1>64&4|ZX`!Gknj5e%O$?{iX5DE{wy_?z{}!$Y?x zr{dL?ixCB_fCYJQW*9eEOcqDUS!+M>dm@-E`X|Zpuo)lnbv;Z5`rs+r#dI7~pZuv| zKP`mJM(2S-WqI!k?H}plXsWr%%cVI4`ma{TGUd`(F}Q^lbSM>3!y{kzT9-kaM%k(q zbBN0wZ8z2TzK19ZiNG3|qxlJ~jxFMWEY#mNt`C zd@0;LisKN1h3e1s3M4) zmVz?(ow3P@+{^DuNW!x>=3lMR^@w=_uQxpHm*i?uh^0zDL`Bz_&h`7|HrC-!P}`ux z&cXc5=big!vll7(1Pi}Bkr5`kp~R7lL}H);|$ja4D{s)`aRd_GU3zF1u45XS{s-Ag^vn z?Uc=$u-(q>1aZ>fYHVpqL?Qq7J+avMYXY~o;85Fb!bO2wUajxThrcZ=uP;kh|Fg*) z$Z#@#%f}hi2bEIHnU)i;j-oR!oKp^ZAz0aNV}{+lA66-1RASsKcND4=E90pO)C>G1 zJ|GA1Eog%Y-Ylj!84cpXLFKJX^9+4~g?YwjMCo~~q+p=P z6Z@}mV=6+Vl$O-rJf7lMCuAYSd3mm0Si?(*Pl zd(t4gF(m&tN!ZhpRP9)gVahBpdFl^0SN|yd4%@#iK?EUKwLM*g>(2H$-pPNUl~KqP zHUYJR#$w<7;N+VKt=>(7e&A<9;E}8?-5$X%L2k@qt7V@BCMVuA%_OW1S$S?hnM0Yo zds;t@?T;U?-`>z~V2X(fwgzX9o5B#MC3<8AZ`PO|xpiDbnXlVjMNbPvWIrxLoZVej zZsGQSw=*mjeqq;}eE+^wsrEzhkh-2Hk6YiQaT^hVkvEkpb1aN1{K${)>rZaJ8^MV0 zV+MbilZ+SQrblF5ZXY13$;#pC|5vxG4yhA>JtNlxxG_9KdoFzNJ`K^CVWjkW4$z+W zDf#UlK3RD%^*y2E(RQzNv_R$ojP_KlBOzeH2so(uN)T)#yqCyt2LMaxe9?75!zY!1>{%dbB$KciJr91Om7I? zp!h46x#Lg+YnDuGsb~3E-eXy+$hUSZXFent31#Jy!&RUwrTDm2DcT^1V6W?yu{7wb zZ@hp;m-Sti+x(dIlKhX67MDlYeA)XCw_OycgY#n;l&3JwoL}Jf%B6@7G&ZW89z zM3eF1GHPXN63OA>=T)t@=rf%2lKWkR5Q#nkV7PX_R%FILLI%|rn*ZeHaAyln-mugw z*>9{`vjK<7#@ycHM@-4R96XbGxLSdbR^i6n+jQCcPyG^_QL3^oFUF9{aRdpaM!#h$ zX&p-pn|b`@TDTP1TP8ri&Kdl({%*DhaV&_iZWpZ@ye$5b5kuN)LF zk%`n&E`ZZtIyi=TcSkxsf4q@Kl}#pX%CglXr`Nn`RE3{TW-s%SLX{?=_X~zo83y&& zuJC#CFJ`e zf**4qcugn{>18KaLaDz%viC<9h&=-E5+Uwpx<&vJqePVXzDG=MU8JGWzrfW_0pnGN zbD+n@xstYD0uU3O1HY&K|C`N1^7r1@5THUgLzE6{D~yPousW{ePzs}0m#h>Eeerv+ z>G0mUSxtXI6cZ*!D!|G7wO?q3i-Y+#upG2-%{bF$f4=o37BGkAqN{0%PiuPrt`@+WmVGQ7B68y0I#$#Ad>!4$tqF8 z>y2yJtFczr#+^)uf2FlZIYlSAr)pixC zwf59D`eA6~R#96^QFLG|)Vea?(cX@Y#C>i(bk;K`XXg3cImYB;2Cr8Ro#lSin$3t2 zj2HFbdCz9Vk5YhwVj}a74tWwKNQ&so!I#>Lh`XSTmxywScOPCZWnYe6`JCC9zhokH z^K>6!@FpZwSXWPZ|8OqgpeTXLRY(sqaZai?Qf$~hi*u=a4Zndhe=Py4XN&crO-Mlc zmNv!@{&V}{k@}8p@Z4B}n}ovb&)Uo*A~S-vWqdXXT)sRi?$ry!e_;(KW^mU5h38&A ze;8R0G1c}je}FSg7R=gHRg3(eGvr-1uVD6C233%|6p&jI_p*JMW+x;J~&1n~$e%`Nt8@u)BY2e_S$f z?0NBoqypxBA@B`8*&}CvTG3a<8lhVpAH|Fso~z5PsscR9Z71%&wpvRpAW#rm?m^dR zgMOoV)h^Qn=HlPbNY8c=oORA2UHJJJ=j^|fMJm0meUod<3t)$d+m?W7dF6>{(;Z7N z3Y1XNNwRbJ=2;~E)fpC}?>e$(-&bA5rO#qPHUjy6e}PIw0%q|7$w0;K^oj?~Y}UX2 zoG3@@v%FrZ-~HSpx&7HRv6->Y$wqB@o>=8PMMIQgtZ z=qd~xZ4x*@3S#0fob!)#0!uN-0pMl==c6skUscvzJsR@ccQzk=(X>r$6)fnIt3>xY zf4$eXQ*Hoz_UuP1G!Qe{7k-D#7<)LKINEkne%ONlDCX>ws+U5aUVu~t{N>j-4z2P{ zk2;OxkI)1i^7bEHsH1Tw?Vbgv+NK(Q376%bN_sp}XO&ZsRZ{umGI#v$)x0~M37ZZ)e?)Ot_&dfXbp- z^J%0QoDVxGC;%n*ygC?JVgp0imm(H`smFtNk17pvFmW~L3>A}yAy?J>Uh$_%u9=sR zWt?|G)^Oq-E#r}{h2@y$7o|ywOtp>M{hm<6nl^9grxIQOqjVUi-y=u{laFM|?o;sV z1eR33ZA8ir=frhF`xdlX)Nc!gt*)s3Z50khX_4qhsAW$Kr{Ohw{4&10zM=`{zA9Wb zua(&SR3U?=gY{9wxh&g>#OAqQ z+#$b?cw8LKMZS}Y?V-{nlN1;B#X>rsLoDNKM04MRZTf%P%^UacBs;cc_n0Ou6+9JZP~2e|oy!(kR)bM24GUUkACp-l6zR9+ zs@3xiBqJ9?ecR{v!A!m(?byX;Z=?CDaMap&=V!YZJYwH@hc%i}qeSjTnn=Xk#6Ghz zO-r~otk;e*VihG(&Jp4JqFnIGBsrnkbmYCSsIBwB)tnZ!5=D+{Do|Mw`*4F)B}DcR z@!J%-;Eema_x>u#sdw0c_DoAvXVgxK)IwvFt7D+OtiA3q%r{qeU;c~8O@|!+#=`{o zUh^(bfxAJPQ;;B8ir|r(%KrCqn3Zq7%09+nUU?rWVqRnUff$1V$Zp7GhA1q z{d`uYKBJ){ccN0;(jjtRv3_|-gsg^6_n$k-G?f8Y#njC0rUyj5=$h{&hWYuTG%s8! z(pA zxMgQnq(JTkd+uCg*y0rJ?8^Dir5JPAzYx3N<9Ys`No!(*pqHE0=c{psh3!90c98Du z3tA$V{yQf%4pn`(FPDHM@!;)diHZbEmm+n>u%TFj?Gv}DVLPLjFB;P^dYu*>y#G~g zB}l{VD3PN!Z&nFenPG}SQwM)C_;afSH@d^J+SQ~c+Pk`kEuC+O|CPOxVn54s5u0~^O>!bJPsSsr$th_T^K z1?7++U)mH%1wV_`g-*MpAbCIPaT;Nzk82RmKw^(!Ma*4AJEymkc53p8H3qzpMg)^& z+Bvwk$*St8&I#>-!)xUW7@VV-P~sxUKEc|HwBy?OHoP_{x0CH+T5#@SAH7i51nRiw zqrJs+t9B$NwFdm4#X2{RWP?9j^DD|S?tojCZ>Guo9d7U~(*bAn(R^OMT%KPfWGcBU zZc5=Rb1R^N_Gub(E(%OKfb_>YYjz}S z5dTl1@E=ece(+KpFg6f8W>1lSwhZjsA;%e5Xtm|y!C?&4&zWHLP1*NP%XP@gMg=HR zhDt^tKGRR-T&H|uEBy1RJu}m6QZLx~*g%>*miCatl8IwGNsCeIDUH ze;_6q{BE1A*Hr{={}csx~6d3QZw98pGycx)t0yk6TuzO>gYj zJBt>lvdvy)wP3N)v)^O5gu&}Fn`cBbgfkx4(EP*1U@524dlNSz|F3*htmP#iVbJk$ zFwyS6WnEHE?AwOAu|JX2yOJUj z(LzLP#dRa2L}bK5m~FAbh@{jpB_wD9qxBFfnSP&|2LPI`m5_gCB{5u1yZVDbkMN`- z3Qn*T4ML2f?~d@t>?026^_MVZv5EG$Ws(O?3X)9`QN4K+ZVWMbs_1xlE^tWnjYQ$9 zMtWl-yU3RQJ9Yo3^*&nS89mpgYKuBg=z6fv?o!eX42~w%HG&lria;HO2G$NKGx4kW z2w|$;gsZoB3(=gl@#MZYaUS{g6FFi0B^Kzxdh#y6{xo$d9m`&V{SQbgl=73#KuN%qi;B~a!o})akAR@LJ##jgege;=_yD=gp%rt`Gdd`P7 zhOq(l`ZK~|^oDd3ha6idudP3yV3!TooRKBA#Saxs39qI;xiAC073bmixrv!lp$mh& z(qbW$16T;}qUDK8i7S+6g5y}CZhym&w>-i;1z0B5qd=&U!|YjkHUHhUh7?FA5T3-E z3kqgwwv;evxnCU!g^Kv18n~+JA8X(i%UbGxHK`nmg}{*ZcE;`$HNr*U}qw&R; z+;$9Ma`7GcL3m_3$xhPm3;G)24>WrDg$H#?Jgc1Rsn3}Un7(=*<+yFqOuyaR^fU2d z0VblWczG7wat`w}V?=AmCYWj)V}zG7%!&W9N=rFFd7b~CrK?W>GJXJxG-;@z4aBiw zrta8ydHE+RY~E@|goMVt=b)AyR(y>OH{$S$Uj-x;Z4`tdzIUPd$%-TYHM1}#kW%4F zhdRFWit&X@*fW(>*+8QABqX?V+?!BaSU9UfK%U5TT251Ir%yDR|A8s|Msw6@J;tA5 z2yp?re)jD`GnMk)Vh9sHBArM5xkvKZwUL3}9*n`{304te=}b!|_{MFG`id#S%e#Uw zd+bQbRp|N~(&)t|n(kR9XzBdWX9#ngNhf_ZPv>OhnrPZ~rWk)QEqrdrH8QjUtWy0z zhxrqKRA}VAwgKKcZ5%T)L5OE#i;ye^@(<2(ru)&~a}#nGvGQKrHR1!hFD_`WSOH%K zP80&7pQjz7wpMPj9Q|5j&+a9)r1@$@>@Zeul^y8k@*K>uvlRc*j_79_1O9&qX$YH8 zklxD*7vRZ~!sn~#4=N~MX1dV{NpULb{J0aOH<;+1UvYLIR-3mE&Ao{IeIMt22?O-> zG?#}VyCPrU$M)9BlTwx!Xs@&~;YFrKuQ|s0Hk!zMi?PqINoB6x9v>XN+hW(UT}@q5 zQR`>Kw}JS2>bKAl29&BF-gE~ENdXn=+U-4qoZl;2E9P|BiSLmuVAHrU)zpBb{&`xXZrDfZxs8{gH9U61m^~Ore$S47@E21#l5tXIhi8 z>8GrBwU^ET=tPk`n0}B;;vEF8Pf81E_5KIz{O^|N#*h?HhIa==gKOmC@F`Uq;2-?pbZlY^ruW1?e@Y!XXku-&W_|uTrQW&mWde@uv zJ1Aah=-C@*s6Yx0!+_MCA7kjUtn1}s7Sa;=9nE+K(vjgcv!ef`rLo ziSq;61%rrSMLXpjzbP_%6y`fXhL%}>iTJV6-DJ!Ed#HPuL#-4y zntL7Gwxr*k4NL1z11CLHWQ1jm{m8W=V9WtYeW5VE55eRXULIOH1^sY+Zr??`XuPk8 zO%^o2`xE11XwdGJPe8*H)1#cksG6fcY!?0Ve7Q$cUHE%%d7f#z$*{+C1E;G8vH-w6mr@xRbH z5VDUjQq+&zGGETzv(JRRl+T;^rFoT-IyB)ml z#EUHV#Dw0tZcOIo76*UL>g*E(u%GBBK=~TKXiAa}{p%yVO&G}+wrX%A*dJ0iT-f0f0kX2F@vH&Z_oPxR>f@y zB!M1(0wfViL*8DA8FV0h-&fsY1_2PNZ0qzKeV8d~sl{Bs9~KxUZE*UZfq?E`PQVKM ze!?AY9sqrU?(pLo!{vl}U80JQ0aa6}W@qkppTP|;J!eF(bczAvoRtru_oyxyULa($ zdi454uAqqwT{(km{pEEHal0LK*`lP=TCfx@H2-cirAR9>DIW`*88+U6=&DLa8Bt(CtFjsoOM$+p0nO;IX!ZWO5 ztI!-Oc<9^8y0C&yAtf!)2CM$(zav~osGynqPcG z3AHT&_t}~#hT|kk@jtX z}GBU1>T8NnLoj!;%}ta`qM$6;4Ggqj6CnIH#mYG zVv#A-K`6L_D?bG|f$Ln?Cg8ovEFLQi_V$CXT&BoJ#@-NR*dD93)bJbq9Eh99-m7Ga9W&+jevGG&%VdO5dXlw0lT_ z%zJ1&Cw17%Gi+TMu`X0Dm@VMyh66o=s!i=@UA?IqhBPlgaFI?;Zbd9BGBH-9KAZ~n zJH9r4UM)u3T#Z{NI<`e66Qgw&?<6B{#QmQ?n-z)JsQI~8S?}Zto=<7luGJZ-J}zy( z)-KfcBclwF3gg0Q+mJmSX*H3O`BvamlJg1?T0YcP zd-Q?GW4FEqKiOF8hC{3y0Qw4FC9XCxx9#5c-)6pi;`^hO6=%XMgn=$u^n3>$~gbQ4By}bT{zue+50=Y3Q zdDfq7HU#f--E#MC|EFN)(tpte!_Nwq6zFbU|k)PapV&ie2B_G z9lcutOn|kN&oRJTIML#9bt@CmHk|aCDWW7SrKgDIH$0)6(jhInkg&X`xs`kF9E}VLJ!g1svijZU{1pAG|G)GG8U+iQ2nZGCQIdF~hn%&$KMC=@S&F%8K#)y2G zG&WQqARu7d-}Ip(n%!59;qn4X)yw&pfRbbcZDrr|$x(GAOIoc-z zPE6v<;AW58E3nL+*uGG!0GS-=q$|HLnVbCM2LI`o-`xPU6ciYjy_)-^^NzZ+RXct+cTz}B5k>=?q1bzbY_|iw)?$TAhTU1iKVJt< zs;#obcEPs?!o6^wgBQfD@%Uo*ZA-m3GmR@jZVtY8;~X;O)|r6INsI!tALqlpm+7{k zDZAzSw^0A0GyM)2d7;|oAuyJS6C|WZ-DRgE*+_B-_BP`~n`uSgYJl;n^K7ir8*KSZ zLEKg7HP0vp)3}*mPdiAPIilp>B}TcX7k(EqCY1w-85REzBm9wVDt9)JDvrHXoEf8 zz9|hSe2E~w@G8Lj{e?z;tbg=@Pq}0Gfq&Csi8%O?-Cu5DZ3(=*$Pd}D#!5E#9tNh5 zlSS4OMsZh4shN==)OLSU-=~VNeY#0@6z+Hd3j^{TF;GzG5!paDqcqnw?%ng9?jPan zU}0pf`)1}*7(qL$^6FDRQs}z7yE}#GG%Q+(Nh)Ha<+YDWdJM@ru7H*taH_ zp{CVjfp&X=>kG$r`&o_iroxH#Ccsuf=+o~Od#<>#q*~Ar4s2YAy^pPVlLwR7*jY`d zQ;(nWqI?H!#p zV;>RPij(j8BpM4{U(*+s`^m92nQHiUBTko4V^IH*g!F-I*XK>q#sWXyPT3_TR? zGi55OX^tRdyC%vFa2l$M+MSc_)H7{*kmnZ=`iIG(Q35QAhl{5NsV(6Rv@`0o^VGpO zg(|o3Z0l}XySp@Y0&Fz!2enMFQYe6Q08*#Ou9#cVb2h8+*~ab_XE7w>qZ*17mk{ZGqd1U*F-U2Q@G~f=URM+ideU!8$vB1=uRDM!^ zuaf%SU$7=KhAx8X}LOc3Sb=EhN z--+M%G*lT~E{*2M4e_9^Fp^UHY_^yx{-{7OfKDf#bRXB{kg68Ig@V$JUQ45jJ9iRzSc zJ-f~L3zdea|5z)`a|c<;jwUFU37jlrf_F1n$wc^1ASS*hZ1z&6=>>XSwEb$G5xUzJ z`$vkFr?HGEqV;R#{nGpOx9L=x@{T(;ezt=0aqt}~^8xX%v|prN#AUTSKHm4hEfz!H zz*)g3@VY>^A@?v)u^HvR>K9^V=#d&J#Gu7uJD0P^&#{}5P400~IuJmXy*~m{k&Q_! zFa#39xrn4<2;SY08`m3G3r`6bWBKm$=%-nb=U|$FdZ)nDoNM+lBpO0F%lVfb$UfEx z6G6*OhNfJz4=4<@P`=N!FV<3I8R}AifwzQXS>?01%`(b*gVLhg*~hItsvXAJM{5f5 z&lM{VWJk<>eULGjL3+twhV;!UnL{x_((w(~(0?EZio;;HD>7}hz9l4`YzRg}a=CG{ z+r8~8ZxD+3$i{zbJFB|5yJKt zIxZ^+e8D0Nk_c>4mjPBZn5)qd6fWGW$<*agy5==BV!OKDYlMsUTXC-6e zx5e@FgZ=()K9&NeKgwvof`8|8$e=T5=DXKCHpiHLlwwCE?RI<ZLdwY8{dhWvC7~S}8`SF9_ThFo5Cy%p!PjE>A!i(-oal$7^TO(D_^k7%8v}-C0 zD6M`i-?9-~Ve&vsc(u%dG4cQ$gbO4lD{E634 z8&7TF*USAG%ILC}H6ATrpsFBv+ZbFxyw+&XSCr*O%6&0CBtnK=z1-rP%*T!w+ShqJ zU!6ooMR;pIf6hlnA4|@^WBKp(;B-3Z#`%rb&fK^O*^kLr)DIy)b)OS5!%83X zkLSdoR-}jm?2TU;-^sAW2X>uaBbo7X#%}2&woiEn98~#h?F?`{3i0lp2Pn@5W-kl3S*(5AS75`UZqM@|l&7h+z=dT2!xhM}H5p6q0QK5n}&2QgB;w;D~Oo0j=I=evaQfIN>T3j6 zY0`*vSfwyUKpS+5QI@I84``Tg z%l1SPlH{R8HO%JO8K-NPo-Sxh_A9YSn&Rk(m)YHUDZt@WF^Vfgw0m~HRwT>31@6ck zy)aAXw)cnM;L;@)=HQ!XgGaThzV4_NtLvOy@XZ)$)*M(MCYkH$<%8xvV@jqlw6cB7r>Jag*^`OL!$z)^~?ALXFM|3jrnTS-z(GFm z)?I1mRaTDZOZ&?*k$@LCj^bkLXe-;Qq1Vj*aQ5-RoM{*1W#QCj1j|F5Pv?&+hjbRI zMn2S&ULPCpD|LK*qwTZxjR;&_ZU}@Y0z2?5PHf*Qd|lP=+Ut+INU-$Tj`*9l+;g!p z((}Yp;OG=yN2|)5vTtBGZv~_tS#AozvPu#$F)?nikD%dxhTQij`^^VP1%2YdOdO^| zE-)fcaMC6Y(AQJm)(Vd)9(SY;*qhgtDf49p2BoepL&8;8Ql$k+!YS#uX_^h?E($GDu7rHO>sybWhLA3nlME?3k%J{-l9g{et(7vxr* z5ls}Cg7TDm3;J#xj!oW)FKEWtP2?Q;6_FMgj1-4_zGWI<+)=LF5zkFuvKD9UB89G` zxj-PZp36*!G0P((&Bh9P`%a)iWZiaZT3SU8PpCv+J)S)n{l@i7mkSan04sW9$wPT1 zenvbK=pKfa>-V*>nE~@iKktF~r$S354s<-Kqz;Befzn>$O$fj6tC!m&@9Z3;Da7)v zP3~|sR*m_%;@#;7p@zvvDZ(rSSlp!<@}rH)*rlGA8lgxJ zhQ;`n37$Bc^s3WB&BS}~a(fg$(X}ALdBxAr!WwQREXm_tdV5sD+*l+~^Xakw3-tR} zn|_-rR5;)Hha5{3Q0o&iX zXj>Y1{wIh5za-xb|9O+-W*@x?er_Z#T0TVB{bcqc8*gl3JGtuwSzzy2z0V=~2#Y}M z$0e!4-duVf1-qFDcalg90+XXc?m2rCEh6PwMZKHINh3cB8d1V1O42um?2y*B@{~Td zC_Tfvkt)JxUr)%EjOo;Ic~cm0)sI$ykss!40z#T(`RK(A<-v}Z6Oez8;CE%tl+9x> zaEY*O*8}EC_QAQQ8(<^1ji4p&(^x4LJ`f=E{AN_h!?ST#nwUQrbFPtr0}eooZ*;Le zq#d&DM&griE@tm2H^(}f=~|HdqiljMb(;KUZM`ED+Fx#D)Fl6l3t|5o)_=9S8$Jb) zhamCIPVWWBpBL{S znWdvE-u~VZXl(JBjM?1OU$zY{Y2#ZFf)%A(ud{xi#U3#K(UFka%p8LNb>2(3f~C?5 zd1b5Iw3O{$=z!-n1GO%}B`W?Z(BX7VO_Hb@==&WofW28BYR(ZpVRoWnU*X;Lk8Tog?Ts(W(@F z%Ra=b9UTnkNFRtl`YOylE490;^+Ign-0_^NEyUl%f$2c3Yk~3)I5R?)&i!wbWe#UV zYIDSYw66aVhC2rNO9>3U=XxJcgM1~0)--GSPQ~Sm)z&9Jt;N9Qt_?4QNi*`@e_IIA zgCF`KHKw|b!%U{5KB?yOJ`s&Coa_lPuyBMJY?@C(M$&d^cTO^~z0baEvf4>)%Av$< z8$j;WR7WfR!wchwwWw_ln&0ytsl)D2?a--J(x}Xg_sz+b`tlrBZgEm3D}iUXO^X2u zBuoiwK2HDSPsl%qXpd&-Hbpxk&>p2o9oS`Rt&ozEfpkb*Zu@;oUaA}>ob#xk=PAEj zFnORK`@8MaU_P+(ck@o%#gSjEJI2jGY}3-rsrI-0{PYaGxSBWGjrf`o6Hq~w^;@Yt zCiNZ$6x&Jt@DW0QEM@1)eC?93{*u;>U`>c+HYn}bOw!J<5Zd1wO~9BdZvoCm;i*0p zid_iH@;wxqlg}}4AjrtPK^gMKtM#6Y#($sL*5J;Q`5u_@1V}rbt}S1^Gu;p`wu!tj zr8_=M!^lsW|GeDJYnb5&WaYI%y8pd}Ic1HX8Fw%eg_L?*jkt!U&E&l0eAPS-_$~hp zKQkWez3c~cu8zhS1Cg_42kebg1E)jCyWrM2q(+1jG>yWS`U!O{(EfS6UNt59|NmoE zmdNkjc==uVYzx*px}MKB6|017rkM;W@C4R@eMfKQ3f z560jz(pmaciVUT#m@6R9#7l0A_JRKMsX3w;3qp+)2zV*VDifgc9r3m+%twRMp=E_w z;B!75g-G&Sy131K<%m*HbTmwMGW5F*w)lm~!d9i!CrX&JWEE9$<>PbCn=E>*m>Cv<7bC{8xF+s^b zDkrt;Lt{Cmdf~Y-J;Boh2C@-r*sJj1?!dF|2f?PB^85-kfOar_kjPL8fyVx7+v%hH zkl$t%8N7dUD5L##DJ&^PD5tH$+)w~3Q<|N7T?ulcDCo~ojAzjplKf&i83%A&#N0GQ z&rv0*ydjjAHobqHf0Or`9giy#q7k1MYzijwLn}CcxfaWqiIxj{y92Rm6ciKPTviih z8PSMx7a#a&(;CmD7~s|Nlj=EFAzMM`x&OrySb& z;p}oUu|be!z|46!6ut0!eH(3l++N~^5hZBqeScMsBz8h?BbBT`pIr|*A?n|^t&RAk?m@uJG>td+v?-@rL;3R<Dw=UT{U5J)$_{ z-De>R<(?jg(1V4MFXProCN;{C5Q)ao7c1z^T*yT8U(Pt!#a@atuq4ZV=aD4Xw#N+A zk#kk2#zsVV0Za_Y=TG;Se=%z87%IA)(ydU`f88{XjV;nY-O(cW*&0}GZA8t zSmN0x)g~dClRnC_i;LbBRI#<$7V^BHF}#FkeoVYFUIE_URkkXFJ1QQ>F5&3+wYBul z?#~d?_=rITQFB{@w0}R8!Wg@%V$_lj&W7j>m&)i#7ovm( z$R`TbJS|Lk+m!R|D0w3nHs}$ybDZNm(o%6myQvPsE9`d3)TCT&If#pjGT{QdCsFqI zCoIla3wve@#!9Wx;xG`jhI-lR6Wj_p=@r?c!&rhW`gH(R}_(2J`W7rPS z8*;}SqOPS}gM{S~3i6A&-`eQbe=%w^>@-Ok>|~vV+Q+7~MHFe$fm;~z-I1yIh9QmS zJ{G~kC%%T;EX9bOFkGrpQ4u-9>aqG>kRIF#;MrS8Qo=`9HH@Ww#DGFagD4*h;vWn` zR2smD5zG5sh|V5UCQKbw54ErZEH#Y!I44v|^%7vFV+u9rg92 z#V8=s-v5k#b^im&y8;_9n^Db@Sdp(&4DiqSB`?(9!FwJCeJ@!zpadS3GcGHOCG|J10(-_aWeELP(S{ zM=37Xd)Q%CA{XX$KygQB28fXDKwUuC!td(jBSOsc{Fz4>_U}r-4y?gHQzwDA$Ij0w z7lr;FPqW4-&JHmxN{uSA$LftNKgJ~G#ZN%i?IL+L)Z{s<_lXOirI zt}@xS+7jXOV*vbD1fFhwr~5%m%b`D0>1|rA9OC-;>Tf=>Hq8+IusCy;SdbDsI1>ACYks!V9`%H z`pGsv#@hY!Y0P%{`KdQ3VpNu$xb?qCdqAfZndeIWg#WYf?{h1b9-&+uZ~9&ZAk2gQ zgSd(TK_4@)GY5)*9}zpTh#MZp%$PI!mN;#B(X=e?&Fa@F;broTY0}3`8{0w04{+4*Ar~nC(*_d0H8^ttlhqPk3zm5+?HmCejrE@K@kfMftz3teHp;o<#3{{xS zR7oTT<2j*e?JHG=AT^A(p`i8`w)@qYu@`!e&&Spz?xEGI+1ZLCp*Ix7DzhOsj89}= ztB~7DoXcrH4a6flX^YYQ7>&U?V%6~FnTZ$=ZH=m>~QsJY)Irz}bD?SP=v>h%(52Vyq_ zD;77}gIIrzJNoT$w71rqV`w2eHUmclvhscF>-udIF&2O>y->$5C ziY=3JNB+Qr?$lTBf3<{KKGOoul5jHGCy>45f0)PBTk%Q~HlD zB4B_XAhodr8$O=q*C78Q0Rhr7Q`GxSPTFCV3;NZK{RgIK3VbY8;`IS>luC(Ixmdm9^?o=OT8V=(YJrM`8HrK9~y&Uj+0`O`Q*F?NUM z#)_Ye=5s7oZ?)4OWbkL&_rqDc-YkN+$6`|CafgF{#p4Sn29#^B(90ApoNv!ZDA9f? zkv+e|5W!NqO|GC;Xpi?<^$pvDmFL=f1`2D41b4BI(XcQXv^BZigHAvq4MRV=^P^ZQKH|%0 zG%LCE93XT@=n7Kpmyf(@X1icHZZRpW;3W49I>Ddvg^gk%iulWE+GwLWg9qmX*EUGD zUKRsb?cJM*Y8H%_lNWJ%z%QiqJGTevrcW3n7*kadW7fu`J9q>Y@UIA;fChe;*ye7- zZ!3J=2|{HS2XF9CopW+BqYMU{v?;}6>(V;H8?q=A|6XqY~b0J_X4`9-a!WMm|W zH*|;|9aXj^bjY9RMx;mLTEXrJj;QPQ)G#F${v&#)e@)W+1UE?ofeN(snnw=(JLP^hx zIy$`0L0`};ymIGret)}|b@|nJ|L=pY3c8dA0ORKYp-b4~hGwMNwTh#bX0izIgBa+Z z%DEXaogRZne)!IyA;UV)l*NbS^kQvG6?oRxp4`F8)M1?+x-t{i--}e@3p>njKB{SS z20L^hoGR#V6mS=D3PeW^QRYpKb!`@0YI^PR|CB3(cMs58O+*Z*RAw0&4aHD_k`OndA=g|ySx`MyN13zGSHKHz4){V zkt8))!YC;O#X+DlT}!`RwQjuW_&M)m{XOz4)S0FeLRvi)nB;5G?Z(D1 zxi$IbP=3m2kk-}Lj2{ob>X-nP_#lKw&A+MqU@qXz1p72T-ZPd0x5yOF5phN6tlJk^ zl=q7I?c2P+o!$rIh9qa6Mh;3O9$)|(L_&m3xFO)mpv#lfFa8bPLyC%aLwa?I?&@mEaFS)a=b@pAsT*$z`>Ht>vL{bDJ#iO_E) z4s*Zm7fI4DQa?Q~K#QnX& zbS1m& zD%1mOoVB2y&g7)806yT3_U+DY!Z>r+g5ErL7x)!Nk`-H8aOW4cXqQ}DeZez`_qy!R z^lbMs19McJ=uDxZ%J0U<>M0zs1OBSa3pCgnnr+ z#`|csU7xI{Rwg3%UnS)6 zX+6k^;>83fGW<{yX7fK63g#Km;U6a zY%~fh2)(O07U0iZa&%G9lAPj6S0jn{sh<5LTb9xl(`x><%BUW5YjzYBC%aseQ{P4b zr0INtXhn5@3NiD{eh+*MPJE(j-?_>DM(0t|-$HBK_Jp1^m=`@j641oz-U5T?5Ru3kjHO-MKj>mQzNC%W$9y&Y*69Ve^fkWVD)_Kp`} z24427I5wwFwzB}O4Q+3$t=`>^h*3@KW_~P>WW_f2*KT{dGyA`5**;lo43cHg)LzK3 zQz^tD4ZavN&)Z^XySaXydZ3+&4{w!Q(aM!(EZ_C5x}WA zuK4$;jCgsZ(%_!{dZ5|!vg>i|-LKnl({FLnl!8I4rqq`bGr@|{=EcD-lo|-gR>%X7 zQZ{L_%3`aX!MM#-@GDoQN)PMl>2=@(h7)$)=3FMXbl+Fct*+8-y)3ot_ra0GZ+$$p zm=5r~pJX-}meIm%e)sj5<+RBGgFUnAi4Xekmkn9c41L?OJx?oczziHQ!PkuGXm$4& zff@d$J}@AK!2>MT1Y`o{6-Xtz{YXC{dH?PHSAWgDaU4fePx%yWXhB_5pUUv5e{ybpfx9_3qspf$LEtCm zzaw1@5@uo>;BRUxujF|JUtWPcPWPRSanZkTww9F8V*8iP*7_W+bojpBD);6V$;n!q zG;HVMF2NcFK=|aU#w{Wt#_0DjQ)2nZ$V>IW9QJ5DEE{7}fyiozyLU4yxh@yfQxUTY zltu9(7p*Q_U+SnY0W)-Tmfw#a$=qIHW$3$KT=vO}G&!^ZgZ^fY4RI1aiN|NDG;HMG zE+b{l8AIm$L9X2lD39ll2;*PHiJpB7`wer?9gSD;2wd+c%`L7{<$Wxa4X+vAjcF1pCJ@=bWqa?P8IpDgCN;%z z*2Dd0^B_q^SbvZm!f#Rr-QTAhsrh5cMrGWXjU2`Jn@JmsYiJal)l$>A1l+K`*IFI??RN1F=s9NAH9C^YUd$N@8$@0QsP*!UT_At~ z**bn#8cnDfWl9RoYvLSRVv;S3HX~9YNx$@0iK`zok%A9fYRebj>M*${{^^4aFkVFr zbcIK~fpsHzvmo~-ZZ8LR!iYGzsxJ&jgv|M*j0&@jp5PLQ)ciH}Pa;sE8e{0Ire@iG zxS|s_l)}bI_q0SF))CJ}-QuUeU@mW+p8Gq^L2{B}f_M~ehC`SWTKLHWXI*Y6rWG}y z>2O-YW4U9qNs;Rx6!}&FVaESs0dR}=3gTGo^w;{m+j@ZM*&T|COp}uqe;3aRYnt8G z-&YM@Tc`d+{HuA|YZw82_bLzJV2Bp6(f{IqE(P)MtfPSn-9?y(`ot111r2_@AN`~kktIr30!Cz5Zi24Tj;X-?j91mMpNQfj9>^zu2GtaEDXPqgy zzQY#8a6y7ZWX{Oqdl9t3XTFi!IicGW`g6v(9SZ&x2&CNH3@hQwB2ESaf|6LrAm(u) zba#1Hg#IR%!hIW3Zu+7T*`)d4Qv^-NRM~h7?|%Vb#@GcZ_w()HT!3Vaf$eFOq@Qkg z@I9%MW#Hw9Lb6`*Sabzvv*^bPpsx_mfmGDuIw5&oR;I_9;SDpcY`>3bKmkdO3F5rR{R=ad+AZ+{v>ifA!>gs8OcP@KIq!ZNJEnZJ}s*ms&`!&kAj|1hs z%)jl+T?!u$f8X*lT$&X1@AuQ0O8ngn_yFE~P^k=d*|rVOHHhUmR}nYeLDrIo`{!Du>-K&_#!+5AD9 zQ*-6BFE8{X_02<=3a(NBR7jR$_lrnEM>tg#crMwmCiO+m5cYu*5xeT!H*n^@zg4VF zeRbU*#@{@pU?0z6RlG{9eWRkyFjbakg75hO2__;sN0S2s*h_EH!bQy0D*6Yi;z@;U z5oMyA5m~gq!HXrv{*KU_A&5(dBgIu?jKrs?+seufp=#doaK#T&HTFn|ff$Zrb6RSL zLa?zW#im6{rB9<@w^O|y_M>_3~ZXMyn6$xMsZ+dws7S6a}Fu(FE56t1c*<^={A&hOc#IlcP$xZFls=fQ7We4nF=t z1&Oc*a@Fk(`VZ*#wN>}1 zed}C3&+^X`mVy*C#C~rpN~efh&7kX*XE`*BqUng+>`~%e-4=x<;oS0?56)nItCa!u z%Z@Pz)NHR!4^ton`-yYSDSbgfC}J|+eRF!OiBUb;=8f-zd*^N|PNP;yR8BfBWg zJaQxrTm*1-H@2v)g8O1F4{5LAZycQNUlQE^0?*EEH>5JhG7n<_mr2-Rqz4$bk2RV4 z`h$$+d`d=0%iB--}BDiVL+276AFSUbix(Gh%5kjhBwr zJOEDMB}!^?u@mmf^q?tZG}3njsW_7pANs1)a62U=9QpZ7ik+*(VV%`ED#bgwfQoP4 zX~;CnT373BPX%xA%=Od@q?$}LxthSfSV_ud)E&zD8O!;DO7yM5hLRa*1*I<%y0Ov- z6B|kHa*?(VxFs4Z7V9nJvUp4DME0F^Z*x_|my}7pM}m($Z9Abv!!MO#(LW0z-G0{> zBEOH>vubO%Ih6pC4<3`L3+j=4r{TGlO!vMBEsINIq`=S7(T{^sUPH8O*|sk=!C~}xWyjZQ(<3eq$B_&*$t+u* zm7^p(cNLfIk2HBsH|e|OZ<{W0oKgMDJG3)Z2yjG^pEv5h3H+-^6i_4&iFn1Szjb{} ze;{wk81J65NYAM>m$r98j{64@nOHp~1!5{ZSem`OpcL5Mpr)>#b!jKkPYC55rzM2=vggv~y_ z(e}HZ;t~TeTBbLRtqA?<84!YlV~hD ztfaNU7M~{AZD)-lz~XnpdBhursR&sPC*4$*a##!z*f0=`p4?4jGbX6$lDjVx4DdJb zZ)mqGFN8G+D4>jl{!(sB2F%)qEDsMhoAjoFXYl~2v6QDWOxq{9cNNNSdT)yI*vfxG zOjV&DgT4lINb21zX)-3dJO8adnoMg#DuNg0TRx8K90W$q%pk<5-FqnwLrk@CmQlr- zm>4PQ>c~vv&cFP&8Uy(ms&EJ%Ab&VF|o}Zl&NuqP*SW`zp{e#y9 zvzSR!MN}|sl#Qwo!xg=EvG()^Ydddbp3D4RXfn|1a&;;ey>?RKXRM`yz}b8Eiw#di zx=-109X*QR{fXk9`V2K+^Cc@C?muNzR3f!ahFus(s5*^6RbwdKp7f1~j~pv>c!@lmp}21)ub zMLqW%?_}3nvPCl{=CC-Sht?_q$Lh|bjY|Tq2bbjwj=9FvOm>gYs8D6GJ}v+8TTKzh zKyubMSJJp@qQkv5?Nc0Y2DC6%hvfKTqZ0D-qY|9rn7kuFCGL7oKmiN(;tZ%q9OdPr z<|ZQyLgfI$Ym@F3d=RJ`m|( zW&B&etw52`mj%@SIip{A1#%N+)f({y!RR0T>6_dfXHa|!uD?N}z6qwy&+jer+igeX z-UiUOF056v@Kd>SN1T)p@YAM<~CQB zBMGu3Lo?3D6qMkM4)j!3kt5mC0e9UT&SIV(W-NL zy(WoPpsHLcK+er5&V%_j!H4g^?2iC#a*?Q|Bv-B(IZld$r`@0j=tjeO*pBT>+m1`; zAx%YC6EVO8W)=z;6 z>u`xLq(&GW3xq)`H9v@Hs0D8!r7bOK6iw!S>t_GsTrY@vkm-bczScJ1+ElSSfRLB| zV|ubUVgWtYV{qh?ootI|#N8gx%+k`A(&%(<#e3=%XiHHsv4O_igp>1xk2!brsom&L z!#57JR0(o~g})RN{%rsCmltD2)0l=0lJA>PL*m8dqWkNnNL zxm!Q=r}#+5$(MxHr#zliEXM<=z<`UF5`?*C_N9fmQ9IrpJ-CbKk?Mj9ulrjGobhI2 zUxf0G=b9;l=l$s~dz0a35PsJRejb%?18uh>*!h;NX?W5Bmc$>AQ)tK3L`i7Uc-AP2 zNcva{XuCQsa2a)z4_Zq&}!@u8>lTx7%l2hal*-st31-0V>+BO%V?rMcyaO_@K)aj;m zg>rblI$G*>+59y$vRpfI8P1XZG{D9RMKzu%wymB%+DTpc+dnL@`$6+g*4IO4{C#oJ zYkrmDeSJ<2`Xw{w=4mgEC{5zN_~(#J@4v#x5TTq&p=aHzZsCOt+XbuEAVRj=23k)m zYGd%Fr7SPyPn$qd%>BHMJ)Sw)7VvA4DwL6jl=$m}F^r-~!Sal>fM%~;WPjix$AYsG zogS1Osomu%o4-l#!2u{#ysjGZ`+?*PrQ};DL{wF^}enlc|RIh=XCX{z}G{0bE91vz>zp|2M1p&R90@Woz)mp(f9up(5 z?*#t zg?)jzDzK0`f_Emv67BY-avftFn$mkX0?8T5v`Q@m3kR~$8FT>h5gCboEa0LEAEx;G zks&;}$wV(inn`6R^`Dl+Q+_o;k7e}lp_g8dP`D1mlYtc#*Y`uyrno7-W=5y}DG+v_ zI}#+6)O`hMepXh#yPY4^Ah*9fqXlGQ6D1&vqtqIR{R6`X#2m2t2tHWreB_8vnV@O?7dT{=!|K~w^%0C0!23=b`3o3^v z%6tMQaO>hf`iwe|h_qpt1RfR)8d@JAwhSd9t*&wHxj`!nc=?Mh$apW*!mUAqbq*a) z_3RA_uu+I|IcKVI&iQ=IRr>|tjl=wJFh7!orGduDueZm!5v z+uWm2BpK-^-BN4aM4O~$fJxc<2s)fYr@xM&WwGjdJ3ye=Jn_<9|{A{>S z;^79W7T+y?)c{=Gj+HBGxneJvwyB*X%T3xG#p0x z!e;={C3K=@M-?GEe*7$W9!OSLctS?MEvweUTFF4jyA+C;yxYN;plcE>=G?~N4ThgA zDTSM);x{$O#;}7~SxEU06E8xOs=;cRVMyogyHx&Ra^bpOZdC8Nj z8~#dgq4f9pQYjr8P7vder8Y3La7Z-D(hv_el*C%h5prSL731QbgQw#VRkqw)M9kZB zZKrYz8{WnGdaUzAWC&N&9~Vc7Pk%1qIm09QRG4Z~ajzGaB03V!VVaPr9@e6an{evo|e8Jbx_1`?+VPw@tpFxeN0z$`UlF@~d}R!j_=N z7Ut6h7^GBt0CE=-Yf5^FTXwGVEe(1!rvz$52(#)}W*!Y?yav~liml+Klbi1`@))h7 z#}aw(l~_C2+UHBUQQZt5g_34F81uu)O^Ugwe&>E7V+fX1i!1sXFrU36RHRtQ^s65FjwONV-$&WMubdeJh&BAsmUM*Lp5`d5J&Jv# zQkSue1#K#(jb|0G;Q&!)h`lyB4D=oTyf2cJNO*k(I&%T#4ȢZTD2T$aT^M>P5m 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 diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java deleted file mode 100644 index c4cf0cdccd..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/EmptyGraphBuilder.java +++ /dev/null @@ -1,20 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; - -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributor.DataDistributorException; -import edu.cornell.library.scholars.webapp.controller.api.distribute.DataDistributorContext; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.rdf.model.ModelFactory; - -/** - * Creates an empty RDF graph. For use as a placeholder or in tests. - */ -public class EmptyGraphBuilder extends AbstractGraphBuilder { - - @Override - public Model buildGraph(DataDistributorContext ddContext) throws DataDistributorException { - return ModelFactory.createDefaultModel(); - } - -} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java deleted file mode 100644 index 150251aadf..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ParallelGraphBuilder.java +++ /dev/null @@ -1,14 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; - -/** - * TODO - * - * Iterate like the drill-down builder (share code?) Each sub-request is handled - * by a thread-pool, whose size can be configured. Requires the use of - * ThreadsafeGraphBuilders. - */ -public class ParallelGraphBuilder { - -} diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java deleted file mode 100644 index 22fccffd20..0000000000 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/distribute/rdf/graphbuilder/ThreadsafeGraphBuilder.java +++ /dev/null @@ -1,13 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder; - -/** - * TODO - * - * Marker interface asserting that the GraphBuilder may be called by several - * threads at once without conflict - */ -public interface ThreadsafeGraphBuilder { - -} diff --git a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl index 1d471e5092..93f6d32d05 100644 --- a/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl +++ b/home/src/main/resources/rdf/i18n/en_US/interface-i18n/firsttime/vitro_UiLabel.ttl @@ -6537,20 +6537,6 @@ uil-data:dd_config_run.Vitro uil:hasApp "Vitro" ; uil:hasKey "dd_config_run" . -uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor.Vitro - rdf:type owl:NamedIndividual ; - rdf:type uil:UILabel ; - rdfs:label "JavaScript Transfrom Distributor"@en-US ; - uil:hasApp "Vitro" ; - uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.decorator.JavaScriptTransformDistributor" . - -uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor.Vitro - rdf:type owl:NamedIndividual ; - rdf:type uil:UILabel ; - rdfs:label "Hello Distributor"@en-US ; - uil:hasApp "Vitro" ; - uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.examples.HelloDistributor" . - uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.file.FileDistributor.Vitro rdf:type owl:NamedIndividual ; rdf:type uil:UILabel ; @@ -6600,13 +6586,6 @@ uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute uil:hasApp "Vitro" ; uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.DrillDownGraphBuilder" . -uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder.Vitro - rdf:type owl:NamedIndividual ; - rdf:type uil:UILabel ; - rdfs:label "Empty Graph Builder"@en-US ; - uil:hasApp "Vitro" ; - uil:hasKey "dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.EmptyGraphBuilder" . - uil-data:dd_config_edu.cornell.library.scholars.webapp.controller.api.distribute.rdf.graphbuilder.IteratingGraphBuilder.Vitro rdf:type owl:NamedIndividual ; rdf:type uil:UILabel ; From e043cec01c0fa453bdd75fd0bbd393e2afa4c6d9 Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 7 Feb 2025 15:42:35 +0100 Subject: [PATCH 18/19] Adjustments to require java 11. --- api/pom.xml | 22 ++++++++++++++++++++++ installer/pom.xml | 3 ++- pom.xml | 3 +-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index c85a0cb5ff..b34ea6e96a 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,9 +21,31 @@ + + maven-enforcer-plugin + 3.5.0 + + + enforce-java + validate + + enforce + + + + + 11 + You are running an incompatible version of Java. Minimum required version is 11. + + + + + + org.apache.maven.plugins maven-surefire-plugin + 2.12.4 alphabetical diff --git a/installer/pom.xml b/installer/pom.xml index 0110832a65..36ac40304a 100644 --- a/installer/pom.xml +++ b/installer/pom.xml @@ -76,7 +76,7 @@ maven-enforcer-plugin - 1.4.1 + 3.5.0 org.apache.maven.plugins @@ -206,6 +206,7 @@ maven-enforcer-plugin + 3.5.0 enforce-properties diff --git a/pom.xml b/pom.xml index 017d7a4228..e717421a12 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ false / UTF-8 + 11 @@ -255,8 +256,6 @@ org.apache.maven.plugins maven-compiler-plugin - 1.8 - 1.8 UTF-8 -XDcompilePolicy=simple From 301dfd103874d1f01779133a1fd93e4e0ee671ba Mon Sep 17 00:00:00 2001 From: Georgy Litvinov Date: Fri, 14 Feb 2025 13:26:01 +0100 Subject: [PATCH 19/19] Removed CORS headers from DistributeDataApiController --- .../webapp/controller/api/DistributeDataApiController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java index 47b107ec63..3b0a61a0da 100644 --- a/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java +++ b/api/src/main/java/edu/cornell/library/scholars/webapp/controller/api/DistributeDataApiController.java @@ -53,7 +53,6 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se String uri = findDistributorForAction(req, model); DataDistributor instance = instantiateDistributor(req, uri, model); - setCorsHeaders(req, resp); runIt(req, resp, instance); } catch (NoSuchActionException e) { do400BadRequest(e.getMessage(), resp); @@ -128,11 +127,6 @@ private void runIt(HttpServletRequest req, HttpServletResponse resp, DataDistrib } } - private void setCorsHeaders(HttpServletRequest req, HttpServletResponse resp) { - log.debug("Setting CORS header for every request."); - resp.setHeader("Access-Control-Allow-Origin", "*"); - } - private void do400BadRequest(String message, HttpServletResponse resp) throws IOException { log.debug("400BadRequest: " + message); resp.setStatus(400);