Skip to content

Commit

Permalink
CARDS-2021: PREMs: Automatically delete submitted survey responses ol…
Browse files Browse the repository at this point in the history
…der than one year
  • Loading branch information
veronikaslc committed Feb 9, 2023
1 parent 4b419db commit 2a907db
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 0 deletions.
8 changes: 8 additions & 0 deletions prems-resources/backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,13 @@
<artifactId>org.osgi.framework</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component</artifactId>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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.prems.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.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.uhndata.cards.resolverProvider.ThreadResourceResolverProvider;

/**
* Automatically delete submitted survey responses older than max age.
*
* @version $Id$
* @since 0.9.6
*/
@Designate(ocd = SubmittedFormsCleanupScheduler.Config.class, factory = true)
@Component(immediate = true)
public class SubmittedFormsCleanupScheduler
{
/** Default log. */
private static final Logger LOGGER = LoggerFactory.getLogger(SubmittedFormsCleanupScheduler.class);

private static final String SCHEDULER_JOB_NAME = "SubmittedFormsCleanup";

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

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

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

@ObjectClassDefinition(name = "Submitted survey responses cleanup",
description = "Automatically delete submitted survey responses older than one max age")
public static @interface Config
{
/** Default value of how long the submissions can be kept in the database in days. */
int MAX_AGE = 365;

@AttributeDefinition(
name = "Max age of submitted survey responses",
description = "Days of how long submissions can be kept in the database")
int maxAgeDays() default MAX_AGE;
}

@Activate
private void activate(final Config config, final ComponentContext componentContext)
{
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 SubmittedFormsCleanupTask(this.resolverFactory, this.rrp,
config.maxAgeDays());
this.scheduler.schedule(cleanupJob, options);
} catch (final Exception e) {
LOGGER.error("UnsubmittedFormsCleanup failed to schedule: {}", e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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.prems.patients.internal;

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

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.resolverProvider.ThreadResourceResolverProvider;

/**
* Periodically delete submitted survey responses older than max age.
*
* @version $Id$
* @since 0.9.6
*/
public class SubmittedFormsCleanupTask implements Runnable
{

/** Default log. */
private static final Logger LOGGER = LoggerFactory.getLogger(SubmittedFormsCleanupTask.class);

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

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

private final int maxAgeDays;

/**
* @param resolverFactory a valid ResourceResolverFactory providing access to resources
* @param patientAccessConfiguration details on patient authentication for token lifetime purposes
* @param maxAgeDays OSGi config defines days of how long submissions can be kept in the database
*/
SubmittedFormsCleanupTask(final ResourceResolverFactory resolverFactory, final ThreadResourceResolverProvider rrp,
final int maxAgeDays)
{
this.resolverFactory = resolverFactory;
this.rrp = rrp;
this.maxAgeDays = maxAgeDays;
}

@Override
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 visitInformationQuestionnaire =
(String) resolver.getResource("/Questionnaires/Visit information").getValueMap().get("jcr:uuid");
final String submitted = (String) resolver
.getResource("/Questionnaires/Visit information/surveys_submitted").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"
+ " inner join [cards:Answer] as submitted on isdescendantnode(submitted, visitInformation)"
+ " where"
// link to the correct Visit Information questionnaire
+ " visitInformation.questionnaire = '%1$s'"
// the data form was last modified by the patient before the allowed timeframe
+ " and dataForm.[jcr:lastModified] < '%2$s'"
// the visit is submitted
+ " and submitted.question = '%3$s'"
+ " and submitted.value = 1"
// exclude the Visit Information form itself
+ " and dataForm.questionnaire <> '%1$s'",
visitInformationQuestionnaire, ZonedDateTime.now().minusDays(this.maxAgeDays), submitted),
Query.JCR_SQL2);
resources.forEachRemaining(form -> {
try {
resolver.delete(form);
} catch (final PersistenceException e) {
LOGGER.warn("Failed to delete expired survey form {}: {}", form.getPath(), e.getMessage());
}
});
resolver.commit();
} catch (final LoginException e) {
LOGGER.warn("Invalid setup, service rights not set up for the expired survey forms cleanup task");
} catch (final PersistenceException e) {
LOGGER.warn("Failed to delete expired survey responses forms: {}", e.getMessage());
} finally {
if (mustPopResolver) {
this.rrp.pop();
}
}
}
}

0 comments on commit 2a907db

Please sign in to comment.