001/*
002 *  Copyright 2021 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.odf.schedulable;
017
018import java.io.File;
019import java.io.FileOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Random;
030
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.components.source.impl.SitemapSource;
035import org.apache.commons.io.FileUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.excalibur.source.SourceResolver;
038import org.apache.excalibur.source.SourceUtil;
039import org.quartz.JobDataMap;
040import org.quartz.JobExecutionContext;
041
042import org.ametys.cms.schedule.AbstractSendingMailSchedulable;
043import org.ametys.odf.ODFHelper;
044import org.ametys.odf.enumeration.OdfReferenceTableHelper;
045import org.ametys.odf.orgunit.OrgUnit;
046import org.ametys.plugins.core.schedule.Scheduler;
047import org.ametys.plugins.repository.AmetysObjectResolver;
048import org.ametys.runtime.config.Config;
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.runtime.model.ElementDefinition;
051import org.ametys.runtime.util.AmetysHomeHelper;
052import org.ametys.runtime.workspace.WorkspaceMatcher;
053
054/**
055 * Schedulable to export the ODF catalog as PDF
056 */
057public class CatalogPDFExportSchedulable extends AbstractSendingMailSchedulable
058{
059    /** The key for the catalog */
060    public static final String JOBDATAMAP_CATALOG_KEY = "catalog";
061    /** The key for the lang */
062    public static final String JOBDATAMAP_LANG_KEY = "lang";
063    /** The key for the orgunit */
064    public static final String JOBDATAMAP_ORGUNIT_KEY = "orgunit";
065    /** The key for the degree */
066    public static final String JOBDATAMAP_DEGREE_KEY = "degree";
067
068    /** Map key where the generated filename is stored */
069    protected static final String _CATALOG_FILENAME = "catalogFilename";
070    
071    /** The Ametys object resolver. */
072    protected AmetysObjectResolver _resolver;
073    /** The ODF reference table helper. */
074    protected OdfReferenceTableHelper _odfRefTableHelper;
075    /** The avalon source resolver. */
076    protected SourceResolver _sourceResolver;
077
078    /** The catalog directory. */
079    protected File _catalogRootDirectory;
080    
081    @Override
082    public void service(ServiceManager manager) throws ServiceException
083    {
084        super.service(manager);
085        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
086        _odfRefTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
087        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
088    }
089    
090    @Override
091    public void initialize() throws Exception
092    {
093        super.initialize();
094        _catalogRootDirectory = new File(AmetysHomeHelper.getAmetysHomeData(), "odf/catalog");
095    }
096    
097    @Override
098    protected void _doExecute(JobExecutionContext context) throws Exception
099    {
100        SitemapSource source = null;
101        File pdfTmpFile = null;
102        try
103        {
104            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
105            String catalog = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY);
106            String lang = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_LANG_KEY);
107            
108            FileUtils.forceMkdir(_catalogRootDirectory);
109            
110            File catalogDir = new File (_catalogRootDirectory, catalog);
111            if (!catalogDir.exists())
112            {
113                catalogDir.mkdir();
114            }
115            
116            File langDir = new File (catalogDir, lang);
117            if (!langDir.exists())
118            {
119                langDir.mkdir();
120            }
121            
122            String filenamePrefix = "catalog";
123            
124            // Resolve the export to the appropriate pdf url.
125            Map<String, String> params = new HashMap<>();
126            
127            // Org unit
128            String orgunit = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_ORGUNIT_KEY);
129            if (StringUtils.isNotEmpty(orgunit))
130            {
131                filenamePrefix += "-" + _resolver.<OrgUnit>resolveById(orgunit).getUAICode();
132                params.put(JOBDATAMAP_ORGUNIT_KEY, orgunit);
133            }
134            
135            // Degree
136            String degree = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_DEGREE_KEY);
137            if (StringUtils.isNotEmpty(degree))
138            {
139                filenamePrefix += "-" + _odfRefTableHelper.getItemCode(degree);
140                params.put(JOBDATAMAP_DEGREE_KEY, degree);
141            }
142            
143            // Set the attribute to force the switch to live data
144            ContextHelper.getRequest(_context).setAttribute(ODFHelper.REQUEST_ATTRIBUTE_VALID_LABEL, true);
145            source = (SitemapSource) _sourceResolver.resolveURI("cocoon://_plugins/odf/programs/" + catalog + "/" + lang + "/catalog.pdf", null, params);
146            
147            // Save the pdf into a temporary file.
148            String tmpFilename = filenamePrefix + "-" + new Random().nextInt() + ".tmp.pdf";
149            pdfTmpFile = new File(langDir, tmpFilename);
150            
151            try (
152                OutputStream pdfTmpOs = new FileOutputStream(pdfTmpFile);
153                InputStream sourceIs = source.getInputStream()
154            )
155            {
156                SourceUtil.copy(sourceIs, pdfTmpOs);
157            }
158            
159            // If all went well until now, rename the temporary file 
160            File catalogFile = new File(langDir, filenamePrefix + ".pdf");
161            if (catalogFile.exists())
162            {
163                catalogFile.delete();
164            }
165            
166            context.put(_CATALOG_FILENAME, catalogFile.getName());
167            
168            if (!pdfTmpFile.renameTo(catalogFile))
169            {
170                throw new IOException("Fail to rename catalog.tmp.pdf to catalog.pdf");
171            }
172        }
173        finally
174        {
175            if (pdfTmpFile != null)
176            {
177                FileUtils.deleteQuietly(pdfTmpFile);
178            }
179            
180            if (source != null)
181            {
182                _sourceResolver.release(source);
183            }
184        }
185        
186    }
187    
188    @SuppressWarnings("unchecked")
189    @Override
190    // FIXME CMS-9980
191    public Map<String, ElementDefinition> getParameters()
192    {
193        Map<String, ElementDefinition> original = super.getParameters();
194        
195        boolean isAdminContext = Optional.ofNullable(_context)
196            .map(ContextHelper::getRequest)
197            .map(r -> r.getAttribute(WorkspaceMatcher.WORKSPACE_NAME))
198            .map("admin"::equals)
199            .orElse(false);
200        
201        // Remove the widget on orgunit and degree fields if we are in an admin context
202        if (isAdminContext)
203        {
204            // Copy to avoid the modification of the original object
205            Map<String, ElementDefinition> copy = new HashMap<>(original);
206            copy.put(JOBDATAMAP_ORGUNIT_KEY, _removeWidget(original.get(JOBDATAMAP_ORGUNIT_KEY)));
207            copy.put(JOBDATAMAP_DEGREE_KEY, _removeWidget(original.get(JOBDATAMAP_DEGREE_KEY)));
208            return copy;
209        }
210        
211        return original;
212    }
213    
214    private <T> ElementDefinition<T> _removeWidget(ElementDefinition<T> originalElement)
215    {
216        ElementDefinition<T> copiedElement = new ElementDefinition<>(originalElement);
217        copiedElement.setWidget(null);
218        copiedElement.setWidgetParameters(Collections.EMPTY_MAP);
219        return copiedElement;
220    }
221
222    @Override
223    protected I18nizableText _getSuccessMailSubject(JobExecutionContext context) throws Exception
224    {
225        return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT");
226    }
227
228    @Override
229    protected I18nizableText _getSuccessMailBody(JobExecutionContext context) throws Exception
230    {
231        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
232        String catalog = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_CATALOG_KEY);
233        String lang = jobDataMap.getString(Scheduler.PARAM_VALUES_PREFIX + JOBDATAMAP_LANG_KEY);
234        
235        String downloadLink = StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html");
236        downloadLink += (downloadLink.endsWith("/") ? "" : "/") + "plugins/odf/download/" + catalog + "/" + lang + "/" + context.get(_CATALOG_FILENAME);
237
238        List<String> params = new ArrayList<>();
239        params.add(downloadLink); // {0}
240        
241        return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_SUCCESS", params);
242    }
243
244    @Override
245    protected I18nizableText _getErrorMailSubject(JobExecutionContext context) throws Exception
246    {
247        return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_SUBJECT");
248    }
249
250    @Override
251    protected I18nizableText _getErrorMailBody(JobExecutionContext context, Throwable throwable) throws Exception
252    {
253        return new I18nizableText("plugin.odf", "PLUGINS_ODF_PDF_EXPORT_MAIL_BODY_FAILURE");
254    }
255}