001/*
002 *  Copyright 2017 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.extraction.edition;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.nio.file.Paths;
024import java.nio.file.StandardCopyOption;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029import java.util.stream.Collectors;
030import java.util.stream.IntStream;
031import java.util.stream.Stream;
032
033import javax.xml.parsers.DocumentBuilder;
034import javax.xml.parsers.DocumentBuilderFactory;
035import javax.xml.transform.OutputKeys;
036import javax.xml.transform.Transformer;
037import javax.xml.transform.TransformerFactory;
038import javax.xml.transform.dom.DOMSource;
039import javax.xml.transform.sax.SAXTransformerFactory;
040import javax.xml.transform.sax.TransformerHandler;
041import javax.xml.transform.stream.StreamResult;
042
043import org.apache.avalon.framework.service.ServiceException;
044import org.apache.avalon.framework.service.ServiceManager;
045import org.apache.cocoon.xml.AttributesImpl;
046import org.apache.cocoon.xml.XMLUtils;
047import org.apache.excalibur.source.Source;
048import org.apache.excalibur.source.SourceResolver;
049import org.apache.excalibur.source.impl.FileSource;
050import org.apache.xml.serializer.OutputPropertiesFactory;
051import org.w3c.dom.Document;
052import org.w3c.dom.Element;
053import org.w3c.dom.NodeList;
054
055import org.ametys.cms.repository.ContentDAO;
056import org.ametys.cms.workflow.ContentWorkflowHelper;
057import org.ametys.core.ui.Callable;
058import org.ametys.core.ui.StaticClientSideElement;
059import org.ametys.core.util.I18nUtils;
060import org.ametys.plugins.extraction.ExtractionConstants;
061import org.ametys.runtime.i18n.I18nizableText;
062
063/**
064 * This client site element manages a button to create an extraction definition file
065 */
066public class EditExtractionClientSideElement extends StaticClientSideElement
067{
068    /** The Avalon role name */
069    public static final String ROLE = EditExtractionClientSideElement.class.getName();
070    
071    private SourceResolver _sourceResolver;
072    private ContentWorkflowHelper _contentWorkflowHelper;
073    private I18nUtils _i18nUtils;
074    private ContentDAO _contentDAO;
075    
076    @Override
077    public void service(ServiceManager serviceManager) throws ServiceException
078    {
079        super.service(serviceManager);
080        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
081        _contentWorkflowHelper = (ContentWorkflowHelper) serviceManager.lookup(ContentWorkflowHelper.ROLE);
082        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
083        _contentDAO = (ContentDAO) serviceManager.lookup(ContentDAO.ROLE);
084    }
085
086    /**
087     * Creates an extraction definition file.
088     * @param definitionFileName The extraction definition name
089     * @param language the language used to create the description
090     * @return Map containing success boolean and error codes if one occurs
091     * @throws Exception if an error occurs
092     */
093    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
094    public Map<String, Object> createExtraction(String definitionFileName, String language) throws Exception
095    {
096        // Create extraction definitions directory
097        Source definitionsSrc = _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
098        File definitionsDir = ((FileSource) definitionsSrc).getFile();
099        definitionsDir.mkdirs();
100        
101        String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName;
102        Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath);
103        File definitionFile = ((FileSource) definitionSrc).getFile();
104
105        if (definitionFile.exists())
106        {
107            getLogger().error("A definition file already exists with name '{}'", definitionFileName);
108            return Map.of(
109                    "success", false,
110                    "error", "already-exists");
111        }
112        
113        try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath())))
114        {
115            // Create the description content
116            String extractionName = _getExtractionNameFromFileName(definitionFileName);
117            I18nizableText descriptionTitle = new I18nizableText(ExtractionConstants.PLUGIN_NAME, ExtractionConstants.DESCRIPTION_DEFAULT_TITLE_KEY, Collections.singletonList(extractionName));
118            
119            Map<String, Object> contentInfos = _contentWorkflowHelper.createContent(
120                    ExtractionConstants.DESCRIPTION_CONTENT_WORKFLOW_NAME,
121                    ExtractionConstants.DESCRIPTION_CONTENT_INITIAL_ACTION_ID,
122                    extractionName,
123                    _i18nUtils.translate(descriptionTitle, language),
124                    new String[] {ExtractionConstants.DESCRIPTION_CONTENT_TYPE_ID},
125                    new String[0],
126                    language
127                );
128            String descriptionId = (String) contentInfos.get("contentId");
129            
130            // Create a transformer for saving sax into a file
131            TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
132            
133            StreamResult result = new StreamResult(os);
134            handler.setResult(result);
135
136            // create the format of result
137            Properties format = new Properties();
138            format.put(OutputKeys.METHOD, "xml");
139            format.put(OutputKeys.INDENT, "yes");
140            format.put(OutputKeys.ENCODING, "UTF-8");
141            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
142            handler.getTransformer().setOutputProperties(format);
143            handler.startDocument();
144
145            // sax skeleton
146            XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG);
147            
148            AttributesImpl attributes = new AttributesImpl();
149            attributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
150            XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, attributes);
151            
152            XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG);
153            
154            handler.endDocument();
155        
156            return Map.of(
157                    "success", true, 
158                    "definitionFilePath", definitionFilePath,
159                    "descriptionId", descriptionId);
160        }
161        catch (Exception e)
162        {
163            getLogger().error("Error when trying to create the extraction definition file '{}'", definitionFileName, e);
164            return Map.of(
165                    "success", false,
166                    "error", "other-error");
167        }
168    }
169    
170    private String _getExtractionNameFromFileName(String fileName)
171    {
172        return fileName.contains(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName;
173    }
174    
175    /**
176     * Adds a description to an extraction.
177     * @param definitionFileName The extraction definition file name
178     * @param descriptionId the identifier of the description 
179     * @return <code>true</code> if extraction saving succeed, <code>false</code> otherwise
180     * @throws IllegalArgumentException if the definition file doesn't exist
181     * @throws Exception if an error occurs
182     */
183    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
184    public Map<String, Object> addDescription(String definitionFileName, String descriptionId) throws IllegalArgumentException, Exception
185    {
186        String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName;
187        Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath);
188        File definitionFile = ((FileSource) definitionSrc).getFile();
189        
190        if (!definitionFile.exists())
191        {
192            getLogger().error("Error while adding a description to the extraction '{}': this definition file doesn't exist.", definitionFileName);
193            return Map.of(
194                    "success", false,
195                    "error", "unexisting");
196        }
197        
198        String tmpFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName + ".tmp";
199        Source tmpSrc = _sourceResolver.resolveURI(tmpFilePath);
200        File tmpFile = ((FileSource) tmpSrc).getFile();
201        
202        try (OutputStream os = Files.newOutputStream(Paths.get(tmpFile.getAbsolutePath())))
203        {
204            _copyDefinitionFileAndInsertDescription(definitionFile, os, descriptionId);
205            
206            Files.copy(tmpFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
207
208            return Map.of("success", true);
209        }
210        catch (Exception e)
211        {
212            getLogger().error("Error when trying to add a description to the extraction '{}'", definitionFileName, e);
213            return Map.of(
214                    "success", false,
215                    "error", "other-error");
216        }
217        finally
218        {
219            // delete the temporary file to keep the original one
220            _deleteTemporaryFile(definitionFileName, tmpFile.toPath());
221        }
222    }
223
224    private void _copyDefinitionFileAndInsertDescription(File definitionFile, OutputStream os, String descriptionId) throws Exception
225    {
226        // Parse existing definition file
227        Document document = _parseDefinitionFile(definitionFile);
228        Element extractionRoot = document.getDocumentElement();
229        
230        // Delete the current description node if exists
231        _getDescriptionsElements(document.getDocumentElement())
232            .forEach(extractionRoot::removeChild);
233
234        // Insert the description in the extraction root node
235        Element description = document.createElement(ExtractionConstants.DESCRIPTION_TAG);
236        description.setAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
237        extractionRoot.insertBefore(description, extractionRoot.getFirstChild());
238
239        // Write the updated definition file
240        DOMSource source = new DOMSource(document);
241        Transformer transformer = TransformerFactory.newInstance().newTransformer();
242        StreamResult result = new StreamResult(os);
243        transformer.transform(source, result);
244    }
245
246    private void _deleteTemporaryFile(String definitionFileName, Path temporaryFilePath)
247    {
248        try
249        {
250            Files.deleteIfExists(temporaryFilePath);
251        }
252        catch (IOException e)
253        {
254            getLogger().error("Error when deleting the temporary file for '{}'", definitionFileName, e);
255        }
256    }
257
258    /**
259     * Renames an extraction definition file.
260     * @param definitionOldFileName The extraction definition old file name
261     * @param definitionNewFileName The extraction definition new file name
262     * @return Map containing success boolean and error codes if one occurs
263     * @throws Exception if an error occurs
264     */
265    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
266    public Map<String, Object> renameExtraction(String definitionOldFileName, String definitionNewFileName) throws Exception
267    {
268        String oldFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionOldFileName;
269        Source oldSrc = _sourceResolver.resolveURI(oldFilePath);
270        File oldFile = ((FileSource) oldSrc).getFile();
271        
272        if (!oldFile.exists())
273        {
274            getLogger().error("Error while renaming '{}': this definition file doesn't exist.", definitionOldFileName);
275            return Map.of(
276                    "success", false,
277                    "error", "unexisting");
278        }
279        
280        String newFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionNewFileName;
281        Source newSrc = _sourceResolver.resolveURI(newFilePath);
282        File newFile = ((FileSource) newSrc).getFile();
283        
284        if (newFile.exists())
285        {
286            getLogger().error("Error while renaming to '{}': a definition file with this name already exists.", definitionNewFileName);
287            return Map.of(
288                    "success", false,
289                    "error", "already-exists");
290        }
291
292        try
293        {
294            // Copy old file in the new one
295            Files.copy(oldFile.toPath(), newFile.toPath());
296        }
297        catch (IOException e)
298        {
299            getLogger().error("Error while copying old definition file '{}' in the new one.", definitionOldFileName, e);
300            return Map.of(
301                    "success", false,
302                    "error", "other-error");
303        }
304        
305        try
306        {
307            Files.deleteIfExists(oldFile.toPath());
308        }
309        catch (IOException e)
310        {
311            getLogger().error("Error while deleting old definition file '{}'", definitionOldFileName, e);
312            return Map.of(
313                    "success", false,
314                    "error", "other-error");
315        }
316        
317        return Map.of("success", true);
318    }
319    
320    /**
321     * Deletes an extraction definition file.
322     * @param definitionFileName The extraction definition file to delete
323     * @return <code>true</code> if extraction deletion succeed, <code>false</code> otherwise
324     * @throws Exception if an error occurs
325     */
326    @Callable (right = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
327    public boolean deleteExtraction(String definitionFileName) throws Exception
328    {
329        String filePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName;
330        Source source = _sourceResolver.resolveURI(filePath);
331        File file = ((FileSource) source).getFile();
332        
333        if (!file.exists())
334        {
335            throw new IllegalArgumentException("Error while deleting '" + definitionFileName + "': this definition file doesn't exist.");
336        }
337        
338        try
339        {
340            // Get description identifiers
341            Document document = _parseDefinitionFile(file);
342            List<String> descriptionIds = _getDescriptionsElements(document.getDocumentElement())
343                .map(description -> description.getAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME))
344                .collect(Collectors.toList());
345            
346            // Delete descriptions contents
347            _contentDAO.deleteContents(descriptionIds, ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID);
348            
349            Files.deleteIfExists(file.toPath());
350        }
351        catch (IOException e)
352        {
353            if (getLogger().isErrorEnabled())
354            {
355                getLogger().error("Error while deleting definition file '" + definitionFileName + "'.", e);
356            }
357            throw e;
358        }
359        
360        return true;
361    }
362    
363    private Document _parseDefinitionFile(File definitionFile) throws Exception
364    {
365        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
366        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
367        return documentBuilder.parse(definitionFile);
368    }
369    
370    private Stream<Element> _getDescriptionsElements(Element extractionRoot)
371    {
372        NodeList descriptions = extractionRoot.getElementsByTagName(ExtractionConstants.DESCRIPTION_TAG);
373        return IntStream.range(0, descriptions.getLength())
374                .mapToObj(descriptions::item)
375                .map(Element.class::cast);
376    }
377}