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}