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