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 definition 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("type", (String) variable.get("type"));
258                
259                XMLUtils.startElement(handler, "variable", attributes);
260
261                List<String> contentTypeIds = (List<String>) variable.get("contentTypeIds");
262                if (contentTypeIds != null && !contentTypeIds.isEmpty())
263                {
264                    XMLUtils.startElement(handler, "content-types");
265                    
266                    for (String contentTypeId : contentTypeIds)
267                    {
268                        AttributesImpl contentTypeAttributes = new AttributesImpl();
269                        contentTypeAttributes.addCDATAAttribute("id", contentTypeId);
270                        XMLUtils.createElement(handler, "content-type", contentTypeAttributes);
271                    }
272                    
273                    XMLUtils.endElement(handler, "content-types");
274                }
275                
276                String solrRequest = (String) variable.get("solrRequest");
277                if (StringUtils.isNotEmpty(solrRequest))
278                {
279                    XMLUtils.createElement(handler, "solr-request", solrRequest);
280                }
281                
282                XMLUtils.endElement(handler, "variable");
283            }
284            XMLUtils.endElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG);
285        }
286    }
287
288    @SuppressWarnings("unchecked")
289    private void _saxOptionalColumns(Map<String, Object> child, TransformerHandler handler) throws SAXException
290    {
291        Map<String, Object> data = (Map<String, Object>) child.get("data");
292        
293        List<String> names = (List<String>) data.get("names");
294        if (null != names && !names.isEmpty())
295        {
296            XMLUtils.startElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
297            for (String name : names)
298            {
299                XMLUtils.startElement(handler, "name");
300                XMLUtils.data(handler, name.trim());
301                XMLUtils.endElement(handler, "name");
302            }
303            XMLUtils.endElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
304        }
305    }
306
307    @SuppressWarnings("unchecked")
308    private void _saxExtractionComponent(Map<String, Object> component, TransformerHandler handler) throws SAXException
309    {
310        Map<String, Object> data = (Map<String, Object>) component.get("data");
311        
312        String tag = (String) component.get("tag");
313        AttributesImpl attributes = _getComponentAttibutes(data);
314        XMLUtils.startElement(handler, tag, attributes);
315        
316        // sax component's elements
317        _saxExtractionComponentClauses(data, handler);
318        _saxExtractionComponentGroupingFields(data, handler);
319        _saxExtractionComponentColumns(data, handler);
320        _saxExtractionComponentSorts(data, handler);
321        
322        // process children
323        if (component.get("children") != null)
324        {
325            List<Map<String, Object>> children = (List<Map<String, Object>>) component.get("children");
326            for (Map<String, Object> child : children)
327            {
328                _saxExtractionComponent(child, handler);
329            }
330        }
331        
332        XMLUtils.endElement(handler, tag);
333    }
334
335    @SuppressWarnings("unchecked")
336    private AttributesImpl _getComponentAttibutes(Map<String, Object> data)
337    {
338        AttributesImpl attributes = new AttributesImpl();
339        
340        Object componentTagName = data.get("componentTagName");
341        if (null != componentTagName && !StringUtils.isEmpty((String) componentTagName))
342        {
343            attributes.addCDATAAttribute("tagName", (String) componentTagName);
344        }
345        
346        List<String> contentTypes = (List<String>) data.get("contentTypes");
347        if (null != contentTypes && !contentTypes.isEmpty())
348        {
349            attributes.addCDATAAttribute("contentTypes", String.join(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER, contentTypes));
350        }
351        
352        Object queryReferenceId = data.get("queryReferenceId");
353        if (null != queryReferenceId && !StringUtils.isEmpty((String) queryReferenceId))
354        {
355            attributes.addCDATAAttribute("ref", (String) queryReferenceId);
356        }
357        
358        Object microThesaurusId = data.get("microThesaurusId");
359        if (null != microThesaurusId && !StringUtils.isEmpty((String) microThesaurusId))
360        {
361            attributes.addCDATAAttribute("microThesaurus", (String) microThesaurusId);
362        }
363        
364        Object maxLevel = data.get("maxLevel");
365        if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel)))
366        {
367            attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel));
368        }
369        
370        return attributes;
371    }
372    
373    @SuppressWarnings("unchecked")
374    private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException
375    {
376        List<String> clauses = (List<String>) data.get("clauses");
377        if (null != clauses && !clauses.isEmpty())
378        {
379            XMLUtils.startElement(handler, "clauses");
380            for (String clause : clauses)
381            {
382                XMLUtils.startElement(handler, "clause");
383                XMLUtils.data(handler, clause);
384                XMLUtils.endElement(handler, "clause");
385            }
386            XMLUtils.endElement(handler, "clauses");
387        }
388    }
389
390    private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException
391    {
392        Object groupingFields = data.get("groupingFields");
393        if (null == groupingFields || StringUtils.isEmpty((String) groupingFields))
394        {
395            return;
396        }
397        XMLUtils.startElement(handler, "grouping-fields");
398        XMLUtils.data(handler, (String) groupingFields);
399        XMLUtils.endElement(handler, "grouping-fields");
400    }
401
402    private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException
403    {
404        Object columnsObj = data.get("columns");
405        if (null == columnsObj || StringUtils.isEmpty((String) columnsObj))
406        {
407            return;
408        }
409        
410        Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj);
411        
412        if (!columns.isEmpty())
413        {
414            AttributesImpl columnsAttributes = new AttributesImpl();
415            Object overrideColumns = data.get("overrideColumns");
416            if (null != overrideColumns && (Boolean) overrideColumns)
417            {
418                columnsAttributes.addCDATAAttribute("override", "true");
419            }
420            XMLUtils.startElement(handler, "columns", columnsAttributes);
421            
422            for (Map.Entry<String, String> column : columns.entrySet())
423            {
424                AttributesImpl columnAttributes = new AttributesImpl();
425                if (column.getValue() != null)
426                {
427                    columnAttributes.addCDATAAttribute("optional", column.getValue());
428                }
429                XMLUtils.startElement(handler, "column", columnAttributes);
430                XMLUtils.data(handler, column.getKey());
431                XMLUtils.endElement(handler, "column");
432            }
433            
434            XMLUtils.endElement(handler, "columns");
435        }
436    }
437
438    private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException
439    {
440        Object sortsObj = data.get("sorts");
441        if (null == sortsObj || StringUtils.isEmpty((String) sortsObj))
442        {
443            return;
444        }
445        
446        Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj);
447        
448        if (!sorts.isEmpty())
449        {
450            AttributesImpl sortsAttributes = new AttributesImpl();
451            Object overrideSorts = data.get("overrideSorts");
452            if (null != overrideSorts && (Boolean) overrideSorts)
453            {
454                sortsAttributes.addCDATAAttribute("override", "true");
455            }
456            XMLUtils.startElement(handler, "sorts", sortsAttributes);
457            
458            for (Map.Entry<String, String> sort : sorts.entrySet())
459            {
460                AttributesImpl sortAttributes = new AttributesImpl();
461                if (sort.getValue() != null)
462                {
463                    sortAttributes.addCDATAAttribute("order", sort.getValue());
464                }
465                XMLUtils.startElement(handler, "sort", sortAttributes);
466                XMLUtils.data(handler, sort.getKey());
467                XMLUtils.endElement(handler, "sort");
468            }
469            
470            XMLUtils.endElement(handler, "sorts");
471        }
472    }
473    
474    private Map<String, String> _splitDataAndExtradataFromString(String str)
475    {
476        Map<String, String> result = new LinkedHashMap<>();
477        
478        for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER))
479        {
480            Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX);
481            Matcher matcher = pattern.matcher(data);
482            
483            String extra = null;
484            if (matcher.find())
485            {
486                extra = matcher.group(1);
487            }
488            
489            String finalData = data;
490            if (null != extra)
491            {
492                extra = extra.trim();
493                int indexOfExtra = data.indexOf("(");
494                finalData = data.substring(0, indexOfExtra);
495            }
496            
497            result.put(finalData.trim(), extra);
498        }
499        
500        return result;
501    }
502    
503}