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_DELEMITER, 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 thesaurusName = data.get("thesaurusName");
302        if (null != thesaurusName && !StringUtils.isEmpty((String) thesaurusName))
303        {
304            attributes.addCDATAAttribute("thesaurusName", (String) thesaurusName);
305        }
306        
307        Object microThesaurusName = data.get("microThesaurusName");
308        if (null != microThesaurusName && !StringUtils.isEmpty((String) microThesaurusName))
309        {
310            attributes.addCDATAAttribute("microThesaurusName", (String) microThesaurusName);
311        }
312        
313        Object maxLevel = data.get("maxLevel");
314        if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel)))
315        {
316            attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel));
317        }
318        
319        return attributes;
320    }
321    
322    @SuppressWarnings("unchecked")
323    private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException
324    {
325        List<String> clauses = (List<String>) data.get("clauses");
326        if (null != clauses && !clauses.isEmpty())
327        {
328            XMLUtils.startElement(handler, "clauses");
329            for (String clause : clauses)
330            {
331                XMLUtils.startElement(handler, "clause");
332                XMLUtils.data(handler, clause);
333                XMLUtils.endElement(handler, "clause");
334            }
335            XMLUtils.endElement(handler, "clauses");
336        }
337    }
338
339    private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException
340    {
341        Object groupingFields = data.get("groupingFields");
342        if (null == groupingFields || StringUtils.isEmpty((String) groupingFields))
343        {
344            return;
345        }
346        XMLUtils.startElement(handler, "grouping-fields");
347        XMLUtils.data(handler, (String) groupingFields);
348        XMLUtils.endElement(handler, "grouping-fields");
349    }
350
351    private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException
352    {
353        Object columnsObj = data.get("columns");
354        if (null == columnsObj || StringUtils.isEmpty((String) columnsObj))
355        {
356            return;
357        }
358        
359        Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj);
360        
361        if (!columns.isEmpty())
362        {
363            AttributesImpl columnsAttributes = new AttributesImpl();
364            Object overrideColumns = data.get("overrideColumns");
365            if (null != overrideColumns && (Boolean) overrideColumns)
366            {
367                columnsAttributes.addCDATAAttribute("override", "true");
368            }
369            XMLUtils.startElement(handler, "columns", columnsAttributes);
370            
371            for (Map.Entry<String, String> column : columns.entrySet())
372            {
373                AttributesImpl columnAttributes = new AttributesImpl();
374                if (column.getValue() != null)
375                {
376                    columnAttributes.addCDATAAttribute("optional", column.getValue());
377                }
378                XMLUtils.startElement(handler, "column", columnAttributes);
379                XMLUtils.data(handler, column.getKey());
380                XMLUtils.endElement(handler, "column");
381            }
382            
383            XMLUtils.endElement(handler, "columns");
384        }
385    }
386
387    private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException
388    {
389        Object sortsObj = data.get("sorts");
390        if (null == sortsObj || StringUtils.isEmpty((String) sortsObj))
391        {
392            return;
393        }
394        
395        Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj);
396        
397        if (!sorts.isEmpty())
398        {
399            AttributesImpl sortsAttributes = new AttributesImpl();
400            Object overrideSorts = data.get("overrideSorts");
401            if (null != overrideSorts && (Boolean) overrideSorts)
402            {
403                sortsAttributes.addCDATAAttribute("override", "true");
404            }
405            XMLUtils.startElement(handler, "sorts", sortsAttributes);
406            
407            for (Map.Entry<String, String> sort : sorts.entrySet())
408            {
409                AttributesImpl sortAttributes = new AttributesImpl();
410                if (sort.getValue() != null)
411                {
412                    sortAttributes.addCDATAAttribute("order", sort.getValue());
413                }
414                XMLUtils.startElement(handler, "sort", sortAttributes);
415                XMLUtils.data(handler, sort.getKey());
416                XMLUtils.endElement(handler, "sort");
417            }
418            
419            XMLUtils.endElement(handler, "sorts");
420        }
421    }
422    
423    private Map<String, String> _splitDataAndExtradataFromString(String str)
424    {
425        Map<String, String> result = new LinkedHashMap<>();
426        
427        for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELEMITER))
428        {
429            Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX);
430            Matcher matcher = pattern.matcher(data);
431            
432            String extra = null;
433            if (matcher.find())
434            {
435                extra = matcher.group(1);
436            }
437            
438            String finalData = data;
439            if (null != extra)
440            {
441                extra = extra.trim();
442                int indexOfExtra = data.indexOf("(");
443                finalData = data.substring(0, indexOfExtra);
444            }
445            
446            result.put(finalData.trim(), extra);
447        }
448        
449        return result;
450    }
451    
452}