diff --git a/aggregated-cnd/pom.xml b/aggregated-cnd/pom.xml
new file mode 100644
index 0000000000..d79064905d
--- /dev/null
+++ b/aggregated-cnd/pom.xml
@@ -0,0 +1,54 @@
+
+
+
+ 4.0.0
+
+
+ io.uhndata.cards
+ cards-parent
+ 0.9-SNAPSHOT
+
+
+ cards-aggregated-cnd
+ bundle
+ CARDS - The aggregated CND files for unit testing
+
+
+
+
+ org.apache.sling
+ slingfeature-maven-plugin
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ {maven-resources}
+ SLING-INF/nodetypes/aggregated.cnd
+
+
+
+
+
+
+
diff --git a/aggregated-cnd/src/main/features/feature.json b/aggregated-cnd/src/main/features/feature.json
new file mode 100644
index 0000000000..88b4bc8ba5
--- /dev/null
+++ b/aggregated-cnd/src/main/features/feature.json
@@ -0,0 +1,25 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+{
+ "bundles":[
+ {
+ "id":"${project.groupId}:${project.artifactId}:${project.version}",
+ "start-order":"25"
+ }
+ ]
+}
diff --git a/aggregated-cnd/src/main/resources/SLING-INF/nodetypes/aggregated.cnd b/aggregated-cnd/src/main/resources/SLING-INF/nodetypes/aggregated.cnd
new file mode 100644
index 0000000000..7a88bf8696
--- /dev/null
+++ b/aggregated-cnd/src/main/resources/SLING-INF/nodetypes/aggregated.cnd
@@ -0,0 +1,98 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+
+
+
+//-----------------------------------------------------------------------------
+// Nodetype for data query handling
+[cards:dataQuery] > sling:Folder
+ // Attributes:
+ query
+
+ // Properties:
+ // Hardcode the resource type
+ - sling:resourceType (STRING) = "cards/dataQuery" mandatory autocreated protected
+ // Hardcode the resource supertype: the dataQuery is a resource
+ - sling:resourceSuperType (STRING) = "cards/Resource" mandatory autocreated protected
+ // Set a default title
+ - title (String) = "CARDS query" mandatory autocreated
+
+//-----------------------------------------------------------------------------
+// The configuration for the quick search widget.
+[cards:QuickSearchConfiguration] > sling:Folder
+ // Attributes:
+
+ // We can use this in a query.
+ query
+
+ // Properties:
+
+ // Hardcode the resource type.
+ - sling:resourceType (STRING) = "cards/QuickSearchConfiguration" mandatory autocreated protected
+
+ // Hardcode the resource supertype: this is a resource.
+ - sling:resourceSuperType (STRING) = "cards/Resource" mandatory autocreated protected
+
+ // What types of resources will be queried.
+ // Default: all.
+ - allowedResourceTypes (STRING) multiple
+
+ // How many results should be displayed.
+ - limit (long) = '5'
+
+ // Whether to show the total number of results.
+ // Can be set to 'false' in case of performance issues.
+ - showTotalRows (BOOLEAN) = 'true'
+
+//-----------------------------------------------------------------------------
+[cards:QueryCache] > nt:unstructured
+ // Attributes
+
+ // We can query the cache for saved counts.
+ query
+
+ // Properties
+
+ // Hardcode the resource type.
+ - sling:resourceType (STRING) = "cards/QueryCache" mandatory autocreated protected
+
+ // Hardcode the resource supertype: each queryCache is a resource.
+ - sling:resourceSuperType (STRING) = "cards/Resource" mandatory autocreated protected
+
+ // Children
+
+//-----------------------------------------------------------------------------
+// The homepage for the QueryCache space.
+[cards:QueryCacheHomepage] > sling:Folder
+ // Attributes:
+
+ // We can use this homepage in a query.
+ query
+
+ // Properties:
+
+ // Hardcode the resource type.
+ - sling:resourceType (STRING) = "cards/QueryCacheHomepage" mandatory autocreated protected
+
+ // Hardcode the resource supertype: the QueryCacheHomepage is a resource homepage.
+ - sling:resourceSuperType (STRING) = "cards/ResourceHomepage" mandatory autocreated protected
+
+ // Children
+ + * (cards:QueryCache)
diff --git a/modules/data-entry/pom.xml b/modules/data-entry/pom.xml
index c2cceffc6a..77f1a20c51 100644
--- a/modules/data-entry/pom.xml
+++ b/modules/data-entry/pom.xml
@@ -30,6 +30,10 @@
bundle
CARDS - Data entry module
+
+ 0.90
+
+
@@ -68,6 +72,18 @@
+
+
+ maven-compiler-plugin
+
+
+ -Werror
+
+ true
+ true
+ true
+
+
@@ -135,5 +151,79 @@
javax.servlet
javax.servlet-api
+
+ ${project.groupId}
+ cards-data-model-subjects-api
+ ${project.version}
+ test
+
+
+ ${project.groupId}
+ cards-data-model-forms-api
+ ${project.version}
+ test
+
+
+ ${project.groupId}
+ cards-aggregated-cnd
+ ${project.version}
+ test
+
+
+ org.apache.sling
+ org.apache.sling.resourcebuilder
+ 1.0.4
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.sling-mock.core
+ 3.4.2
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.sling-mock.junit4
+ 3.4.2
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.sling-mock-oak
+ 3.1.4-1.40.0
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.jcr-mock
+ 1.5.4
+ test
+
+
+ org.osgi
+ org.osgi.framework
+ 1.8.0
+ test
+
+
+ com.google.guava
+ guava
+ 20.0
+ test
+
+
+ junit
+ junit
+
+
+ org.mockito
+ mockito-core
+
+
+ org.assertj
+ assertj-core
+ 3.24.2
+ test
+
diff --git a/modules/data-entry/src/main/java/io/uhndata/cards/DeleteServlet.java b/modules/data-entry/src/main/java/io/uhndata/cards/DeleteServlet.java
index dbbed9e7f5..fd4a996360 100644
--- a/modules/data-entry/src/main/java/io/uhndata/cards/DeleteServlet.java
+++ b/modules/data-entry/src/main/java/io/uhndata/cards/DeleteServlet.java
@@ -538,7 +538,6 @@ private static void sendJsonError(final SlingHttpServletResponse response, int s
.writeEnd();
}
jsonGen.writeEnd().close();
- response.setStatus(sc);
}
/**
diff --git a/modules/data-entry/src/main/java/io/uhndata/cards/FilterServlet.java b/modules/data-entry/src/main/java/io/uhndata/cards/FilterServlet.java
index a135cc4f54..86b6b1329f 100644
--- a/modules/data-entry/src/main/java/io/uhndata/cards/FilterServlet.java
+++ b/modules/data-entry/src/main/java/io/uhndata/cards/FilterServlet.java
@@ -189,7 +189,7 @@ private void copyFields(JsonObject question, String key, JsonObjectBuilder build
if (seenTypes.containsKey(key)) {
// If this question already exists, make sure that it has the same dataType
String questionType = question.getString("dataType");
- if (seenTypes.get(key) != questionType) {
+ if (!seenTypes.get(key).equals(questionType)) {
// DIFFERENT -- prepend a slightly differently named version
String newKey = questionType.concat("|").concat(key);
seenTypes.put(newKey, questionType);
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/CountServletTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/CountServletTest.java
new file mode 100644
index 0000000000..2e347ccbc2
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/CountServletTest.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.resource.*;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link CountServlet}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class CountServletTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String SUBJECT_TYPE = "cards:Subject";
+ private static final String QUESTIONNAIRE_TYPE = "cards:Questionnaire";
+ private static final String QUESTION_TYPE = "cards:Question";
+ private static final String FORM_TYPE = "cards:Form";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String COUNT_PROPERTY = "count";
+ private static final String COUNT_TYPE_PROPERTY = "countType";
+ private static final String RESOURCE_TYPE_PROPERTY = "resourceType";
+ private static final String TEST_FORM_PATH = "/Forms/f1";
+ private static final String TEST_SUBJECT_PATH = "/Subjects/r1";
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private CountServlet countServlet;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void doGetForExistingParametersWritesNotEmptyResponse() throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionnaireUuid = session.getNode(TEST_QUESTIONNAIRE_PATH).getIdentifier();
+
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource("/Forms"));
+ request.setRemoteUser("admin");
+ request.setParameterMap(Map.of(
+ "includeallstatus", new String[]{"true"},
+ "filternames", new String[]{QUESTIONNAIRE_TYPE},
+ "filtervalues", new String[]{questionnaireUuid},
+ "filtercomparators", new String[]{"="},
+ "filtertypes", new String[]{QUESTIONNAIRE_PROPERTY},
+
+ "fieldname", new String[]{NODE_TYPE},
+ "fieldcomparator", new String[]{"="},
+ "fieldvalue", new String[]{FORM_TYPE}));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.countServlet.doGet(request, response);
+ assertEquals("UTF-8", response.getCharacterEncoding());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ JsonObject responseJson = reader.readObject();
+ assertTrue(responseJson.containsKey(COUNT_PROPERTY));
+ assertEquals(Json.createValue(1), responseJson.get(COUNT_PROPERTY));
+
+ NodeIterator queryCacheNodeChildren = session.getNode("/QueryCache").getNodes();
+ assertTrue(queryCacheNodeChildren.hasNext());
+ Node queryCacheNode = queryCacheNodeChildren.nextNode();
+ assertTrue(queryCacheNode.hasProperty(COUNT_TYPE_PROPERTY));
+ assertEquals("=", queryCacheNode.getProperty(COUNT_TYPE_PROPERTY).getString());
+ assertTrue(queryCacheNode.hasProperty(COUNT_PROPERTY));
+ assertEquals(1, queryCacheNode.getProperty(COUNT_PROPERTY).getLong());
+ assertTrue(queryCacheNode.hasProperty("time"));
+ assertTrue(queryCacheNode.hasProperty(RESOURCE_TYPE_PROPERTY));
+ assertEquals("Forms", queryCacheNode.getProperty(RESOURCE_TYPE_PROPERTY).getString());
+
+ assertTrue(queryCacheNode.hasProperty("cards:Questionnaire="));
+ assertEquals(questionnaireUuid, queryCacheNode.getProperty("cards:Questionnaire=").getString());
+
+ assertTrue(queryCacheNode.hasProperty(NODE_TYPE + "="));
+ assertEquals(FORM_TYPE, queryCacheNode.getProperty(NODE_TYPE + "=").getString());
+ }
+
+ @Test
+ public void doGetForSpecialEmptyFilterWritesEmptyResponse() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource("/Forms"));
+ request.setRemoteUser("admin");
+ request.setParameterMap(Map.of("filterempty", new String[]{QUESTIONNAIRE_TYPE}));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.countServlet.doGet(request, response);
+ assertEquals("UTF-8", response.getCharacterEncoding());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ JsonObject responseJson = reader.readObject();
+ assertTrue(responseJson.containsKey(COUNT_PROPERTY));
+ assertEquals(Json.createValue("0"), responseJson.get(COUNT_PROPERTY));
+ }
+
+ @Test
+ public void doGetForNotSpecialEmptyAndNonEmptyFilterWritesNotEmptyResponse() throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource("/Forms"));
+ request.setRemoteUser("admin");
+ request.setParameterMap(Map.of(
+ "filterempty", new String[]{QUESTION_TYPE},
+ "filternotempty", new String[]{SUBJECT_TYPE}));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.countServlet.doGet(request, response);
+ assertEquals("UTF-8", response.getCharacterEncoding());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ JsonObject responseJson = reader.readObject();
+ assertTrue(responseJson.containsKey(COUNT_PROPERTY));
+ assertEquals(Json.createValue("0"), responseJson.get(COUNT_PROPERTY));
+
+ NodeIterator queryCacheNodeChildren = session.getNode("/QueryCache").getNodes();
+ assertTrue(queryCacheNodeChildren.hasNext());
+ Node queryCacheNode = queryCacheNodeChildren.nextNode();
+ assertTrue(queryCacheNode.hasProperty(COUNT_TYPE_PROPERTY));
+ assertTrue(queryCacheNode.hasProperty(COUNT_PROPERTY));
+ assertTrue(queryCacheNode.hasProperty("time"));
+ assertTrue(queryCacheNode.hasProperty(RESOURCE_TYPE_PROPERTY));
+ assertEquals("=", queryCacheNode.getProperty(COUNT_TYPE_PROPERTY).getString());
+ assertEquals(0, queryCacheNode.getProperty(COUNT_PROPERTY).getLong());
+ assertEquals("Forms", queryCacheNode.getProperty(RESOURCE_TYPE_PROPERTY).getString());
+
+ assertTrue(queryCacheNode.hasProperty(SUBJECT_TYPE));
+ assertEquals("is not empty", queryCacheNode.getProperty(SUBJECT_TYPE).getString());
+
+ assertTrue(queryCacheNode.hasProperty(QUESTION_TYPE));
+ assertEquals("is empty", queryCacheNode.getProperty(QUESTION_TYPE).getString());
+
+ }
+
+ @Test
+ public void doGetForNotAdminRemoteUserWritesError() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setRemoteUser("notAdmin");
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.countServlet.doGet(request, response);
+ assertEquals(403, response.getStatus());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ JsonObject responseJson = reader.readObject();
+ assertTrue(responseJson.containsKey("status"));
+ assertEquals(Json.createValue("error"), responseJson.get("status"));
+ assertTrue(responseJson.containsKey("error"));
+ assertEquals(Json.createValue("Only admin can perform this operation."), responseJson.get("error"));
+ }
+
+ @Test
+ public void doGetForNotAdminRemoteUserCatchesException() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setRemoteUser("notAdmin");
+ SlingHttpServletResponse response = mock(SlingHttpServletResponse.class);
+ when(response.getWriter()).thenThrow(new IOException());
+ this.countServlet.doGet(request, response);
+ verify(response).setStatus(403);
+ verify(response).setContentType("application/json;charset=UTF-8");
+ verify(response).getWriter();
+ }
+
+ @Before
+ public void setUp() throws RepositoryException, LoginException
+ {
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage")
+ .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage")
+ .resource("/Forms", NODE_TYPE, "cards:FormsHomepage")
+ //.resource("/QueryCache", NODE_TYPE, "sling:Folder")
+ .resource("/QueryCache", NODE_TYPE, "cards:QueryCacheHomepage")
+ .commit();
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root");
+ this.context.build()
+ .resource(TEST_SUBJECT_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ "type",
+ this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class))
+ .commit();
+ final Session session = this.context.resourceResolver().adaptTo(Session.class);
+ Node subject = session.getNode(TEST_SUBJECT_PATH);
+ Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH);
+ Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1");
+
+ this.context.build()
+ .resource(TEST_FORM_PATH,
+ NODE_TYPE, FORM_TYPE,
+ "subject", subject,
+ QUESTIONNAIRE_PROPERTY, questionnaire)
+ .resource(TEST_FORM_PATH + "/a1",
+ NODE_TYPE, "cards:Answer",
+ "question", question)
+ .commit();
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+ }
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/DataImportServletTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/DataImportServletTest.java
new file mode 100644
index 0000000000..5ead1481d2
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/DataImportServletTest.java
@@ -0,0 +1,613 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Workspace;
+import javax.jcr.query.Query;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.assertj.core.api.Assertions;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link DataImportServlet}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DataImportServletTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String SUBJECT_TYPE = "cards:Subject";
+ private static final String ANSWER_OPTION_TYPE = "cards:AnswerOption";
+ private static final String FORM_TYPE = "cards:Form";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String SUBJECT_PROPERTY = "subject";
+ private static final String RELATED_SUBJECTS_PROPERTY = "relatedSubjects";
+ private static final String VALUE_PROPERTY = "value";
+ private static final String TYPE_PROPERTY = "type";
+ private static final String IDENTIFIER_PROPERTY = "identifier";
+ private static final String LABEL_PROPERTY = "label";
+
+ // Paths
+ private static final String ROOT_FORM_PATH = "/Forms";
+ private static final String TEST_SUBJECT_PATH = "/Subjects/r1";
+ private static final String TEST_SUBJECT_2_PATH = "/Subjects/r2";
+ private static final String ROOT_SUBJECT_TYPE = "/SubjectTypes/Root";
+ private static final String BRANCH_SUBJECT_TYPE = "/SubjectTypes/Root/Branch";
+ private static final String PATIENT_SUBJECT_TYPE = "/SubjectTypes/Patient";
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+ private static final String TEST_TEXT_QUESTIONNAIRE_PATH = "/Questionnaires/TestTextQuestionnaire";
+
+ // Parameters
+ private static final String SUBJECT_TYPE_PARAMETER = ":subjectType";
+ private static final String DATA_PARAMETER = ":data";
+ private static final String QUESTIONNAIRE_PARAMETER = ":questionnaire";
+ private static final String PATCH_PARAMETER = ":patch";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private DataImportServlet dataImportServlet;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void doPostWithoutDataTypeParameterValueSendsError() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
+ assertEquals("Required parameter \":data\" missing", response.getStatusMessage());
+ }
+
+ @Test
+ public void doPostWithoutQuestionnaireTypeParameterValueSendsError() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, "",
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
+ assertEquals("Required parameter \":questionnaire\" missing", response.getStatusMessage());
+ }
+
+ @Test
+ public void doPostWithInvalidQuestionnaireTypeParameterValueSendsError() throws IOException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ String invalidQuestionnaireName = "/Questionnaires/InvalidQuestionnaire";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, "",
+ QUESTIONNAIRE_PARAMETER, invalidQuestionnaireName,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus());
+ assertEquals("Invalid questionnaire name " + invalidQuestionnaireName, response.getStatusMessage());
+ }
+
+ @Test
+ public void doPostUpdatesValueInTextAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tText Question\tText Question_notes\r\n"
+ + "f3\tRoot Subject\tnewValue\tnewNote";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_TEXT_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ Node form = this.context.resourceResolver().getResource("/Forms/f3").adaptTo(Node.class);
+ assertEquals("newValue", form.getNode("a1").getProperty(VALUE_PROPERTY).getString());
+ assertEquals("newNote", form.getNode("a1").getProperty("note").getString());
+ }
+
+ @Test
+ public void doPostCreatesValueInTextAnswerOfDoubleNestedSection() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tText Question\r\n"
+ + "f1\tRoot Subject\tnewValue";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ Node form = this.context.resourceResolver().getResource("/Forms/f1").adaptTo(Node.class);
+ assertTrue(form.getNode("s1").getNode("a6").hasProperty(VALUE_PROPERTY));
+ assertEquals(1, form.getNode("s1").getNode("a6").getProperty(VALUE_PROPERTY).getValues().length);
+ assertEquals("newValue",
+ form.getNode("s1").getNode("a6").getProperty(VALUE_PROPERTY).getValues()[0].getString());
+ }
+
+ @Test
+ public void doPostCreatesValueInLongAnswerForChildSubject() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tBranch ID\tLong Question\r\n"
+ + "f2\tRoot Subject\tBranch Subject\t100";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, new String[]{ROOT_SUBJECT_TYPE, BRANCH_SUBJECT_TYPE},
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+ Node form = this.context.resourceResolver().getResource("/Forms/f2").adaptTo(Node.class);
+ assertEquals(100, form.getNode("a1").getProperty(VALUE_PROPERTY).getLong());
+ }
+
+ @Test
+ public void doPostCreatesSubjectOfPatientTypeAndCreatesNewFormWithTextValueAnswers() throws IOException,
+ RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tPatient ID\tText Question\tText2 Question\r\n"
+ + "f5\tPatient Subject\tnewValue\tnewValue2";
+ request.setParameterMap(Map.of(
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node formBySubject = getFormOfPatientSubjectType();
+ NodeIterator sectionAnswers = formBySubject.getNodes().nextNode().getNodes();
+ assertEquals("newValue", sectionAnswers.nextNode().getProperty(VALUE_PROPERTY).getValues()[0].getString());
+ assertEquals("newValue2", sectionAnswers.nextNode().getProperty(VALUE_PROPERTY).getValues()[0].getString());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithLongValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tLong Question\r\n"
+ + "f5\tRoot2 Subject\t100";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+
+ assertEquals(100, form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getLong());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithDoubleValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tDouble Question\r\n"
+ + "f5\tRoot2 Subject\t100";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+
+ assertEquals(100.0, form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getDouble(), 0);
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithDecimalValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tDecimal Question\r\n"
+ + "f5\tRoot2 Subject\t100";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+
+ assertEquals(BigDecimal.valueOf(100), form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getDecimal());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithBooleanValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tBoolean Question\r\n"
+ + "f5\tRoot2 Subject\ttrue";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+ assertEquals(1, form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getLong());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithVocabularyValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tOptions Question\r\n"
+ + "f5\tRoot2 Subject\t/Vocabularies/Option1";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+ assertEquals("/Vocabularies/Option1", form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getString());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithVocabularyValueAnswerIgnoresCase() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tOptions Question\r\n"
+ + "f5\tRoot2 Subject\t/VOCABULARIES/OPTION2";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+ assertEquals("/Vocabularies/Option2", form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getString());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithTimeValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tTime Question\r\n"
+ + "f5\tRoot2 Subject\t01:23:13";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+ assertEquals("01:23:13", form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getString());
+ }
+
+ @Test
+ public void doPostCreatesNewFormWithDateValueAnswer() throws IOException, RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ final String date = "2023-01-01";
+ String dataCsv = "Identifier\tRoot ID\tDate Question\r\n"
+ + "f5\tRoot2 Subject\t" + date;
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ assertEquals(date, sdf.format(form.getNodes().nextNode().getProperty(VALUE_PROPERTY).getDate().getTime()));
+ }
+
+ @Test
+ public void doPostCatchesRepositoryException() throws RepositoryException
+ {
+ ResourceResolver resolver = mock(ResourceResolver.class);
+ Session mockedSession = mock(Session.class);
+ Workspace mockedWorkspace = mock(Workspace.class);
+ when(resolver.getResource(eq(ROOT_FORM_PATH))).thenReturn(this.resourceResolver.getResource(ROOT_FORM_PATH));
+ when(resolver.getResource(eq("/Subjects"))).thenReturn(this.resourceResolver.getResource("/Subjects"));
+ when(resolver.adaptTo(Session.class)).thenReturn(mockedSession);
+ when(mockedSession.getWorkspace()).thenReturn(mockedWorkspace);
+ when(mockedWorkspace.getQueryManager()).thenThrow(new RepositoryException());
+
+ MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(resolver, this.slingBundleContext);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ Assertions.assertThatCode(() -> this.dataImportServlet.doPost(request, response)).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void doPostCatchesNumberFormatExceptionAndCreatesNewFormWithAnswerWithoutValue() throws IOException,
+ RepositoryException
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tRoot ID\tDouble Question\r\n"
+ + "f5\tRoot2 Subject\tnotParsableValue";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "false"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ this.dataImportServlet.doPost(request, response);
+
+ Node subject = this.context.resourceResolver().getResource(TEST_SUBJECT_2_PATH).adaptTo(Node.class);
+ Node form = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subject.getIdentifier());
+
+ assertFalse(form.getNodes().nextNode().hasProperty(VALUE_PROPERTY));
+ }
+
+ @Test
+ public void doPostForUnrealSubject()
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+
+ String dataCsv = "Identifier\tUnreal ID\tText Question\r\n"
+ + "f3\tUnreal Subject\tnewValue";
+ request.setParameterMap(Map.of(
+ SUBJECT_TYPE_PARAMETER, ROOT_SUBJECT_TYPE,
+ DATA_PARAMETER, dataCsv,
+ QUESTIONNAIRE_PARAMETER, TEST_TEXT_QUESTIONNAIRE_PATH,
+ PATCH_PARAMETER, "true"
+ ));
+
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ Assertions.assertThatCode(() -> this.dataImportServlet.doPost(request, response)).doesNotThrowAnyException();
+ }
+
+ @Before
+ public void setUp() throws RepositoryException, LoginException
+ {
+ final Session session = this.context.resourceResolver().adaptTo(Session.class);
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage")
+ .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage")
+ .resource(ROOT_FORM_PATH, NODE_TYPE, "cards:FormsHomepage")
+ .resource("/Vocabularies", NODE_TYPE, "sling:Folder")
+ .commit();
+
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/TextQuestionnaires.json", TEST_TEXT_QUESTIONNAIRE_PATH);
+ this.context.load().json("/SubjectTypes.json", ROOT_SUBJECT_TYPE);
+ this.context.load().json("/SubjectTypesPatient.json", PATIENT_SUBJECT_TYPE);
+
+ this.context.build()
+ .resource(TEST_SUBJECT_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(ROOT_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Root Subject")
+ .resource(TEST_SUBJECT_PATH + "/b1",
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(BRANCH_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Branch Subject")
+
+ .resource(TEST_SUBJECT_2_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(ROOT_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Root2 Subject")
+ .commit();
+
+ Node rootSubject = session.getNode(TEST_SUBJECT_PATH);
+ Node branchSubject = session.getNode(TEST_SUBJECT_PATH + "/b1");
+
+ this.context.build()
+ .resource("/Vocabularies/Option1",
+ NODE_TYPE, ANSWER_OPTION_TYPE,
+ LABEL_PROPERTY, "Option 1",
+ VALUE_PROPERTY, "O1")
+ .resource("/Vocabularies/Option2",
+ NODE_TYPE, ANSWER_OPTION_TYPE,
+ LABEL_PROPERTY, "Option 2",
+ VALUE_PROPERTY, "O2")
+
+ .resource("/Forms/f1",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .resource("/Forms/f1/s1",
+ NODE_TYPE, "cards:AnswerSection",
+ "section", session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1"))
+ .resource("/Forms/f1/s1/a6",
+ NODE_TYPE, "cards:TextAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1/question_6"))
+
+ .resource("/Forms/f2",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, branchSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject, branchSubject).toArray())
+ .resource("/Forms/f2/a1",
+ NODE_TYPE, "cards:LongAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1"))
+
+ .resource("/Forms/f3",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .resource("/Forms/f3/a1",
+ NODE_TYPE, "cards:TextAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH + "/question_1"),
+ VALUE_PROPERTY, "12345")
+ .commit();
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+
+ }
+
+ private Node getFormOfPatientSubjectType() throws RepositoryException
+ {
+ Node subjectType = this.context.resourceResolver().getResource(PATIENT_SUBJECT_TYPE).adaptTo(Node.class);
+ Node subjectBySubjectType = getNodeBySearchParam("Subject", TYPE_PROPERTY, subjectType.getIdentifier());
+ assertNotNull(subjectBySubjectType);
+
+ Node formBySubject = getNodeBySearchParam("Form", SUBJECT_PROPERTY, subjectBySubjectType.getIdentifier());
+ assertNotNull(formBySubject);
+ return formBySubject;
+ }
+
+ private Node getNodeBySearchParam(String nodeType, String searchParamName, String searchParamValue)
+ throws RepositoryException
+ {
+ String query = String.format("select n from [cards:%s] as n where n.%s = '%s'",
+ nodeType, searchParamName, searchParamValue);
+ Query queryObj = this.resourceResolver.adaptTo(Session.class).getWorkspace().getQueryManager()
+ .createQuery(query, "JCR-SQL2");
+ queryObj.setLimit(1);
+ NodeIterator nodeResult = queryObj.execute().getNodes();
+
+ if (nodeResult.hasNext()) {
+ return nodeResult.nextNode();
+ }
+ return null;
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/DateUtilsTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/DateUtilsTest.java
new file mode 100644
index 0000000000..fcd67854b2
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/DateUtilsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.uhndata.cards;
+
+import java.time.Instant;
+import java.util.TimeZone;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link DateUtils}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DateUtilsTest
+{
+ @Test
+ public void getTimezoneForDateString()
+ {
+ String timezone = DateUtils.getTimezoneForDateString("2023-01-01");
+ String expectedTimezone = TimeZone.getDefault().toZoneId().getRules().getStandardOffset(Instant.now()).getId();
+ assertEquals(expectedTimezone, timezone);
+ }
+
+ @Test
+ public void getTimezoneForDateStringCatchesException()
+ {
+ String timezone = DateUtils.getTimezoneForDateString("notParsable");
+ String expectedTimezone = TimeZone.getDefault().toZoneId().getRules().getOffset(Instant.now()).getId();
+ assertEquals(expectedTimezone, timezone);
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/DeleteServletTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/DeleteServletTest.java
new file mode 100644
index 0000000000..ed64d2c1e7
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/DeleteServletTest.java
@@ -0,0 +1,430 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Workspace;
+import javax.jcr.version.VersionManager;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link DeleteServlet}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DeleteServletTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String SUBJECT_TYPE = "cards:Subject";
+ private static final String SUBJECT_TYPE_TYPE = "cards:SubjectType";
+ private static final String QUESTIONNAIRE_TYPE = "cards:Questionnaire";
+ private static final String FORM_TYPE = "cards:Form";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String SECTION_PROPERTY = "section";
+ private static final String SUBJECT_PROPERTY = "subject";
+ private static final String RELATED_SUBJECTS_PROPERTY = "relatedSubjects";
+ private static final String TYPE_PROPERTY = "type";
+ private static final String IDENTIFIER_PROPERTY = "identifier";
+ private static final String LABEL_PROPERTY = "label";
+ private static final String STATUS_CODE = "status.code";
+
+ // Paths
+ private static final String TEST_SUBJECT_PATH = "/Subjects/r1";
+ private static final String ROOT_SUBJECT_TYPE = "/SubjectTypes/Root";
+ private static final String BRANCH_SUBJECT_TYPE = "/SubjectTypes/Root/Branch";
+ private static final String LEAF_SUBJECT_TYPE = "/SubjectTypes/Root/Branch/Leaf";
+ private static final String PATIENT_SUBJECT_TYPE = "/SubjectTypes/Patient";
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+ private static final String TEST_TEXT_QUESTIONNAIRE_PATH = "/Questionnaires/TestTextQuestionnaire";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private DeleteServlet deleteServlet;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void doDeleteNotRecursiveForFormResourceWithItsSectionAndAnswerChildren() throws ServletException,
+ IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Forms/f1", false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertNull(resolver.getResource("/Forms/f1"));
+ assertNull(resolver.getResource("/Forms/f1/s1"));
+ assertNull(resolver.getResource("/Forms/f1/s1/a6"));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSubjectResourceWithReferencesSendsJsonError() throws ServletException,
+ IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_SUBJECT_PATH, false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response, "This item is referenced in 2 forms.");
+ assertNotNull(resolver.getResource(TEST_SUBJECT_PATH));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSubjectTypeResourceWithMultipleTypesReferencesSendsJsonError()
+ throws ServletException, IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ Node questionnaire = session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH);
+ Node rootSubjectType = session.getNode(ROOT_SUBJECT_TYPE);
+ questionnaire.setProperty("requiredSubjectTypes", rootSubjectType.getIdentifier(), PropertyType.REFERENCE);
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_TYPE, false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response,
+ "This item is referenced in 2 forms and 2 subjects (Root Subject and Branch Subject).");
+ assertNotNull(resolver.getResource(ROOT_SUBJECT_TYPE));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSectionResourceWithReferencesSendsJsonError() throws ServletException,
+ IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_QUESTIONNAIRE_PATH + "/section_1", false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response, "This item is referenced in 1 answer section.");
+ assertNotNull(resolver.getResource(TEST_QUESTIONNAIRE_PATH + "/section_1"));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForQuestionResourceWithReferencesSendsJsonError() throws ServletException,
+ IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_QUESTIONNAIRE_PATH + "/section_1/question_6",
+ false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response, "This item is referenced in 1 answer.");
+ assertNotNull(resolver.getResource(TEST_QUESTIONNAIRE_PATH + "/section_1/question_6"));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSubjectTypeResourceWithReferencesInSubjectSendsJsonError()
+ throws ServletException, IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(BRANCH_SUBJECT_TYPE, false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response, "This item is referenced in 1 subject (Branch Subject).");
+ assertNotNull(resolver.getResource(BRANCH_SUBJECT_TYPE));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSubjectTypeResourceWithReferencesInSubjectTypeSendsJsonError()
+ throws ServletException, IOException
+ {
+ this.context.build()
+ .resource(PATIENT_SUBJECT_TYPE + "/Visit",
+ NODE_TYPE, SUBJECT_TYPE_TYPE,
+ LABEL_PROPERTY, "Visit",
+ "subjectListLabel", "Visits",
+ "cards:defaultOrder", 1,
+ "reference",
+ this.context.resourceResolver().getResource(LEAF_SUBJECT_TYPE).adaptTo(Node.class))
+ .commit();
+ MockSlingHttpServletRequest request = mockServletRequest(LEAF_SUBJECT_TYPE, false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response, "This item is referenced in 1 subject type (Visit).");
+ assertNotNull(resolver.getResource(LEAF_SUBJECT_TYPE));
+ }
+
+ @Test
+ public void doDeleteNotRecursiveForSubjectTypeResourceWithReferencesInQuestionnaireSendsJsonError()
+ throws ServletException, IOException
+ {
+ this.context.build()
+ .resource("/Questionnaires/TestReferenceQuestionnaire",
+ NODE_TYPE, QUESTIONNAIRE_TYPE,
+ "title", "Test Reference Questionnaire",
+ "description", "A test reference questionnaire",
+ "requiredSubjectTypes",
+ this.context.resourceResolver().getResource(LEAF_SUBJECT_TYPE).adaptTo(Node.class))
+ .commit();
+ MockSlingHttpServletRequest request = mockServletRequest(LEAF_SUBJECT_TYPE, false);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+ assertConflictResponseError(response,
+ "This item is referenced in 1 questionnaire (Test Reference Questionnaire).");
+ assertNotNull(resolver.getResource(LEAF_SUBJECT_TYPE));
+ }
+
+ @Test
+ public void doDeleteRecursiveForQuestionnaireResourceWithReferences() throws ServletException, IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_TEXT_QUESTIONNAIRE_PATH, true);
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ ResourceResolver resolver = this.context.resourceResolver();
+ this.deleteServlet.doDelete(request, response);
+ assertNull(resolver.getResource(TEST_TEXT_QUESTIONNAIRE_PATH));
+ assertNull(resolver.getResource("/Forms/f2"));
+ }
+
+ @Test
+ public void doDeleteCatchesRepositoryExceptionAndSendsJsonError() throws RepositoryException, ServletException,
+ IOException
+ {
+ ResourceResolver resolver = mock(ResourceResolver.class);
+ Session mockedSession = mock(Session.class);
+ Workspace mockedWorkspace = mock(Workspace.class);
+ when(resolver.adaptTo(Session.class)).thenReturn(mockedSession);
+ when(mockedSession.getWorkspace()).thenReturn(mockedWorkspace);
+ when(mockedWorkspace.getVersionManager()).thenThrow(new RepositoryException());
+
+ MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(resolver, this.slingBundleContext);
+ request.setResource(this.resourceResolver.getResource("/Forms/f1"));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus());
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(STATUS_CODE));
+ assertEquals(500, responseJson.getInt(STATUS_CODE));
+
+ assertTrue(responseJson.containsKey("error"));
+ JsonObject error = responseJson.get("error").asJsonObject();
+ assertEquals(2, error.size());
+ assertTrue(error.containsKey("message"));
+ assertTrue(error.containsKey("class"));
+ assertEquals("javax.jcr.RepositoryException", error.getString("class"));
+ }
+
+ @Test
+ public void doDeleteForEmptyRemoteUserCatchesAccessDeniedExceptionAndSendsJsonError() throws RepositoryException,
+ ServletException, IOException
+ {
+ ResourceResolver resolver = mock(ResourceResolver.class);
+ Session mockedSession = mock(Session.class);
+ Workspace mockedWorkspace = mock(Workspace.class);
+ VersionManager mockedVersionManager = mock(VersionManager.class);
+ when(resolver.adaptTo(Session.class)).thenReturn(mockedSession);
+ when(mockedSession.getWorkspace()).thenReturn(mockedWorkspace);
+ when(mockedWorkspace.getVersionManager()).thenReturn(mockedVersionManager);
+ doThrow(new AccessDeniedException()).when(mockedSession).save();
+
+ MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(resolver, this.slingBundleContext);
+ request.setParameterMap(Map.of("recursive", false));
+ request.setResource(this.resourceResolver.getResource("/Forms/f1"));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(STATUS_CODE));
+ assertEquals(401, responseJson.getInt(STATUS_CODE));
+ }
+
+ @Test
+ public void doDeleteForNotEmptyRemoteUserCatchesAccessDeniedExceptionAndSendsJsonError() throws RepositoryException,
+ ServletException, IOException
+ {
+ ResourceResolver resolver = mock(ResourceResolver.class);
+ Session mockedSession = mock(Session.class);
+ Workspace mockedWorkspace = mock(Workspace.class);
+ VersionManager mockedVersionManager = mock(VersionManager.class);
+ when(resolver.adaptTo(Session.class)).thenReturn(mockedSession);
+ when(mockedSession.getWorkspace()).thenReturn(mockedWorkspace);
+ when(mockedWorkspace.getVersionManager()).thenReturn(mockedVersionManager);
+ doThrow(new AccessDeniedException()).when(mockedSession).save();
+
+ MockSlingHttpServletRequest request = new MockSlingHttpServletRequest(resolver, this.slingBundleContext);
+ request.setParameterMap(Map.of("recursive", false));
+ request.setResource(this.resourceResolver.getResource("/Forms/f1"));
+ request.setRemoteUser("notAdmin");
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.deleteServlet.doDelete(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(STATUS_CODE));
+ assertEquals(403, responseJson.getInt(STATUS_CODE));
+ }
+
+ @Before
+ public void setUp() throws RepositoryException, LoginException
+ {
+ final Session session = this.context.resourceResolver().adaptTo(Session.class);
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage")
+ .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage")
+ .resource("/Forms", NODE_TYPE, "cards:FormsHomepage")
+ .commit();
+
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/TextQuestionnaires.json", TEST_TEXT_QUESTIONNAIRE_PATH);
+ this.context.load().json("/SubjectTypes.json", ROOT_SUBJECT_TYPE);
+ this.context.load().json("/SubjectTypesPatient.json", PATIENT_SUBJECT_TYPE);
+
+ this.context.build()
+ .resource(TEST_SUBJECT_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(ROOT_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Root Subject")
+ .resource(TEST_SUBJECT_PATH + "/b1",
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(BRANCH_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Branch Subject")
+ .commit();
+
+ Node rootSubject = session.getNode(TEST_SUBJECT_PATH);
+
+ this.context.build()
+ .resource("/Forms/f1",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .resource("/Forms/f1/s1",
+ NODE_TYPE, "cards:AnswerSection",
+ SECTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1"))
+ .resource("/Forms/f1/s1/a6",
+ NODE_TYPE, "cards:TextAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1/question_6"))
+ .resource("/Forms/f2",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .commit();
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+ }
+
+
+ private MockSlingHttpServletRequest mockServletRequest(String resourcePath, boolean isRecursive)
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource(resourcePath));
+ request.setParameterMap(Map.of(
+ "recursive", isRecursive
+ ));
+ return request;
+ }
+
+ private JsonObject getResponseJsonReader(MockSlingHttpServletResponse response)
+ {
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ return reader.readObject();
+ }
+
+ private void assertConflictResponseError(MockSlingHttpServletResponse response, String expectedMessage)
+ {
+ assertEquals(HttpServletResponse.SC_CONFLICT, response.getStatus());
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(STATUS_CODE));
+ assertEquals(409, responseJson.getInt(STATUS_CODE));
+ assertTrue(responseJson.containsKey("status.message"));
+ assertEquals(expectedMessage, responseJson.getString("status.message"));
+ }
+
+
+ private void assertCharacterEncodingAndContentType(MockSlingHttpServletResponse response)
+ {
+ assertEquals("UTF-8", response.getCharacterEncoding());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/FilterServletTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/FilterServletTest.java
new file mode 100644
index 0000000000..5074bb9509
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/FilterServletTest.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.text.SimpleDateFormat;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Unit tests for {@link FilterServlet}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class FilterServletTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String SECTION_PROPERTY = "section";
+ private static final String SUBJECT_PROPERTY = "subject";
+
+ // Paths
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+ private static final String TEST_TEXT_QUESTIONNAIRE_PATH = "/Questionnaires/TestTextQuestionnaire";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private FilterServlet filterServlet;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void doGetForRequestWithQuestionnaireParameterWithoutDeepJsonSuffix() throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Questionnaires");
+ request.setParameterMap(Map.of(
+ QUESTIONNAIRE_PROPERTY, TEST_TEXT_QUESTIONNAIRE_PATH
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.filterServlet.doGet(request, response);
+ JsonObject jsonObject = getResponseJsonReader(response);
+ assertNotNull(jsonObject);
+ assertEquals(3, jsonObject.keySet().size());
+ }
+
+ @Test
+ public void doGetForRequestWithQuestionnaireParameterWithDeepJsonSuffix() throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Questionnaires");
+ request.setParameterMap(Map.of(
+ QUESTIONNAIRE_PROPERTY, TEST_TEXT_QUESTIONNAIRE_PATH + ".deep.json"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.filterServlet.doGet(request, response);
+ JsonObject jsonObject = getResponseJsonReader(response);
+ assertNotNull(jsonObject);
+ assertEquals(3, jsonObject.keySet().size());
+ }
+
+ @Test
+ public void doGetForRequestWithoutQuestionnaireParameter() throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Questionnaires");
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.filterServlet.doGet(request, response);
+ JsonObject jsonObject = getResponseJsonReader(response);
+ assertNotNull(jsonObject);
+ assertEquals(11, jsonObject.keySet().size());
+ }
+
+ @Before
+ public void setUp() throws LoginException
+ {
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .commit();
+
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/TextQuestionnaires.json", TEST_TEXT_QUESTIONNAIRE_PATH);
+
+ this.context.registerAdapter(Resource.class, JsonObject.class, (Function) resource -> {
+ JsonObjectBuilder jsonObject = null;
+ try {
+ jsonObject = Json.createObjectBuilder(createPropertiesAndChildrenMap(resource));
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ return jsonObject.build();
+ });
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+ }
+
+ private Map createPropertiesAndChildrenMap(Resource originalResource) throws RepositoryException
+ {
+ Map propertiesAndChildrenMap = new HashMap<>();
+
+ // process properties of resource
+ ValueMap valueMap = originalResource.getValueMap();
+ List objectTypeProperties = List.of(QUESTIONNAIRE_PROPERTY, SUBJECT_PROPERTY, SECTION_PROPERTY,
+ QUESTION_PROPERTY);
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+ for (Map.Entry property : valueMap.entrySet()) {
+ String key = property.getKey();
+ Object value = property.getValue();
+ if (objectTypeProperties.contains(key)) {
+ Resource reference =
+ this.context.resourceResolver().getResource(getResourcePathByItsIdentifier((String) value));
+ JsonObjectBuilder referenceJson = Json.createObjectBuilder(createPropertiesAndChildrenMap(reference));
+ propertiesAndChildrenMap.put(key, referenceJson.build());
+ } else {
+ if (value.getClass().isArray()) {
+ JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for (Object valueUnit : (Object[]) value) {
+ if (valueUnit instanceof Resource) {
+ arrayBuilder.add(Json.createObjectBuilder(
+ createPropertiesAndChildrenMap((Resource) valueUnit)).build());
+ } else {
+ arrayBuilder.add((String) valueUnit);
+ }
+ }
+ propertiesAndChildrenMap.put(key, arrayBuilder.build());
+ } else if (value instanceof GregorianCalendar) {
+ sdf.setTimeZone(((GregorianCalendar) value).getTimeZone());
+ value = ((GregorianCalendar) value).getTime();
+ propertiesAndChildrenMap.put(key, sdf.format(value));
+ } else {
+ propertiesAndChildrenMap.put(key, value);
+ }
+ }
+ }
+
+ // process children of resource
+ for (Resource child : originalResource.getChildren()) {
+ JsonObject jsonObject = Json.createObjectBuilder(createPropertiesAndChildrenMap(child)).build();
+ propertiesAndChildrenMap.put(child.getName(), jsonObject);
+ }
+ return propertiesAndChildrenMap;
+ }
+
+ private String getResourcePathByItsIdentifier(String identifier) throws RepositoryException
+ {
+ return this.context.resourceResolver().adaptTo(Session.class).getNodeByIdentifier(identifier).getPath();
+ }
+
+ private MockSlingHttpServletRequest mockServletRequest(String resourcePath)
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource(resourcePath));
+ return request;
+ }
+
+ private JsonObject getResponseJsonReader(MockSlingHttpServletResponse response)
+ {
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ return reader.readObject();
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/PaginationServletTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/PaginationServletTest.java
new file mode 100644
index 0000000000..8f3bedc09e
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/PaginationServletTest.java
@@ -0,0 +1,739 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Unit tests for {@link PaginationServlet}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class PaginationServletTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String SUBJECT_TYPE = "cards:Subject";
+ private static final String CREATED_DATE_TYPE = "cards:CreatedDate";
+ private static final String QUESTIONNAIRE_TYPE = "cards:Questionnaire";
+ private static final String FORM_TYPE = "cards:Form";
+ private static final String LONG_ANSWER_TYPE = "cards:LongAnswer";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String FORM_PROPERTY = "form";
+ private static final String SUBJECT_PROPERTY = "subject";
+ private static final String RELATED_SUBJECTS_PROPERTY = "relatedSubjects";
+ private static final String VALUE_PROPERTY = "value";
+ private static final String ADMIN_USERNAME = "admin";
+
+ // Keys in Response Json
+ private static final String ROWS_PROPERTY = "rows";
+ private static final String OFFSET_PROPERTY = "offset";
+ private static final String LIMIT_PROPERTY = "limit";
+ private static final String RETURNED_ROWS_PROPERTY = "returnedrows";
+ private static final String TOTAL_ROWS_PROPERTY = "totalrows";
+ private static final String TOTAL_IS_APPROXIMATE_PROPERTY = "totalIsApproximate";
+
+ // Search parameters
+ private static final String INCLUDE_ALL_STATUS_PARAMETER = "includeallstatus";
+ private static final String FILTER_NAMES_PARAMETER = "filternames";
+ private static final String FILTER_VALUES_PARAMETER = "filtervalues";
+ private static final String FILTER_COMPARATORS_PARAMETER = "filtercomparators";
+ private static final String FILTER_TYPES_PARAMETER = "filtertypes";
+ private static final String FILTER_NODE_TYPES_PARAMETER = "filternodetypes";
+ private static final String REQUIRED_PARAMETER = "req";
+
+ // Paths
+ private static final String ROOT_FORM_PATH = "/Forms";
+ private static final String ROOT_SUBJECT_PATH = "/Subjects";
+ private static final String TEST_SUBJECT_PATH = "/Subjects/r1";
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+ private static final String TEST_TEXT_QUESTIONNAIRE_PATH = "/Questionnaires/TestTextQuestionnaire";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private PaginationServlet paginationServlet;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void doGetForFormsResourceAndQuestionnaireNotEmptyParameterAndEqualsComparatorWrites2Matches()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionnaireUuid = session.getNode(TEST_QUESTIONNAIRE_PATH).getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, QUESTIONNAIRE_TYPE,
+ FILTER_VALUES_PARAMETER, questionnaireUuid,
+ FILTER_COMPARATORS_PARAMETER, "=",
+ FILTER_TYPES_PARAMETER, QUESTIONNAIRE_PROPERTY,
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(2, rows.size());
+
+ assertTrue(responseJson.containsKey(REQUIRED_PARAMETER));
+ assertEquals("2", responseJson.getString(REQUIRED_PARAMETER));
+
+ assertTrue(responseJson.containsKey(OFFSET_PROPERTY));
+ assertEquals(0, responseJson.getInt(OFFSET_PROPERTY));
+
+ assertTrue(responseJson.containsKey(LIMIT_PROPERTY));
+ assertEquals(10, responseJson.getInt(LIMIT_PROPERTY));
+
+ assertTrue(responseJson.containsKey(RETURNED_ROWS_PROPERTY));
+ assertEquals(2, responseJson.getInt(RETURNED_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_ROWS_PROPERTY));
+ assertEquals(2, responseJson.getInt(TOTAL_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_IS_APPROXIMATE_PROPERTY));
+ assertEquals(Boolean.FALSE, responseJson.getBoolean(TOTAL_IS_APPROXIMATE_PROPERTY));
+ }
+
+ @Test
+ public void doGetForFormsResourceAndQuestionNotEmptyParametersAndBlankFilterTypeAndEqualsComparatorsWrites1Match()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionUuid = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, questionUuid,
+ FILTER_VALUES_PARAMETER, "100",
+ FILTER_COMPARATORS_PARAMETER, "=",
+ FILTER_TYPES_PARAMETER, "",
+ FILTER_NODE_TYPES_PARAMETER, "cards:Answer",
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(1, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceAndDifferentNumberOfNamesAndValueParametersCatchesIllegalArgumentException()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionUuid = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, new String[]{questionUuid, QUESTIONNAIRE_TYPE},
+ FILTER_VALUES_PARAMETER, "100",
+ FILTER_COMPARATORS_PARAMETER, "=",
+ FILTER_TYPES_PARAMETER, "",
+ FILTER_NODE_TYPES_PARAMETER, "cards:Answer",
+ REQUIRED_PARAMETER, "2"
+ ));
+
+ MockSlingHttpServletResponse primaryResponse = new MockSlingHttpServletResponse();
+ MockSlingHttpServletResponse changeableResponse = primaryResponse;
+
+ this.paginationServlet.doGet(request, changeableResponse);
+ assertEquals(primaryResponse, changeableResponse);
+ }
+
+ @Test
+ public void doGetForFormsResourceAndQuestionNotEmptyParametersAndContainsComparatorsWrites1Match()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionUuid = session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH + "/question_1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ "descending", "true",
+ FILTER_NAMES_PARAMETER, questionUuid,
+ FILTER_VALUES_PARAMETER, "12345",
+ FILTER_COMPARATORS_PARAMETER, "contains",
+ FILTER_TYPES_PARAMETER, "text",
+ FILTER_NODE_TYPES_PARAMETER, "cards:Answer",
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ // assertEquals(1, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceAndQuestionNotEmptyParametersAndNotesContainsComparatorsWrites1Match()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionUuid = session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH + "/question_1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, questionUuid,
+ FILTER_VALUES_PARAMETER, "text",
+ FILTER_COMPARATORS_PARAMETER, "notes contain",
+ FILTER_TYPES_PARAMETER, "text",
+ FILTER_NODE_TYPES_PARAMETER, "cards:Answer",
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ // assertEquals(1, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndQuestionNotEmptyParametersAndEqualsComparatorsWrites1Match()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String questionUuid = session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH + "/question_1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, questionUuid,
+ FILTER_VALUES_PARAMETER, "12345",
+ FILTER_COMPARATORS_PARAMETER, "=",
+ FILTER_TYPES_PARAMETER, "text",
+ FILTER_NODE_TYPES_PARAMETER, "cards:Answer",
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ // assertEquals(1, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceAndSubjectNotEmptyParameterAndEqualsComparatorWrites1Match()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String subjectUuid = session.getNode(TEST_SUBJECT_PATH + "/b1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ INCLUDE_ALL_STATUS_PARAMETER, "true",
+ FILTER_NAMES_PARAMETER, SUBJECT_TYPE,
+ FILTER_VALUES_PARAMETER, subjectUuid,
+ FILTER_COMPARATORS_PARAMETER, "=",
+ FILTER_TYPES_PARAMETER, SUBJECT_PROPERTY,
+ REQUIRED_PARAMETER, "2"
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(1, rows.size());
+
+ assertTrue(responseJson.containsKey(REQUIRED_PARAMETER));
+ assertEquals("2", responseJson.getString(REQUIRED_PARAMETER));
+
+ assertTrue(responseJson.containsKey(OFFSET_PROPERTY));
+ assertEquals(0, responseJson.getInt(OFFSET_PROPERTY));
+
+ assertTrue(responseJson.containsKey(LIMIT_PROPERTY));
+ assertEquals(10, responseJson.getInt(LIMIT_PROPERTY));
+
+ assertTrue(responseJson.containsKey(RETURNED_ROWS_PROPERTY));
+ assertEquals(1, responseJson.getInt(RETURNED_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_ROWS_PROPERTY));
+ assertEquals(1, responseJson.getInt(TOTAL_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_IS_APPROXIMATE_PROPERTY));
+ assertEquals(Boolean.FALSE, responseJson.getBoolean(TOTAL_IS_APPROXIMATE_PROPERTY));
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndLessOrEqualsComparatorWrites2Matches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter("<="));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(2, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndMoreOrEqualsComparatorWrites2Matches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter(">="));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(2, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndEqualsComparatorWrites2Matches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter("="));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(2, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndNotEqualsComparatorWritesNoMatches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter("<>"));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndLessComparatorWritesNoMatches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter("<"));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void doGetForSubjectsResourceAndCreatedDateParameterAndMoreComparatorWritesNoMatches()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_SUBJECT_PATH);
+ request.setParameterMap(generateParameterMapWithCreatedDateFilter(">"));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ JsonObject responseJson = getResponseJsonReader(response);
+
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceAndFieldParametersAndWithoutIncludeAllParameterWritesNoMatches()
+ throws IOException, RepositoryException
+ {
+ Session session = this.context.resourceResolver().adaptTo(Session.class);
+ String formUuid = session.getNode("/Forms/f1").getIdentifier();
+
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of(
+ "fieldname", "jcr:uuid",
+ "fieldcomparator", "=",
+ "fieldvalue", formUuid
+ ));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+
+ assertTrue(responseJson.containsKey(REQUIRED_PARAMETER));
+ assertTrue(responseJson.getString(REQUIRED_PARAMETER).isBlank());
+
+ assertTrue(responseJson.containsKey(OFFSET_PROPERTY));
+ assertEquals(0, responseJson.getInt(OFFSET_PROPERTY));
+
+ assertTrue(responseJson.containsKey(LIMIT_PROPERTY));
+ assertEquals(10, responseJson.getInt(LIMIT_PROPERTY));
+
+ assertTrue(responseJson.containsKey(RETURNED_ROWS_PROPERTY));
+ assertEquals(0, responseJson.getInt(RETURNED_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_ROWS_PROPERTY));
+ assertEquals(0, responseJson.getInt(TOTAL_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_IS_APPROXIMATE_PROPERTY));
+ assertEquals(Boolean.FALSE, responseJson.getBoolean(TOTAL_IS_APPROXIMATE_PROPERTY));
+ }
+
+ @Test
+ public void doGetForFormsResourceAndQuestionnaireEmptyParameterWritesEmptyResponse()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of("filterempty", QUESTIONNAIRE_TYPE));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+
+ assertTrue(responseJson.containsKey(REQUIRED_PARAMETER));
+ assertTrue(responseJson.getString(REQUIRED_PARAMETER).isBlank());
+
+ assertTrue(responseJson.containsKey(OFFSET_PROPERTY));
+ assertEquals(0, responseJson.getInt(OFFSET_PROPERTY));
+
+ assertTrue(responseJson.containsKey(LIMIT_PROPERTY));
+ assertEquals(10, responseJson.getInt(LIMIT_PROPERTY));
+
+ assertTrue(responseJson.containsKey(RETURNED_ROWS_PROPERTY));
+ assertEquals(0, responseJson.getInt(RETURNED_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_ROWS_PROPERTY));
+ assertEquals(0, responseJson.getInt(TOTAL_ROWS_PROPERTY));
+
+ assertTrue(responseJson.containsKey(TOTAL_IS_APPROXIMATE_PROPERTY));
+ assertEquals(Boolean.FALSE, responseJson.getBoolean(TOTAL_IS_APPROXIMATE_PROPERTY));
+ }
+
+ @Test
+ public void doGetForFormsResourceAndSubjectEmptyParameterWritesEmptyResponse()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of("filterempty", SUBJECT_TYPE));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceAndCreatedDateEmptyParameterWritesEmptyResponse()
+ throws IOException
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(ROOT_FORM_PATH);
+ request.setParameterMap(Map.of("filterempty", CREATED_DATE_TYPE));
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+
+ this.paginationServlet.doGet(request, response);
+ assertCharacterEncodingAndContentType(response);
+
+ JsonObject responseJson = getResponseJsonReader(response);
+ assertTrue(responseJson.containsKey(ROWS_PROPERTY));
+ assertTrue(responseJson.get(ROWS_PROPERTY) instanceof JsonArray);
+ JsonArray rows = responseJson.getJsonArray(ROWS_PROPERTY);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void doGetForFormsResourceCatchesNullPointerException() throws IOException
+ {
+ ResourceResolver resolver = mock(ResourceResolver.class);
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(resolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource("/Forms"));
+ request.setRemoteUser(ADMIN_USERNAME);
+ request.setParameterMap(Map.of("filternotempty", CREATED_DATE_TYPE));
+
+ MockSlingHttpServletResponse primaryResponse = new MockSlingHttpServletResponse();
+ MockSlingHttpServletResponse changeableResponse = primaryResponse;
+
+ this.paginationServlet.doGet(request, changeableResponse);
+ assertEquals(primaryResponse, changeableResponse);
+ }
+
+ @Before
+ public void setUp() throws RepositoryException, LoginException
+ {
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage")
+ .resource(ROOT_SUBJECT_PATH, NODE_TYPE, "cards:SubjectsHomepage")
+ .resource(ROOT_FORM_PATH, NODE_TYPE, "cards:FormsHomepage")
+ .commit();
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/TextQuestionnaires.json", TEST_TEXT_QUESTIONNAIRE_PATH);
+ this.context.load().json("/SubjectTypes.json", "/SubjectTypes/Root");
+ this.context.build()
+ .resource(TEST_SUBJECT_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ "type", this.context.resourceResolver().getResource("/SubjectTypes/Root").adaptTo(Node.class))
+ .resource(TEST_SUBJECT_PATH + "/b1",
+ NODE_TYPE, SUBJECT_TYPE,
+ "type",
+ this.context.resourceResolver().getResource("/SubjectTypes/Root/Branch").adaptTo(Node.class))
+ .commit();
+ final Session session = this.context.resourceResolver().adaptTo(Session.class);
+ Node subject = session.getNode(TEST_SUBJECT_PATH);
+ Node branchSubject = session.getNode(TEST_SUBJECT_PATH + "/b1");
+ Node questionnaire = session.getNode(TEST_QUESTIONNAIRE_PATH);
+ Node question = session.getNode(TEST_QUESTIONNAIRE_PATH + "/question_1");
+
+ this.context.build()
+ .resource("/Forms/f1",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, subject,
+ QUESTIONNAIRE_PROPERTY, questionnaire,
+ RELATED_SUBJECTS_PROPERTY, List.of(subject).toArray())
+
+ .resource("/Forms/f2",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, branchSubject,
+ QUESTIONNAIRE_PROPERTY, questionnaire,
+ RELATED_SUBJECTS_PROPERTY, List.of(subject, branchSubject).toArray())
+
+ .resource("/Forms/f3",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, subject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(subject).toArray())
+ .commit();
+
+ // create Answers for each Form with "form" property
+ this.context.build()
+ .resource("/Forms/f1/a1",
+ NODE_TYPE, LONG_ANSWER_TYPE,
+ QUESTION_PROPERTY, question,
+ VALUE_PROPERTY, 100,
+ FORM_PROPERTY, session.getNode("/Forms/f1").getIdentifier())
+ .resource("/Forms/f2/a1",
+ NODE_TYPE, LONG_ANSWER_TYPE,
+ QUESTION_PROPERTY, question,
+ VALUE_PROPERTY, 200,
+ FORM_PROPERTY, session.getNode("/Forms/f2").getIdentifier())
+ .resource("/Forms/f3/a1",
+ NODE_TYPE, "cards:TextAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH + "/question_1"),
+ VALUE_PROPERTY, "12345",
+ "note", "some notes",
+ FORM_PROPERTY, session.getNode("/Forms/f3").getIdentifier())
+ .commit();
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+
+ this.context.registerAdapter(Resource.class, JsonObject.class, (Function) resource -> {
+ JsonObjectBuilder jsonObject = null;
+ try {
+ jsonObject = Json.createObjectBuilder(createPropertiesAndChildrenMap(resource));
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ return jsonObject.build();
+ });
+ }
+
+ private Map createPropertiesAndChildrenMap(Resource originalResource) throws RepositoryException
+ {
+ Map propertiesAndChildrenMap = new HashMap<>();
+
+ // process properties of resource
+ ValueMap valueMap = originalResource.getValueMap();
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+ for (Map.Entry property : valueMap.entrySet()) {
+ String key = property.getKey();
+ Object value = property.getValue();
+ if (value.getClass().isArray()) {
+ JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for (Object valueUnit : (Object[]) value) {
+ if (valueUnit instanceof Resource) {
+ arrayBuilder.add(Json.createObjectBuilder(
+ createPropertiesAndChildrenMap((Resource) valueUnit)).build());
+ } else {
+ arrayBuilder.add((String) valueUnit);
+ }
+ }
+ propertiesAndChildrenMap.put(key, arrayBuilder.build());
+ } else if (value instanceof GregorianCalendar) {
+ sdf.setTimeZone(((GregorianCalendar) value).getTimeZone());
+ value = ((GregorianCalendar) value).getTime();
+ propertiesAndChildrenMap.put(key, sdf.format(value));
+ } else {
+ propertiesAndChildrenMap.put(key, value);
+ }
+
+ }
+ return propertiesAndChildrenMap;
+ }
+
+ private Map generateParameterMapWithCreatedDateFilter(String comparator)
+ {
+ return Map.of(
+ FILTER_NAMES_PARAMETER, CREATED_DATE_TYPE,
+ FILTER_VALUES_PARAMETER, getFormattedCurrentDateTime(),
+ FILTER_COMPARATORS_PARAMETER, comparator,
+ FILTER_TYPES_PARAMETER, "createddate"
+ );
+ }
+
+ private MockSlingHttpServletRequest mockServletRequest(String resourcePath)
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource(resourcePath));
+ request.setRemoteUser(ADMIN_USERNAME);
+ return request;
+ }
+
+ private String getFormattedCurrentDateTime()
+ {
+ Calendar date = Calendar.getInstance();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmXXX");
+ formatter.setTimeZone(date.getTimeZone());
+ return formatter.format(date.getTime());
+ }
+
+ private JsonObject getResponseJsonReader(MockSlingHttpServletResponse response)
+ {
+ JsonReader reader = Json.createReader(new StringReader(response.getOutputAsString()));
+ return reader.readObject();
+ }
+
+ private void assertCharacterEncodingAndContentType(MockSlingHttpServletResponse response)
+ {
+ assertEquals("UTF-8", response.getCharacterEncoding());
+ assertEquals("application/json;charset=UTF-8", response.getContentType());
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/QueryBuilderTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/QueryBuilderTest.java
new file mode 100644
index 0000000000..2b29715bac
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/QueryBuilderTest.java
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards;
+
+import java.io.StringReader;
+import java.text.SimpleDateFormat;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+import javax.script.Bindings;
+import javax.script.SimpleBindings;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.testing.mock.sling.MockSlingScriptHelper;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.osgi.framework.BundleContext;
+
+import io.uhndata.cards.spi.QuickSearchEngine;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link QueryBuilder}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class QueryBuilderTest
+{
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String SUBJECT_TYPE = "cards:Subject";
+ private static final String FORM_TYPE = "cards:Form";
+ private static final String QUESTIONNAIRE_PROPERTY = "questionnaire";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String SECTION_PROPERTY = "section";
+ private static final String SUBJECT_PROPERTY = "subject";
+ private static final String RELATED_SUBJECTS_PROPERTY = "relatedSubjects";
+ private static final String TYPE_PROPERTY = "type";
+ private static final String IDENTIFIER_PROPERTY = "identifier";
+
+ // Paths
+ private static final String TEST_SUBJECT_PATH = "/Subjects/r1";
+ private static final String ROOT_SUBJECT_TYPE = "/SubjectTypes/Root";
+ private static final String BRANCH_SUBJECT_TYPE = "/SubjectTypes/Root/Branch";
+ private static final String PATIENT_SUBJECT_TYPE = "/SubjectTypes/Patient";
+ private static final String TEST_QUESTIONNAIRE_PATH = "/Questionnaires/TestSerializableQuestionnaire";
+ private static final String TEST_TEXT_QUESTIONNAIRE_PATH = "/Questionnaires/TestTextQuestionnaire";
+ private static final String ALLOWED_RESOURCE_TYPES = "allowedResourceTypes";
+ private static final String QUERY = "query";
+ private static final String LUCENE = "lucene";
+ private static final String FULL_TEXT = "fulltext";
+ private static final String QUICK = "quick";
+ private static final String OFFSET = "offset";
+ private static final String LIMIT = "limit";
+ private static final String SERIALIZE_CHILDREN = "serializeChildren";
+ private static final String REQ = "req";
+ private static final String DO_NOT_ESCAPE_QUERY = "doNotEscapeQuery";
+ private static final String SHOW_TOTAL_ROWS = "showTotalRows";
+ private static final String ROWS = "rows";
+ private static final String RETURNED_ROWS = "returnedrows";
+ private static final String TOTAL_ROWS = "totalrows";
+ private static final String REQUEST = "request";
+ private static final String RESOLVER = "resolver";
+ private static final String SLING = "sling";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @InjectMocks
+ private QueryBuilder queryBuilder;
+
+ @Mock
+ private QuickSearchEngine quickSearchEngine;
+
+ private BundleContext slingBundleContext;
+
+ private ResourceResolver resourceResolver;
+
+ @Test
+ public void initForJcrQuerySelectsQuestionnairesAndSerializesChildren()
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_TEXT_QUESTIONNAIRE_PATH, true);
+ request.setParameterMap(Map.of(
+ ALLOWED_RESOURCE_TYPES, new String[]{},
+ QUERY, "SELECT q.* FROM [cards:Questionnaire] as q",
+ OFFSET, 0,
+ LIMIT, 10,
+ SERIALIZE_CHILDREN, 1,
+ REQ, "",
+ DO_NOT_ESCAPE_QUERY, "true",
+ SHOW_TOTAL_ROWS, "true"
+ ));
+
+ Bindings bindings = createBindings(request);
+
+ this.queryBuilder.init(bindings);
+ JsonObject jsonObject = getJsonObjectFromQueryBuilderContext();
+
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.containsKey(REQ));
+ assertTrue(StringUtils.isBlank(jsonObject.getString(REQ)));
+ assertTrue(jsonObject.containsKey(OFFSET));
+ assertEquals(0, jsonObject.getInt(OFFSET));
+ assertTrue(jsonObject.containsKey(LIMIT));
+ assertEquals(10, jsonObject.getInt(LIMIT));
+ assertTrue(jsonObject.containsKey(RETURNED_ROWS));
+ assertEquals(2, jsonObject.getInt(RETURNED_ROWS));
+ assertTrue(jsonObject.containsKey(TOTAL_ROWS));
+ assertEquals(2, jsonObject.getInt(TOTAL_ROWS));
+
+ assertTrue(jsonObject.containsKey(ROWS));
+ JsonArray rows = jsonObject.getJsonArray(ROWS);
+ assertEquals(2, rows.size());
+ JsonObject questionnaire1 = rows.get(0).asJsonObject();
+ JsonObject questionnaire2 = rows.get(1).asJsonObject();
+ assertEquals("Test Serializable Questionnaire", questionnaire1.getString("title"));
+ assertEquals("Test Text Questionnaire", questionnaire2.getString("title"));
+
+ // assert if children of found questionnaires are serialized as well
+ assertTrue(questionnaire1.get("question_1") instanceof JsonObject);
+ assertTrue(questionnaire2.get("question_1") instanceof JsonObject);
+ }
+
+ @Test
+ public void initForJcrQueryWithNotParsableLimitValueAndDoesNotSerializeChildren()
+ {
+ MockSlingHttpServletRequest request = mockServletRequest(TEST_TEXT_QUESTIONNAIRE_PATH, true);
+ request.setParameterMap(Map.of(
+ ALLOWED_RESOURCE_TYPES, new String[]{},
+ QUERY, "SELECT q.* FROM [cards:Questionnaire] as q",
+ OFFSET, 0,
+ LIMIT, "notParsable",
+ SERIALIZE_CHILDREN, 0,
+ REQ, "",
+ DO_NOT_ESCAPE_QUERY, "true",
+ SHOW_TOTAL_ROWS, "true"
+ ));
+
+ Bindings bindings = createBindings(request);
+
+ this.queryBuilder.init(bindings);
+ JsonObject jsonObject = getJsonObjectFromQueryBuilderContext();
+
+ assertNotNull(jsonObject);
+
+ assertEquals(10, jsonObject.getInt(LIMIT));
+
+ assertTrue(jsonObject.containsKey(ROWS));
+ JsonArray rows = jsonObject.getJsonArray(ROWS);
+ assertEquals(2, rows.size());
+ JsonObject questionnaire1 = rows.get(0).asJsonObject();
+ JsonObject questionnaire2 = rows.get(1).asJsonObject();
+ assertEquals("Test Serializable Questionnaire", questionnaire1.getString("title"));
+ assertEquals("Test Text Questionnaire", questionnaire2.getString("title"));
+
+ // assert if children of found questionnaires are not serialized
+ assertFalse(questionnaire1.containsKey("question_1"));
+ assertFalse(questionnaire2.containsKey("question_1"));
+ }
+
+ @Test
+ public void initForQuickQuerySelectsForms()
+ {
+ when(this.quickSearchEngine.isTypeSupported(QUICK)).thenReturn(true);
+ MockSlingHttpServletRequest request = mockServletRequest("/Forms/f1", true);
+ request.setParameterMap(Map.of(
+ ALLOWED_RESOURCE_TYPES, new String[]{FORM_TYPE},
+ QUICK, "searchValue",
+ OFFSET, 0,
+ LIMIT, 10,
+ SERIALIZE_CHILDREN, 0,
+ REQ, "",
+ DO_NOT_ESCAPE_QUERY, "true",
+ SHOW_TOTAL_ROWS, "true"
+ ));
+
+ Bindings bindings = createBindings(request);
+
+ this.queryBuilder.init(bindings);
+ JsonObject jsonObject = getJsonObjectFromQueryBuilderContext();
+
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.containsKey(REQ));
+ assertTrue(StringUtils.isBlank(jsonObject.getString(REQ)));
+ assertTrue(jsonObject.containsKey(OFFSET));
+ assertEquals(0, jsonObject.getInt(OFFSET));
+ assertTrue(jsonObject.containsKey(LIMIT));
+ assertEquals(10, jsonObject.getInt(LIMIT));
+ assertTrue(jsonObject.containsKey(RETURNED_ROWS));
+ assertEquals(0, jsonObject.getInt(RETURNED_ROWS));
+ assertTrue(jsonObject.containsKey(TOTAL_ROWS));
+ assertEquals(0, jsonObject.getInt(TOTAL_ROWS));
+
+ assertTrue(jsonObject.containsKey(ROWS));
+ JsonArray rows = jsonObject.getJsonArray(ROWS);
+ assertEquals(0, rows.size());
+ }
+
+ @Test
+ public void initForFullTextQuery()
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Forms/f1", true);
+ request.setParameterMap(Map.of(
+ ALLOWED_RESOURCE_TYPES, new String[]{},
+ FULL_TEXT, "searchValue",
+ OFFSET, 0,
+ LIMIT, 10,
+ SERIALIZE_CHILDREN, 0,
+ REQ, "",
+ DO_NOT_ESCAPE_QUERY, "true",
+ SHOW_TOTAL_ROWS, "false"
+ ));
+
+ Bindings bindings = createBindings(request);
+
+ this.queryBuilder.init(bindings);
+ JsonObject jsonObject = getJsonObjectFromQueryBuilderContext();
+
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.containsKey(REQ));
+ assertTrue(StringUtils.isBlank(jsonObject.getString(REQ)));
+ assertTrue(jsonObject.containsKey(OFFSET));
+ assertEquals(0, jsonObject.getInt(OFFSET));
+ assertTrue(jsonObject.containsKey(LIMIT));
+ assertEquals(10, jsonObject.getInt(LIMIT));
+ assertTrue(jsonObject.containsKey(RETURNED_ROWS));
+ assertEquals(0, jsonObject.getInt(RETURNED_ROWS));
+ assertTrue(jsonObject.containsKey(TOTAL_ROWS));
+ assertEquals(-1, jsonObject.getInt(TOTAL_ROWS));
+ assertTrue(jsonObject.containsKey(ROWS));
+ }
+
+ @Test
+ public void initForLuceneQuery()
+ {
+ MockSlingHttpServletRequest request = mockServletRequest("/Forms/f1", true);
+ request.setParameterMap(Map.of(
+ ALLOWED_RESOURCE_TYPES, new String[]{},
+ LUCENE, "value: 'searchValue'",
+ OFFSET, 0,
+ LIMIT, 10,
+ SERIALIZE_CHILDREN, 0,
+ REQ, "",
+ DO_NOT_ESCAPE_QUERY, "true",
+ SHOW_TOTAL_ROWS, "false"
+ ));
+
+ Bindings bindings = createBindings(request);
+
+ this.queryBuilder.init(bindings);
+ JsonObject jsonObject = getJsonObjectFromQueryBuilderContext();
+
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.containsKey(REQ));
+ assertTrue(StringUtils.isBlank(jsonObject.getString(REQ)));
+ assertTrue(jsonObject.containsKey(OFFSET));
+ assertEquals(0, jsonObject.getInt(OFFSET));
+ assertTrue(jsonObject.containsKey(LIMIT));
+ assertEquals(10, jsonObject.getInt(LIMIT));
+ assertTrue(jsonObject.containsKey(RETURNED_ROWS));
+ assertEquals(0, jsonObject.getInt(RETURNED_ROWS));
+ assertTrue(jsonObject.containsKey(TOTAL_ROWS));
+ assertEquals(-1, jsonObject.getInt(TOTAL_ROWS));
+ assertTrue(jsonObject.containsKey(ROWS));
+ }
+
+ @Before
+ public void setUp() throws RepositoryException, LoginException
+ {
+ final Session session = this.context.resourceResolver().adaptTo(Session.class);
+ this.context.build()
+ .resource("/Questionnaires", NODE_TYPE, "cards:QuestionnairesHomepage")
+ .resource("/SubjectTypes", NODE_TYPE, "cards:SubjectTypesHomepage")
+ .resource("/Subjects", NODE_TYPE, "cards:SubjectsHomepage")
+ .resource("/Forms", NODE_TYPE, "cards:FormsHomepage")
+ .commit();
+
+ this.context.load().json("/Questionnaires.json", TEST_QUESTIONNAIRE_PATH);
+ this.context.load().json("/TextQuestionnaires.json", TEST_TEXT_QUESTIONNAIRE_PATH);
+ this.context.load().json("/SubjectTypes.json", ROOT_SUBJECT_TYPE);
+ this.context.load().json("/SubjectTypesPatient.json", PATIENT_SUBJECT_TYPE);
+
+ this.context.build()
+ .resource(TEST_SUBJECT_PATH,
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(ROOT_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Root Subject")
+ .resource(TEST_SUBJECT_PATH + "/b1",
+ NODE_TYPE, SUBJECT_TYPE,
+ TYPE_PROPERTY,
+ this.context.resourceResolver().getResource(BRANCH_SUBJECT_TYPE).adaptTo(Node.class),
+ IDENTIFIER_PROPERTY, "Branch Subject")
+ .commit();
+
+ Node rootSubject = session.getNode(TEST_SUBJECT_PATH);
+
+ this.context.build()
+ .resource("/Forms/f1",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .resource("/Forms/f1/s1",
+ NODE_TYPE, "cards:AnswerSection",
+ SECTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1"))
+ .resource("/Forms/f1/s1/a6",
+ NODE_TYPE, "cards:TextAnswer",
+ QUESTION_PROPERTY, session.getNode(TEST_QUESTIONNAIRE_PATH + "/section_1/question_6"),
+ "value", "searchValue")
+ .resource("/Forms/f2",
+ NODE_TYPE, FORM_TYPE,
+ SUBJECT_PROPERTY, rootSubject,
+ QUESTIONNAIRE_PROPERTY, session.getNode(TEST_TEXT_QUESTIONNAIRE_PATH),
+ RELATED_SUBJECTS_PROPERTY, List.of(rootSubject).toArray())
+ .commit();
+
+ this.context.registerService(QuickSearchEngine.class, this.quickSearchEngine);
+ this.context.registerAdapter(Resource.class, JsonObject.class, (Function) resource -> {
+ JsonObjectBuilder jsonObject = null;
+ try {
+ jsonObject = Json.createObjectBuilder(createPropertiesAndChildrenMap(resource));
+ } catch (RepositoryException e) {
+ throw new RuntimeException(e);
+ }
+ return jsonObject.build();
+ });
+
+ this.slingBundleContext = this.context.bundleContext();
+ this.resourceResolver = this.slingBundleContext
+ .getService(this.slingBundleContext.getServiceReference(ResourceResolverFactory.class))
+ .getServiceResourceResolver(null);
+
+ }
+
+ private Map createPropertiesAndChildrenMap(Resource originalResource) throws RepositoryException
+ {
+ Map propertiesAndChildrenMap = new HashMap<>();
+
+ // process properties of resource
+ ValueMap valueMap = originalResource.getValueMap();
+ List objectTypeProperties = List.of(QUESTIONNAIRE_PROPERTY, SUBJECT_PROPERTY, SECTION_PROPERTY,
+ QUESTION_PROPERTY);
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+ for (Map.Entry property : valueMap.entrySet()) {
+ String key = property.getKey();
+ Object value = property.getValue();
+ if (objectTypeProperties.contains(key)) {
+ Resource reference =
+ this.context.resourceResolver().getResource(getResourcePathByItsIdentifier((String) value));
+ JsonObjectBuilder referenceJson = Json.createObjectBuilder(createPropertiesAndChildrenMap(reference));
+ propertiesAndChildrenMap.put(key, referenceJson.build());
+ } else {
+ if (value.getClass().isArray()) {
+ JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+ for (Object valueUnit : (Object[]) value) {
+ if (valueUnit instanceof Resource) {
+ arrayBuilder.add(Json.createObjectBuilder(
+ createPropertiesAndChildrenMap((Resource) valueUnit)).build());
+ } else {
+ arrayBuilder.add((String) valueUnit);
+ }
+ }
+ propertiesAndChildrenMap.put(key, arrayBuilder.build());
+ } else if (value instanceof GregorianCalendar) {
+ sdf.setTimeZone(((GregorianCalendar) value).getTimeZone());
+ value = ((GregorianCalendar) value).getTime();
+ propertiesAndChildrenMap.put(key, sdf.format(value));
+ } else {
+ propertiesAndChildrenMap.put(key, value);
+ }
+ }
+ }
+
+ return propertiesAndChildrenMap;
+ }
+
+ private String getResourcePathByItsIdentifier(String identifier) throws RepositoryException
+ {
+ return this.context.resourceResolver().adaptTo(Session.class).getNodeByIdentifier(identifier).getPath();
+ }
+
+ private MockSlingHttpServletRequest mockServletRequest(String resourcePath, boolean isRecursive)
+ {
+ MockSlingHttpServletRequest request =
+ new MockSlingHttpServletRequest(this.resourceResolver, this.slingBundleContext);
+ request.setResource(this.context.resourceResolver().getResource(resourcePath));
+
+ return request;
+ }
+
+ private Bindings createBindings(SlingHttpServletRequest request)
+ {
+ MockSlingHttpServletResponse response = new MockSlingHttpServletResponse();
+ MockSlingScriptHelper sling = new MockSlingScriptHelper(request, response, this.slingBundleContext);
+
+ Bindings bindings = new SimpleBindings();
+ bindings.put(REQUEST, request);
+ bindings.put(RESOLVER, this.resourceResolver);
+ bindings.put(SLING, sling);
+ return bindings;
+ }
+
+ private JsonObject getJsonObjectFromQueryBuilderContext()
+ {
+ JsonReader reader = Json.createReader(new StringReader(this.queryBuilder.getContent()));
+ JsonObject object = reader.readObject();
+ reader.close();
+ return object;
+ }
+
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/ResourceIteratorTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/ResourceIteratorTest.java
new file mode 100644
index 0000000000..b0a769a422
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/ResourceIteratorTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link ResourceIterator}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ResourceIteratorTest
+{
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ private ResourceIterator resourceIterator;
+
+ @Test
+ public void hasNextReturnsTrueForAllNodeChildren()
+ {
+ for (int i = 0; i < 8; i++) {
+ assertTrue(this.resourceIterator.hasNext());
+ this.resourceIterator.next();
+ }
+ assertFalse(this.resourceIterator.hasNext());
+ }
+
+ @Test
+ public void nextReturnsResource()
+ {
+ Resource resource = this.resourceIterator.next();
+ assertNotNull(resource);
+ }
+
+ @Test
+ public void nextCatchesRepositoryExceptionAndReturnsNull() throws RepositoryException
+ {
+ NodeIterator nodeIterator = mock(NodeIterator.class);
+ Node node = mock(Node.class);
+ when(nodeIterator.nextNode()).thenReturn(node);
+ when(node.getPath()).thenThrow(new RepositoryException());
+ this.resourceIterator = new ResourceIterator(this.context.resourceResolver(), nodeIterator);
+ Resource resource = this.resourceIterator.next();
+ assertNull(resource);
+ }
+
+ @Test
+ public void removeThrowsUnsupportedOperationException()
+ {
+ assertThrows(UnsupportedOperationException.class, () -> this.resourceIterator.remove());
+ }
+
+ @Before
+ public void setUp() throws RepositoryException
+ {
+ this.context.build()
+ .resource("/Questionnaires", "jcr:primaryType", "cards:QuestionnairesHomepage")
+ .commit();
+
+ this.context.load().json("/Questionnaires.json", "/Questionnaires/TestSerializableQuestionnaire");
+
+ ResourceResolver rr = this.context.resourceResolver();
+ NodeIterator nodeIterator = rr.adaptTo(Session.class).getNode("/Questionnaires/TestSerializableQuestionnaire")
+ .getNodes();
+ this.resourceIterator = new ResourceIterator(rr, nodeIterator);
+ }
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchParametersFactoryTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchParametersFactoryTest.java
new file mode 100644
index 0000000000..eb37fa216d
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchParametersFactoryTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards.spi;
+
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * Unit tests for {@link SearchParametersFactory}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SearchParametersFactoryTest
+{
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ private SearchParametersFactory searchParametersFactory;
+
+ @Test
+ public void newSearchParametersReturnsNewInstance()
+ {
+ assertNotNull(this.searchParametersFactory);
+ }
+
+ @Test
+ public void buildWithoutTypeThrowsIllegalStateException()
+ {
+ assertThrows("Query type not set yet, withType(type) must be called before build()",
+ IllegalStateException.class, () -> this.searchParametersFactory.build());
+ }
+
+ @Test
+ public void buildWithoutQueryThrowsIllegalStateException()
+ {
+ String type = "quick";
+ this.searchParametersFactory.withType(type);
+ assertThrows("Query not set yet, withQuery(query) must be called before build()",
+ IllegalStateException.class, () -> this.searchParametersFactory.build());
+ }
+
+ @Test
+ public void buildCreatesNewSearchParametersInstance()
+ {
+ setSearchParameters("quick", "search", false, false);
+ this.searchParametersFactory.withMaxResults(5);
+
+ SearchParameters searchParameters = this.searchParametersFactory.build();
+ assertNotNull(searchParameters);
+ assertEquals("quick", searchParameters.getType());
+ assertEquals("search", searchParameters.getQuery());
+ assertEquals(5, searchParameters.getMaxResults());
+ assertFalse(searchParameters.isEscaped());
+ assertFalse(searchParameters.showTotalResults());
+ }
+
+ @Test
+ public void withMaxResultsForNegativeNumberThrowsIllegalArgumentException()
+ {
+ assertThrows("maxResults must be > 0", IllegalArgumentException.class,
+ () -> this.searchParametersFactory.withMaxResults(-1));
+ }
+
+
+ @Before
+ public void setUp()
+ {
+ this.searchParametersFactory = SearchParametersFactory.newSearchParameters();
+ }
+
+ private void setSearchParameters(String type, String query, boolean escaped, boolean showTotalResults)
+ {
+ this.searchParametersFactory.withType(type);
+ this.searchParametersFactory.withQuery(query);
+ this.searchParametersFactory.withEscaped(escaped);
+ this.searchParametersFactory.withShowTotalResults(showTotalResults);
+ }
+}
diff --git a/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchUtilsTest.java b/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchUtilsTest.java
new file mode 100644
index 0000000000..35e342b50c
--- /dev/null
+++ b/modules/data-entry/src/test/java/io/uhndata/cards/spi/SearchUtilsTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package io.uhndata.cards.spi;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link SearchUtils}.
+ *
+ * @version $Id$
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SearchUtilsTest
+{
+
+ private static final String NODE_TYPE = "jcr:primaryType";
+ private static final String FORM_TYPE = "cards:Form";
+
+ // Keys in Response Json
+ private static final String QUERY_MATCH_PROPERTY = "cards:queryMatch";
+ private static final String QUESTION_PROPERTY = "question";
+ private static final String IN_NOTES_PROPERTY = "inNotes";
+ private static final String PATH_PROPERTY = "@path";
+ private static final String BEFORE_PROPERTY = "before";
+ private static final String AFTER_PROPERTY = "after";
+ private static final String TEXT_PROPERTY = "text";
+
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+ @Test
+ public void escapeLikeTextReplacesUnderscoreChar()
+ {
+ String input = "answer_value";
+ String processedText = SearchUtils.escapeLikeText(input);
+ assertEquals("answer\\_value", processedText);
+ }
+
+ @Test
+ public void escapeLikeTextReplacesBackSlashChar()
+ {
+ String input = "answer\\value";
+ String processedText = SearchUtils.escapeLikeText(input);
+ assertEquals("answer\\\\value", processedText);
+ }
+
+ @Test
+ public void escapeLikeTextReplacesPercentChar()
+ {
+ String input = "answer%value";
+ String processedText = SearchUtils.escapeLikeText(input);
+ assertEquals("answer\\%value", processedText);
+ }
+
+ @Test
+ public void escapeLikeTextReplacesQuotationMarkChar()
+ {
+ String input = "answer'value";
+ String processedText = SearchUtils.escapeLikeText(input);
+ assertEquals("answer\\'value", processedText);
+ }
+
+ @Test
+ public void escapeLikeTextReplacesSeveralChars()
+ {
+ String input = "answer'value_test";
+ String processedText = SearchUtils.escapeLikeText(input);
+ assertEquals("answer\\'value\\_test", processedText);
+ }
+
+ @Test
+ public void escapeQueryArgumentReplacesQuotationMarkChar()
+ {
+ String input = "'value'";
+ String processedText = SearchUtils.escapeQueryArgument(input);
+ assertEquals("''value''", processedText);
+ }
+
+ @Test
+ public void getMatchFromArrayReturnsFirstMatchIgnoreCase()
+ {
+ String searchedSubstring = "name";
+ String firstMatch = SearchUtils.getMatchFromArray(new String[]{
+ "email", "firstName", "age", "lastname"
+ }, searchedSubstring);
+ assertEquals("firstName", firstMatch);
+ }
+
+ @Test
+ public void getMatchFromArrayWithNoMatchReturnsNull()
+ {
+ String searchedSubstring = "name";
+ String firstMatch = SearchUtils.getMatchFromArray(new String[]{
+ "email", "age"
+ }, searchedSubstring);
+ assertNull(firstMatch);
+ }
+
+ @Test
+ public void getMatchFromArrayForNullArrayReturnsNull()
+ {
+ String searchedSubstring = "name";
+ String firstMatch = SearchUtils.getMatchFromArray(null, searchedSubstring);
+ assertNull(firstMatch);
+ }
+
+ @Test
+ public void addMatchMetadata()
+ {
+ String resourceValue = "MatchFirstNameValueAnswer";
+ String question = "What is your firstName?";
+ String query = "name";
+ String path = "/Forms/f1/a1";
+ JsonObjectBuilder parent = Json.createObjectBuilder();
+ parent.add(NODE_TYPE, FORM_TYPE);
+
+ JsonObject jsonObject =
+ SearchUtils.addMatchMetadata(resourceValue, query, question, parent.build(), false, path);
+ assertNotNull(jsonObject);
+ assertTrue(jsonObject.containsKey(NODE_TYPE));
+ assertEquals(FORM_TYPE, jsonObject.getString(NODE_TYPE));
+ assertTrue(jsonObject.containsKey(QUERY_MATCH_PROPERTY));
+ JsonObject queryMatchObject = jsonObject.getJsonObject(QUERY_MATCH_PROPERTY);
+ assertTrue(queryMatchObject.containsKey(QUESTION_PROPERTY));
+ assertEquals(question, queryMatchObject.getString(QUESTION_PROPERTY));
+ assertTrue(queryMatchObject.containsKey(IN_NOTES_PROPERTY));
+ assertFalse(queryMatchObject.getBoolean(IN_NOTES_PROPERTY));
+ assertTrue(queryMatchObject.containsKey(PATH_PROPERTY));
+ assertEquals(path, queryMatchObject.getString(PATH_PROPERTY));
+ assertTrue(queryMatchObject.containsKey(BEFORE_PROPERTY));
+ assertEquals("...tchFirst", queryMatchObject.getString(BEFORE_PROPERTY));
+ assertTrue(queryMatchObject.containsKey(AFTER_PROPERTY));
+ assertEquals("ValueAns...", queryMatchObject.getString(AFTER_PROPERTY));
+ assertTrue(queryMatchObject.containsKey(TEXT_PROPERTY));
+ assertEquals("Name", queryMatchObject.getString(TEXT_PROPERTY));
+ }
+}
diff --git a/modules/data-entry/src/test/resources/Questionnaires.json b/modules/data-entry/src/test/resources/Questionnaires.json
new file mode 100644
index 0000000000..9e903e22d0
--- /dev/null
+++ b/modules/data-entry/src/test/resources/Questionnaires.json
@@ -0,0 +1,71 @@
+{
+ "jcr:primaryType": "cards:Questionnaire",
+ "title": "Test Serializable Questionnaire",
+ "description": "A test serializable questionnaire",
+ "question_1": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Long Question",
+ "dataType": "long",
+ "maxAnswers": 1
+ },
+ "question_2": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Double Question",
+ "dataType": "double",
+ "maxAnswers": 1
+ },
+ "question_3": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Decimal Question",
+ "dataType": "decimal",
+ "maxAnswers": 1
+ },
+ "question_4": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Boolean Question",
+ "dataType": "boolean",
+ "maxAnswers": 1
+ },
+ "question_5": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Date Question",
+ "dataType": "date",
+ "maxAnswers": 1,
+ "dateFormat": "yyyy-MM-dd"
+ },
+ "section_1": {
+ "jcr:primaryType": "cards:Section",
+ "label": "Section 1",
+ "question_6": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Text Question",
+ "dataType": "text"
+ },
+ "question_7": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Text2 Question",
+ "dataType": "text"
+ }
+ },
+ "question_8": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Options Question",
+ "dataType": "vocabulary",
+ "displayMode": "list",
+ "maxAnswers": 1,
+ "o1": {
+ "jcr:primaryType": "cards:AnswerOption",
+ "value": "/Vocabularies/Option1"
+ },
+ "o2": {
+ "jcr:primaryType": "cards:AnswerOption",
+ "value": "/Vocabularies/Option2"
+ }
+ },
+ "question_9": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Time Question",
+ "dataType": "time",
+ "maxAnswers": 1
+ }
+}
\ No newline at end of file
diff --git a/modules/data-entry/src/test/resources/SubjectTypes.json b/modules/data-entry/src/test/resources/SubjectTypes.json
new file mode 100644
index 0000000000..5620a1c833
--- /dev/null
+++ b/modules/data-entry/src/test/resources/SubjectTypes.json
@@ -0,0 +1,18 @@
+{
+ "jcr:primaryType": "cards:SubjectType",
+ "label": "Root",
+ "subjectListLabel" : "Roots",
+ "cards:defaultOrder": 0,
+ "Branch": {
+ "jcr:primaryType": "cards:SubjectType",
+ "label": "Branch",
+ "subjectListLabel" : "Branches",
+ "cards:defaultOrder": 1,
+ "Leaf": {
+ "jcr:primaryType": "cards:SubjectType",
+ "label": "Leaf",
+ "subjectListLabel" : "Leafs",
+ "cards:defaultOrder": 2
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/data-entry/src/test/resources/SubjectTypesPatient.json b/modules/data-entry/src/test/resources/SubjectTypesPatient.json
new file mode 100644
index 0000000000..08936f09b3
--- /dev/null
+++ b/modules/data-entry/src/test/resources/SubjectTypesPatient.json
@@ -0,0 +1,6 @@
+{
+ "jcr:primaryType": "cards:SubjectType",
+ "label": "Patient",
+ "subjectListLabel" : "Patients",
+ "cards:defaultOrder": 0
+}
\ No newline at end of file
diff --git a/modules/data-entry/src/test/resources/TextQuestionnaires.json b/modules/data-entry/src/test/resources/TextQuestionnaires.json
new file mode 100644
index 0000000000..6487e4af85
--- /dev/null
+++ b/modules/data-entry/src/test/resources/TextQuestionnaires.json
@@ -0,0 +1,21 @@
+{
+ "jcr:primaryType": "cards:Questionnaire",
+ "title": "Test Text Questionnaire",
+ "description": "A test text questionnaire",
+ "question_1": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Text Question",
+ "dataType": "text",
+ "maxAnswers": 2
+ },
+ "question_6": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Text1 Question",
+ "dataType": "text"
+ },
+ "question_10": {
+ "jcr:primaryType": "cards:Question",
+ "text": "Text2 Question",
+ "dataType": "text"
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index f9bd35a3f4..babc851e5d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -293,6 +293,12 @@
${oak.version}
provided
+
+ org.apache.jackrabbit
+ oak-jcr
+ ${oak.version}
+ provided
+
org.apache.jackrabbit
oak-api
@@ -389,7 +395,10 @@
maven-compiler-plugin
- -Xlint:all
+
+ -Werror
+ -Xlint:all
+
true
true
true
@@ -2044,6 +2053,7 @@
modules
+ aggregated-cnd
aggregated-frontend
lfs-resources
heracles-resources