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