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.Paths;
023import java.nio.file.StandardCopyOption;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import javax.xml.transform.OutputKeys;
032import javax.xml.transform.TransformerConfigurationException;
033import javax.xml.transform.TransformerFactory;
034import javax.xml.transform.TransformerFactoryConfigurationError;
035import javax.xml.transform.sax.SAXTransformerFactory;
036import javax.xml.transform.sax.TransformerHandler;
037import javax.xml.transform.stream.StreamResult;
038
039import org.apache.avalon.framework.component.Component;
040import org.apache.avalon.framework.logger.AbstractLogEnabled;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.cocoon.xml.AttributesImpl;
045import org.apache.cocoon.xml.XMLUtils;
046import org.apache.commons.collections4.MapUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.excalibur.source.Source;
049import org.apache.excalibur.source.SourceResolver;
050import org.apache.excalibur.source.impl.FileSource;
051import org.apache.xml.serializer.OutputPropertiesFactory;
052import org.xml.sax.SAXException;
053
054import org.ametys.core.user.UserIdentity;
055import org.ametys.plugins.core.user.UserHelper;
056import org.ametys.plugins.extraction.ExtractionConstants;
057import org.ametys.plugins.extraction.execution.Extraction;
058import org.ametys.plugins.extraction.execution.ExtractionDefinitionReader;
059
060/**
061 * Helper that manages the button that saves extraction's modifications
062 */
063public class SaveExtractionHelper extends AbstractLogEnabled implements Component, Serviceable
064{
065    /** The Avalon role name */
066    public static final String ROLE = SaveExtractionHelper.class.getName();
067    
068    private static final String EXTRACT_EXTRA_DATA_REGEX = "\\(([-_a-zA-Z]+)\\)";
069    
070    private SourceResolver _sourceResolver;
071    private ExtractionDefinitionReader _definitionReader;
072    private UserHelper _userHelper;
073    
074    public void service(ServiceManager serviceManager) throws ServiceException
075    {
076        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
077        _definitionReader = (ExtractionDefinitionReader) serviceManager.lookup(ExtractionDefinitionReader.ROLE);
078        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
079    }
080    
081    /**
082     * Saves modifications on extraction. Creates the definition file if it doesn't exist
083     * @param relativeDefinitionFilePath The extraction definition file path
084     * @param extractionComponents A <code>Map</code> containing definition informations
085     * @return <code>true</code> if extraction saving succeed, <code>false</code> otherwise
086     * @throws Exception if an error occurs
087     */
088    public boolean saveExtraction(String relativeDefinitionFilePath, Map<String, Object> extractionComponents) throws Exception
089    {
090        boolean errorOccurred = false;
091        
092        String backupFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath + ".tmp";
093        Source backupSrc = _sourceResolver.resolveURI(backupFilePath);
094        File backupFile = ((FileSource) backupSrc).getFile();
095        
096        String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath;
097        Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath);
098        File definitionFile = ((FileSource) definitionSrc).getFile();
099        
100        if (!definitionFile.exists())
101        {
102            throw new IllegalArgumentException("The file " + relativeDefinitionFilePath + " does not exist.");
103        }
104        
105        Extraction extraction = _definitionReader.readExtractionDefinitionFile(definitionFile);
106
107        // Create a backup file
108        try
109        {
110            Files.copy(definitionFile.toPath(), backupFile.toPath());
111        }
112        catch (IOException e)
113        {
114            if (getLogger().isErrorEnabled())
115            {
116                getLogger().error("Error when creating backup '" + definitionFilePath + "' file", e);
117            }
118        }
119        
120        try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath())))
121        {
122            // Create a transformer for saving sax into a file
123            TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
124            
125            StreamResult result = new StreamResult(os);
126            handler.setResult(result);
127
128            // create the format of result
129            Properties format = new Properties();
130            format.put(OutputKeys.METHOD, "xml");
131            format.put(OutputKeys.INDENT, "yes");
132            format.put(OutputKeys.ENCODING, "UTF-8");
133            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
134            handler.getTransformer().setOutputProperties(format);
135
136            // sax the config
137            try
138            {
139                _saxExtraction(extraction, extractionComponents, handler);
140            }
141            catch (Exception e)
142            {
143                if (getLogger().isErrorEnabled())
144                {
145                    getLogger().error("Error when saxing the extraction defintion file '" + definitionFilePath + "'", e);
146                }
147                errorOccurred = true;
148            }
149        }
150        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
151        {
152            if (getLogger().isErrorEnabled())
153            {
154                getLogger().error("Error when trying to modify the extraction definition file '" + definitionFilePath + "'", e);
155            }
156        }
157        
158        try
159        {
160            // Restore the file if an error previously occurred and delete backup
161            if (errorOccurred)
162            {
163                Files.copy(backupFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
164            }
165            Files.deleteIfExists(backupFile.toPath());
166        }
167        catch (IOException e)
168        {
169            if (getLogger().isErrorEnabled())
170            {
171                getLogger().error("Error when restoring backup '" + definitionFilePath + "' file", e);
172            }
173        }
174        
175        return !errorOccurred;
176    }
177
178    @SuppressWarnings("unchecked")
179    private void _saxExtraction(Extraction extraction, Map<String, Object> extractionComponents, TransformerHandler handler) throws SAXException
180    {
181        handler.startDocument();
182
183        // Set extraction's name attribute
184        Map<String, Object> extractionData = (Map<String, Object>) extractionComponents.get("data");
185        AttributesImpl attributes = new AttributesImpl();
186        if (MapUtils.isNotEmpty(extractionData))
187        {
188            attributes.addCDATAAttribute("name", (String) extractionData.get("name"));
189        }
190        XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG, attributes);
191        
192        // Set extraction's description
193        String descriptionId = extraction.getDescriptionId();
194        if (StringUtils.isNotEmpty(descriptionId))
195        {
196            AttributesImpl descriptionAttributes = new AttributesImpl();
197            descriptionAttributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
198            XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, descriptionAttributes);
199        }
200        
201        // Set extraction's author
202        _saxAuthor(extraction, handler);
203
204        List<Map<String, Object>> children = (List<Map<String, Object>>) extractionComponents.get("children");
205        if (null != children)
206        {
207            for (Map<String, Object> child : children)
208            {
209                String tag = (String) child.get("tag");
210                switch (tag)
211                {
212                    case ExtractionConstants.CLAUSES_VARIABLES_TAG:
213                        _saxClausesVariables(child, handler);
214                        break;
215                    case ExtractionConstants.OPTIONAL_COLUMNS_TAG:
216                        _saxOptionalColumns(child, handler);
217                        break;
218                    case ExtractionConstants.QUERY_COMPONENT_TAG:
219                    case ExtractionConstants.THESAURUS_COMPONENT_TAG:
220                    case ExtractionConstants.COUNT_COMPONENT_TAG:
221                    case ExtractionConstants.MAPPING_QUERY_COMPONENT_TAG:
222                        _saxExtractionComponent(child, handler);
223                        break;
224                    default:
225                        break;
226                }
227            }
228        }
229        
230        XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG);
231        handler.endDocument();
232    }
233    
234    private void _saxAuthor(Extraction extraction, TransformerHandler handler) throws SAXException
235    {
236        // Author
237        UserIdentity author = extraction.getAuthor();
238        if (author != null)
239        {
240            _userHelper.saxUserIdentity(author, handler, ExtractionConstants.AUTHOR_TAG);
241        }
242    }
243
244    @SuppressWarnings("unchecked")
245    private void _saxClausesVariables(Map<String, Object> child, TransformerHandler handler) throws SAXException
246    {
247        Map<String, Object> data = (Map<String, Object>) child.get("data");
248        
249        List<Map<String, Object>> variables = (List<Map<String, Object>>) data.get("variables");
250        if (!variables.isEmpty())
251        {
252            XMLUtils.startElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG);
253            for (Map<String, Object> variable : variables)
254            {
255                AttributesImpl attributes = new AttributesImpl();
256                attributes.addCDATAAttribute("name", (String) variable.get("name"));
257                attributes.addCDATAAttribute("contentType", (String) variable.get("contentType"));
258                XMLUtils.createElement(handler, "variable", attributes);
259            }
260            XMLUtils.endElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG);
261        }
262    }
263
264    @SuppressWarnings("unchecked")
265    private void _saxOptionalColumns(Map<String, Object> child, TransformerHandler handler) throws SAXException
266    {
267        Map<String, Object> data = (Map<String, Object>) child.get("data");
268        
269        List<String> names = (List<String>) data.get("names");
270        if (null != names && !names.isEmpty())
271        {
272            XMLUtils.startElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
273            for (String name : names)
274            {
275                XMLUtils.startElement(handler, "name");
276                XMLUtils.data(handler, name.trim());
277                XMLUtils.endElement(handler, "name");
278            }
279            XMLUtils.endElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
280        }
281    }
282
283    @SuppressWarnings("unchecked")
284    private void _saxExtractionComponent(Map<String, Object> component, TransformerHandler handler) throws SAXException
285    {
286        Map<String, Object> data = (Map<String, Object>) component.get("data");
287        
288        String tag = (String) component.get("tag");
289        AttributesImpl attributes = _getComponentAttibutes(data);
290        XMLUtils.startElement(handler, tag, attributes);
291        
292        // sax component's elements
293        _saxExtractionComponentClauses(data, handler);
294        _saxExtractionComponentGroupingFields(data, handler);
295        _saxExtractionComponentColumns(data, handler);
296        _saxExtractionComponentSorts(data, handler);
297        
298        // process children
299        if (component.get("children") != null)
300        {
301            List<Map<String, Object>> children = (List<Map<String, Object>>) component.get("children");
302            for (Map<String, Object> child : children)
303            {
304                _saxExtractionComponent(child, handler);
305            }
306        }
307        
308        XMLUtils.endElement(handler, tag);
309    }
310
311    @SuppressWarnings("unchecked")
312    private AttributesImpl _getComponentAttibutes(Map<String, Object> data)
313    {
314        AttributesImpl attributes = new AttributesImpl();
315        
316        Object componentTagName = data.get("componentTagName");
317        if (null != componentTagName && !StringUtils.isEmpty((String) componentTagName))
318        {
319            attributes.addCDATAAttribute("tagName", (String) componentTagName);
320        }
321        
322        List<String> contentTypes = (List<String>) data.get("contentTypes");
323        if (null != contentTypes && !contentTypes.isEmpty())
324        {
325            attributes.addCDATAAttribute("contentTypes", String.join(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER, contentTypes));
326        }
327        
328        Object queryReferenceId = data.get("queryReferenceId");
329        if (null != queryReferenceId && !StringUtils.isEmpty((String) queryReferenceId))
330        {
331            attributes.addCDATAAttribute("ref", (String) queryReferenceId);
332        }
333        
334        Object microThesaurusId = data.get("microThesaurusId");
335        if (null != microThesaurusId && !StringUtils.isEmpty((String) microThesaurusId))
336        {
337            attributes.addCDATAAttribute("microThesaurus", (String) microThesaurusId);
338        }
339        
340        Object maxLevel = data.get("maxLevel");
341        if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel)))
342        {
343            attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel));
344        }
345        
346        return attributes;
347    }
348    
349    @SuppressWarnings("unchecked")
350    private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException
351    {
352        List<String> clauses = (List<String>) data.get("clauses");
353        if (null != clauses && !clauses.isEmpty())
354        {
355            XMLUtils.startElement(handler, "clauses");
356            for (String clause : clauses)
357            {
358                XMLUtils.startElement(handler, "clause");
359                XMLUtils.data(handler, clause);
360                XMLUtils.endElement(handler, "clause");
361            }
362            XMLUtils.endElement(handler, "clauses");
363        }
364    }
365
366    private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException
367    {
368        Object groupingFields = data.get("groupingFields");
369        if (null == groupingFields || StringUtils.isEmpty((String) groupingFields))
370        {
371            return;
372        }
373        XMLUtils.startElement(handler, "grouping-fields");
374        XMLUtils.data(handler, (String) groupingFields);
375        XMLUtils.endElement(handler, "grouping-fields");
376    }
377
378    private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException
379    {
380        Object columnsObj = data.get("columns");
381        if (null == columnsObj || StringUtils.isEmpty((String) columnsObj))
382        {
383            return;
384        }
385        
386        Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj);
387        
388        if (!columns.isEmpty())
389        {
390            AttributesImpl columnsAttributes = new AttributesImpl();
391            Object overrideColumns = data.get("overrideColumns");
392            if (null != overrideColumns && (Boolean) overrideColumns)
393            {
394                columnsAttributes.addCDATAAttribute("override", "true");
395            }
396            XMLUtils.startElement(handler, "columns", columnsAttributes);
397            
398            for (Map.Entry<String, String> column : columns.entrySet())
399            {
400                AttributesImpl columnAttributes = new AttributesImpl();
401                if (column.getValue() != null)
402                {
403                    columnAttributes.addCDATAAttribute("optional", column.getValue());
404                }
405                XMLUtils.startElement(handler, "column", columnAttributes);
406                XMLUtils.data(handler, column.getKey());
407                XMLUtils.endElement(handler, "column");
408            }
409            
410            XMLUtils.endElement(handler, "columns");
411        }
412    }
413
414    private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException
415    {
416        Object sortsObj = data.get("sorts");
417        if (null == sortsObj || StringUtils.isEmpty((String) sortsObj))
418        {
419            return;
420        }
421        
422        Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj);
423        
424        if (!sorts.isEmpty())
425        {
426            AttributesImpl sortsAttributes = new AttributesImpl();
427            Object overrideSorts = data.get("overrideSorts");
428            if (null != overrideSorts && (Boolean) overrideSorts)
429            {
430                sortsAttributes.addCDATAAttribute("override", "true");
431            }
432            XMLUtils.startElement(handler, "sorts", sortsAttributes);
433            
434            for (Map.Entry<String, String> sort : sorts.entrySet())
435            {
436                AttributesImpl sortAttributes = new AttributesImpl();
437                if (sort.getValue() != null)
438                {
439                    sortAttributes.addCDATAAttribute("order", sort.getValue());
440                }
441                XMLUtils.startElement(handler, "sort", sortAttributes);
442                XMLUtils.data(handler, sort.getKey());
443                XMLUtils.endElement(handler, "sort");
444            }
445            
446            XMLUtils.endElement(handler, "sorts");
447        }
448    }
449    
450    private Map<String, String> _splitDataAndExtradataFromString(String str)
451    {
452        Map<String, String> result = new LinkedHashMap<>();
453        
454        for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER))
455        {
456            Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX);
457            Matcher matcher = pattern.matcher(data);
458            
459            String extra = null;
460            if (matcher.find())
461            {
462                extra = matcher.group(1);
463            }
464            
465            String finalData = data;
466            if (null != extra)
467            {
468                extra = extra.trim();
469                int indexOfExtra = data.indexOf("(");
470                finalData = data.substring(0, indexOfExtra);
471            }
472            
473            result.put(finalData.trim(), extra);
474        }
475        
476        return result;
477    }
478    
479}