Skip to content

Commit

Permalink
CARDS-2026: Automatically delete names and emails once they are no lo…
Browse files Browse the repository at this point in the history
…nger serving any purpose
  • Loading branch information
veronikaslc committed Feb 1, 2023
1 parent d8359dd commit 50195da
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 0 deletions.
5 changes: 5 additions & 0 deletions modules/patient-portal/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@
<artifactId>cards-http-requests</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>cards-data-model-forms-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.servlets.annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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.patients.internal;

import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.uhndata.cards.forms.api.FormUtils;
import io.uhndata.cards.resolverProvider.ThreadResourceResolverProvider;

/**
* Schedule the cleanup of Patient information data every midnight.
*
* @version $Id$
* @since 0.9.2
*/
@Component(immediate = true)
public class PatientInformationCleanupScheduler
{
/** Default log. */
private static final Logger LOGGER = LoggerFactory.getLogger(PatientInformationCleanupScheduler.class);

private static final String SCHEDULER_JOB_NAME = "PatientInformationCleanup";

/** Provides access to resources. */
@Reference
private ResourceResolverFactory resolverFactory;

/** For sharing the resource resolver with other services. */
@Reference
private ThreadResourceResolverProvider rrp;

/** The utils for working with form data. */
@Reference
private FormUtils formUtils;

/** The scheduler for rescheduling jobs. */
@Reference
private Scheduler scheduler;

@Activate
protected void activate(final ComponentContext componentContext) throws Exception
{
try {
// Every night at midnight
final ScheduleOptions options = this.scheduler.EXPR("0 0 0 * * ? *");
options.name(SCHEDULER_JOB_NAME);
options.canRunConcurrently(false);

final Runnable cleanupJob = new PatientInformationCleanupTask(this.resolverFactory, this.rrp,
this.formUtils);
this.scheduler.schedule(cleanupJob, options);
} catch (final Exception e) {
LOGGER.error("PatientInformationCleanup failed to schedule: {}", e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* 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.patients.internal;

import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.jcr.query.Query;

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.uhndata.cards.auth.token.impl.CardsTokenImpl;
import io.uhndata.cards.forms.api.FormUtils;
import io.uhndata.cards.resolverProvider.ThreadResourceResolverProvider;

/**
* Periodically clears last_name, first_name, email from any Patient information form of patient subjects
* for whom the last Visit information form has surveys_submitted set to true or the token associated with the visit
* has expired.
*
* @version $Id$
* @since 0.9.2
*/
public class PatientInformationCleanupTask implements Runnable
{
/** Default log. */
private static final Logger LOGGER = LoggerFactory.getLogger(PatientInformationCleanupTask.class);

/** Provides access to resources. */
private final ResourceResolverFactory resolverFactory;

/** For sharing the resource resolver with other services. */
private final ThreadResourceResolverProvider rrp;

private final FormUtils formUtils;

/**
* @param resolverFactory a valid ResourceResolverFactory providing access to resources
* @param rrp ThreadResourceResolverProvider sharing the resource resolver with other services
* @param formUtils for working with form data
*/
PatientInformationCleanupTask(final ResourceResolverFactory resolverFactory,
final ThreadResourceResolverProvider rrp, final FormUtils formUtils)
{
this.resolverFactory = resolverFactory;
this.rrp = rrp;
this.formUtils = formUtils;
}

@Override
@SuppressWarnings("checkstyle:CyclomaticComplexity")
public void run()
{
boolean mustPopResolver = false;
try (ResourceResolver resolver = this.resolverFactory
.getServiceResourceResolver(Map.of(ResourceResolverFactory.SUBSERVICE, "VisitFormsPreparation"))) {
this.rrp.push(resolver);
mustPopResolver = true;

// Gather the needed UUIDs to place in the query
final String patientInformationQuestionnaire =
(String) resolver.getResource("/Questionnaires/Patient information").getValueMap().get("jcr:uuid");
final String visitInformationQuestionnaire =
(String) resolver.getResource("/Questionnaires/Visit information").getValueMap().get("jcr:uuid");

// Query:
final Iterator<Resource> resources = resolver.findResources(String.format(
// select the data forms
"select distinct dataForm.*"
+ " from [cards:Form] as dataForm"
// belonging to a visit
+ " inner join [cards:Form] as visitInformation on visitInformation.subject = dataForm.subject"
+ " where"
// link to the correct Visit Information questionnaire
+ " visitInformation.questionnaire = '%1$s'"
// link to the correct Patient Information questionnaire
+ " and dataForm.questionnaire = '%2$s'",
visitInformationQuestionnaire, patientInformationQuestionnaire),
Query.JCR_SQL2);
resources.forEachRemaining(form -> {
try {
Node formNode = form.adaptTo(Node.class);
if (!canDeleteInformation(formNode, resolver, visitInformationQuestionnaire,
patientInformationQuestionnaire)) {
return;
}
final NodeIterator children = formNode.getNodes();
while (children.hasNext()) {
final Node child = children.nextNode();
if (child.isNodeType("cards:Answer")) {
final String name = child.getProperty("question").getNode().getName();
if ("first_name".equals(name) || "last_name".equals(name) || "email".equals(name)) {
child.remove();
}
}
}
} catch (final RepositoryException e) {
LOGGER.warn("Failed to delete patient information {}: {}", form.getPath(), e.getMessage());
}
});
resolver.commit();
} catch (final LoginException e) {
LOGGER.warn("Invalid setup, service rights not set up for the patient information cleanup task");
} catch (final PersistenceException e) {
LOGGER.warn("Failed to delete patient information: {}", e.getMessage());
} finally {
if (mustPopResolver) {
this.rrp.pop();
}
}
}

private boolean canDeleteInformation(final Node form, final ResourceResolver resolver, final String visitQ,
final String patientQ) throws ValueFormatException, PathNotFoundException, RepositoryException
{
// run query to get all associated Visit information forms sorted by time property
Iterator<Resource> resources = resolver.findResources(String.format(
// select the data forms
"select distinct visitInformation.*"
+ " from [cards:Form] as visitInformation"
// belonging to a visit
+ " inner join [cards:Form] as dataForm on visitInformation.subject = dataForm.subject"
+ " where"
// link to the correct Visit Information questionnaire
+ " visitInformation.questionnaire = '%1$s'"
// link to the correct Patient Information questionnaire
+ " and dataForm.questionnaire = '%2$s'"
+ " and dataForm.[jcr:uuid] = '%3$s'"
+ " order by visitInformation.time desc",
visitQ, patientQ, form.getProperty("jcr:uuid").getString()),
Query.JCR_SQL2);

if (!resources.hasNext()) {
return false;
}
// get the last visit and see if it's surveys_submitted = true
final Node visit = resources.next().adaptTo(Node.class);
if (visit == null) {
return false;
}
if (visit.hasProperty("surveys_submitted") && visit.getProperty("surveys_submitted").getBoolean()) {
return true;
}

final Node visitSubjectPath = this.formUtils.getSubject(visit);
// run the query to get an expired token associated with patient name and visit
resources = resolver.findResources(String.format(
"select * from [cards:Token]"
+ " where"
+ " [" + CardsTokenImpl.TOKEN_ATTRIBUTE_EXPIRY + "] < '%1$s'"
+ " and [cards:sessionSubject] = '%2$s'",
ZonedDateTime.now(), visitSubjectPath.getPath()),
Query.JCR_SQL2);
if (resources.hasNext()) {
return true;
}

return false;
}
}

0 comments on commit 50195da

Please sign in to comment.