Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CARDS-2021: PREMs: Automatically delete submitted survey responses older than one year #1325

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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.16
*/
@Designate(ocd = SubmittedFormsCleanupScheduler.Config.class)
@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 @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.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.16
*/
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();
}
}
}
}