001/*
002 *  Copyright 2025 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.forms.schedulable;
017
018import java.time.Period;
019import java.time.ZonedDateTime;
020import java.util.List;
021import java.util.Map;
022
023import org.apache.avalon.framework.service.ServiceException;
024import org.apache.avalon.framework.service.ServiceManager;
025import org.quartz.JobExecutionContext;
026
027import org.ametys.core.schedule.progression.ContainerProgressionTracker;
028import org.ametys.core.trace.ForensicLogger;
029import org.ametys.core.user.population.UserPopulationDAO;
030import org.ametys.plugins.core.impl.schedule.AbstractStaticSchedulable;
031import org.ametys.plugins.forms.dao.FormEntryDAO;
032import org.ametys.plugins.forms.repository.Form;
033import org.ametys.plugins.forms.repository.Form.ExpirationPolicy;
034import org.ametys.plugins.forms.repository.FormEntry;
035import org.ametys.plugins.forms.repository.FormFactory;
036import org.ametys.plugins.repository.AmetysObjectIterable;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.query.QueryHelper;
039import org.ametys.plugins.repository.query.expression.AndExpression;
040import org.ametys.plugins.repository.query.expression.BooleanExpression;
041import org.ametys.plugins.repository.query.expression.DateExpression;
042import org.ametys.plugins.repository.query.expression.Expression;
043import org.ametys.plugins.repository.query.expression.Expression.Operator;
044import org.ametys.plugins.repository.query.expression.MetadataExpression;
045import org.ametys.plugins.repository.query.expression.NotExpression;
046
047/**
048 * Schedular to delete or anonymize all expired form entries
049 */
050public class FormEntriesExpirationSchedulable extends AbstractStaticSchedulable
051{
052    private AmetysObjectResolver _resolver;
053    private FormEntryDAO _formEntryDAO;
054
055    @Override
056    public void service(ServiceManager manager) throws ServiceException
057    {
058        super.service(manager);
059        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
060        _formEntryDAO = (FormEntryDAO) manager.lookup(FormEntryDAO.ROLE);
061    }
062    
063    public void execute(JobExecutionContext context, ContainerProgressionTracker progressionTracker) throws Exception
064    {
065        String query = QueryHelper.getXPathQuery(null, FormFactory.FORM_NODETYPE, new BooleanExpression(Form.EXPIRATION_ENABLED, true));
066        try (AmetysObjectIterable<Form> queryResult = _resolver.query(query))
067        {
068            for (Form form: queryResult)
069            {
070                ExpirationPolicy policy = form.getExpirationPolicy();
071                
072                ZonedDateTime expirationLimit = ZonedDateTime.now().minus(Period.ofMonths((int) form.getExpirationPeriod()));
073                Expression dateExpr = new DateExpression(FormEntry.ATTRIBUTE_SUBMIT_DATE, Operator.LT, expirationLimit);
074                // Filter already anonymized entries when the policy is to anonymize
075                Expression expiredExpr = policy == ExpirationPolicy.ANONYMIZE ? new AndExpression(dateExpr, new NotExpression(new MetadataExpression(FormEntry.ATTRIBUTE_ANONYMIZATION_DATE))) : dateExpr;
076                
077                List<FormEntry> entries = _formEntryDAO.getFormEntries(form, false, expiredExpr, List.of());
078                if (!entries.isEmpty())
079                {
080                    for (FormEntry entry: entries)
081                    {
082                        switch (policy)
083                        {
084                            case DELETE:
085                                entry.remove();
086                                break;
087                            case ANONYMIZE:
088                                entry.anonymize();
089                                break;
090                            default:
091                                // not possible
092                                getLogger().error("Unsupported expiration policy '{}' for form '{}'. Action is ignored", policy, form.getId());
093                                break;
094                        }
095                        entry.saveChanges();
096                    }
097                    String category = switch (policy)
098                    {
099                        case DELETE -> "data.policy.gdpr.remove.form.submissions.expiration";
100                        case ANONYMIZE -> "data.policy.gdpr.anonymize.form.submissions.expiration";
101                        default -> "data.policy.gdpr.form.submissions.expiration";
102                    };
103                    ForensicLogger.info(category, Map.of("handled", Long.toString(entries.size()), "form", form.getId(), "formTitle", form.getTitle()), UserPopulationDAO.SYSTEM_USER_IDENTITY);
104                }
105            }
106        }
107    }
108}