001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.scheduler.jobs;
029
030import org.opencms.db.CmsResourceState;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProject;
033import org.opencms.file.CmsProperty;
034import org.opencms.file.CmsPropertyDefinition;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsResourceFilter;
037import org.opencms.file.types.I_CmsResourceType;
038import org.opencms.loader.CmsResourceManager;
039import org.opencms.lock.CmsLock;
040import org.opencms.main.CmsException;
041import org.opencms.main.OpenCms;
042import org.opencms.publish.CmsPublishManager;
043import org.opencms.report.CmsLogReport;
044import org.opencms.report.I_CmsReport;
045import org.opencms.scheduler.I_CmsScheduledJob;
046import org.opencms.util.CmsStringUtil;
047
048import java.util.Collections;
049import java.util.Iterator;
050import java.util.List;
051import java.util.Map;
052
053/**
054 * A schedulable OpenCms job to delete expired resources.<p>
055 *
056 * The user to execute the process should have have access to the required "Workplace manager" role.<p>
057 *
058 * The "Offline" project has to be configured for this job because the operations cannot be performed in the "Online" project.<p>
059 *
060 * Job parameters:<p>
061 * <dl>
062 * <dt><code>expirationdays={Number/Integer}</code></dt>
063 * <dd>Amount of days a resource has to be expired to be deleted.</dd>
064 * <dt><code>resourcetypes={csv list}</code></dt>
065 * <dd>Comma separated list of resource type names to specify the types of expired resources that may be deleted.
066 * If left out, expired resources of all types will be deleted. .</dd>
067 * <dt><code>folder={csv list}</code></dt>
068 * <dd>Allows to specify a comma separated list of folders in which all expired resources will be deleted. If omitted "/" will be taken as single folder
069 * for this operation. </dd>
070 * </dl>
071 * <p>
072 *
073 * The property "delete.expired" (<code>{@link CmsPropertyDefinition#PROPERTY_DELETE_EXPIRED}</code>) may be used
074 * to override the global setting of the parameter <code>expirationdays</code>. A value of "never", "false" or "none" will
075 * prevent resources from being deleted. Other values are "true" (default) or the amount of days a resource has
076 * to be expired for qualification of deletion.<p>
077 *
078 * Only published / unchanged files will be processed. Anything with unpublished changes will not
079 * be touched by the job. <p>
080 *
081 * Folders with expiration dates are ignored by default. Only if the scheduler parameter "resourcetypes" contains "folder"
082 * a folder that has been expired will be deleted (with all contained resources). <p>
083 *
084 * @since 7.5.0
085 */
086public class CmsDeleteExpiredResourcesJob implements I_CmsScheduledJob {
087
088    /** Name of the parameter where to configure the amount of days a resource has to be expired before deletion. */
089    public static final String PARAM_EXPIRATIONSDAYS = "expirationdays";
090
091    /** Name of the parameter where to configure the resource types for resources to delete if expired. */
092    public static final String PARAM_RESOURCETYPES = "resourcetypes";
093
094    /** Name of the parameter where to configure the folder below which the operation will be done. */
095    public static final String PARAM_FOLDER = "folder";
096
097    /** Constant for calculation. */
098    private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
099
100    /** Setting for the <code>{@link CmsPropertyDefinition#PROPERTY_DELETE_EXPIRED}</code> to disallow deletion. */
101    public static final String PROPERTY_VALUE_DELETE_EXPIRED_NEVER = "never";
102
103    /** Setting for the <code>{@link CmsPropertyDefinition#PROPERTY_DELETE_EXPIRED}</code> to disallow deletion. */
104    public static final String PROPERTY_VALUE_DELETE_EXPIRED_NONE = "none";
105
106    /**
107     * @see org.opencms.scheduler.I_CmsScheduledJob#launch(org.opencms.file.CmsObject, java.util.Map)
108     */
109    public String launch(CmsObject cms, Map<String, String> parameters) throws Exception {
110
111        // this job requires a higher runlevel than is allowed for all jobs:
112        if (OpenCms.getRunLevel() == OpenCms.RUNLEVEL_4_SERVLET_ACCESS) {
113            long currenttime = System.currentTimeMillis();
114
115            // read the parameter for the versions to keep
116            int expirationdays = 30;
117            String expirationdaysparam = parameters.get(PARAM_EXPIRATIONSDAYS);
118            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(expirationdaysparam)) {
119                try {
120                    expirationdays = Integer.parseInt(expirationdaysparam);
121                } catch (NumberFormatException nfe) {
122                    // don't care
123                }
124            }
125
126            // read the parameter if to clear versions of deleted resources
127            String resTypes = parameters.get(PARAM_RESOURCETYPES);
128            String[] resTypesArr = null;
129            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(resTypes)) {
130                resTypesArr = CmsStringUtil.splitAsArray(resTypes, ',');
131            }
132
133            // read the optional parameter for the time range to keep versions
134            String[] topFoldersArr = new String[] {"/"};
135            String topfolders = parameters.get(PARAM_FOLDER);
136            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(topfolders)) {
137                topFoldersArr = CmsStringUtil.splitAsArray(topfolders, ',');
138            }
139
140            // create a temp project for publishing everything together at the end:
141            CmsProject project = cms.createTempfileProject();
142            cms.getRequestContext().setCurrentProject(project);
143
144            I_CmsReport report = new CmsLogReport(
145                cms.getRequestContext().getLocale(),
146                CmsDeleteExpiredResourcesJob.class);
147            report.println(Messages.get().container(Messages.RPT_DELETE_EXPIRED_START_0), I_CmsReport.FORMAT_HEADLINE);
148
149            // collect all resources:
150            List<CmsResource> resources = Collections.emptyList();
151            CmsResourceFilter filter = CmsResourceFilter.ALL.addExcludeState(CmsResourceState.STATE_DELETED);
152            filter = filter.addRequireExpireBefore(currenttime);
153
154            // if we have configured resource types reading is more complicated because inclusion of several types
155            // is not supported by resource filter api:
156            int changedFiles = 0;
157            if (resTypesArr != null) {
158                I_CmsResourceType type;
159                CmsResourceManager resManager = OpenCms.getResourceManager();
160                for (int i = resTypesArr.length - 1; i >= 0; i--) {
161                    type = resManager.getResourceType(resTypesArr[i]);
162                    filter = filter.addRequireType(type.getTypeId());
163                    for (int j = topFoldersArr.length - 1; j >= 0; j--) {
164                        resources = cms.readResources(topFoldersArr[j], filter, true);
165                        changedFiles += deleteExpiredResources(cms, report, resources, expirationdays, currenttime);
166                    }
167                }
168
169            } else {
170                filter = filter.addRequireFile();
171                for (int j = topFoldersArr.length - 1; j >= 0; j--) {
172                    resources = cms.readResources(topFoldersArr[j], filter, true);
173                    changedFiles += deleteExpiredResources(cms, report, resources, expirationdays, currenttime);
174                }
175            }
176            if (changedFiles > 0) {
177                CmsPublishManager publishManager = OpenCms.getPublishManager();
178                publishManager.publishProject(cms, report);
179                // this is to not scramble the logging output:
180                publishManager.waitWhileRunning();
181            }
182            report.println(Messages.get().container(Messages.RPT_DELETE_EXPIRED_END_0), I_CmsReport.FORMAT_HEADLINE);
183        }
184        return null;
185    }
186
187    /**
188     * Deletes the expired resources if the have been expired longer than the given amount of days. <p>
189     *
190     * At this level the resource type is not checked again. <p>
191     *
192     * @param resources a <code>List</code> containing <code>CmsResource</code> instances to process.
193     * @param cms needed to delete resources
194     * @param report needed to print messages to
195     * @param expirationdays the amount of days a resource has to be expired before it is deleted
196     * @param currenttime the current time in milliseconds since January 1st 1970
197     * @return the amount of deleted files
198     *
199     */
200    private int deleteExpiredResources(
201        final CmsObject cms,
202        final I_CmsReport report,
203        final List<CmsResource> resources,
204        final int expirationdays,
205        final long currenttime) {
206
207        int result = 0;
208        CmsResource resource;
209        CmsLock lock;
210        CmsProperty property;
211        String propertyValue;
212        long expirationdate;
213        int expirationDaysPropertyOverride;
214        Iterator<CmsResource> it = resources.iterator();
215        String resourcePath;
216        while (it.hasNext()) {
217            resource = it.next();
218            resourcePath = cms.getRequestContext().removeSiteRoot(resource.getRootPath());
219            report.print(
220                Messages.get().container(Messages.RPT_DELETE_EXPIRED_PROCESSING_1, new String[] {resourcePath}),
221                I_CmsReport.FORMAT_DEFAULT);
222            report.print(org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_DOTS_0));
223
224            if (resource.getState() == CmsResourceState.STATE_UNCHANGED) {
225                expirationdate = resource.getDateExpired();
226                expirationDaysPropertyOverride = expirationdays;
227                try {
228                    property = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_DELETE_EXPIRED, true);
229                    propertyValue = property.getValue();
230                    if (!property.isNullProperty()) {
231                        if (PROPERTY_VALUE_DELETE_EXPIRED_NEVER.equals(propertyValue)
232                            || PROPERTY_VALUE_DELETE_EXPIRED_NONE.equals(propertyValue)
233                            || Boolean.FALSE.toString().equals(propertyValue)) {
234                            report.println(
235                                Messages.get().container(Messages.RPT_DELETE_EXPIRED_PROPERTY_NEVER_0),
236                                I_CmsReport.FORMAT_NOTE);
237                            continue;
238                        } else {
239                            // true is allowed, but any other value will be treated as a configuration error and skip the
240                            // resource:
241
242                            if (!Boolean.TRUE.toString().equals(propertyValue)) {
243                                // NumberFormatException should skip the resource because the property value was mistyped
244                                expirationDaysPropertyOverride = Integer.parseInt(propertyValue);
245                            }
246                        }
247                    }
248
249                    // no Calendar - semantics required for simple timespan check:
250                    if ((expirationdate != Long.MAX_VALUE)
251                        && ((currenttime - expirationdate) > (expirationDaysPropertyOverride * MILLIS_PER_DAY))) {
252                        lock = cms.getLock(resource);
253                        if (lock.isNullLock()) {
254                            cms.lockResource(resourcePath);
255                        } else {
256                            if (!lock.getUserId().equals(cms.getRequestContext().getCurrentUser().getId())) {
257                                report.println(
258                                    Messages.get().container(Messages.RPT_DELETE_EXPIRED_LOCKED_0),
259                                    I_CmsReport.FORMAT_WARNING);
260                                continue;
261                            }
262                        }
263                        cms.deleteResource(resourcePath, CmsResource.DELETE_PRESERVE_SIBLINGS);
264                        result++;
265                        report.println(
266                            org.opencms.report.Messages.get().container(org.opencms.report.Messages.RPT_OK_0),
267                            I_CmsReport.FORMAT_OK);
268
269                    } else {
270                        report.println(
271                            Messages.get().container(
272                                Messages.RPT_DELETE_EXPIRED_NOT_EXPIRED_1,
273                                new Integer[] {Integer.valueOf(expirationDaysPropertyOverride)}));
274                    }
275                } catch (Exception e) {
276                    report.println(
277                        Messages.get().container(
278                            Messages.RPT_DELETE_EXPIRED_FAILED_1,
279                            new String[] {CmsException.getStackTraceAsString(e)}),
280                        I_CmsReport.FORMAT_ERROR);
281
282                }
283            } else {
284                report.println(Messages.get().container(Messages.RPT_DELETE_EXPIRED_UNPUBLISHED_0));
285            }
286        }
287        return result;
288    }
289}