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