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.Set;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import javax.xml.transform.OutputKeys;
033import javax.xml.transform.TransformerConfigurationException;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.TransformerFactoryConfigurationError;
036import javax.xml.transform.sax.SAXTransformerFactory;
037import javax.xml.transform.sax.TransformerHandler;
038import javax.xml.transform.stream.StreamResult;
039
040import org.apache.avalon.framework.component.Component;
041import org.apache.avalon.framework.logger.AbstractLogEnabled;
042import org.apache.avalon.framework.service.ServiceException;
043import org.apache.avalon.framework.service.ServiceManager;
044import org.apache.avalon.framework.service.Serviceable;
045import org.apache.cocoon.xml.AttributesImpl;
046import org.apache.cocoon.xml.XMLUtils;
047import org.apache.commons.collections4.MapUtils;
048import org.apache.commons.lang3.StringUtils;
049import org.apache.excalibur.source.Source;
050import org.apache.excalibur.source.SourceResolver;
051import org.apache.excalibur.source.impl.FileSource;
052import org.apache.xml.serializer.OutputPropertiesFactory;
053import org.xml.sax.SAXException;
054
055import org.ametys.core.group.GroupIdentity;
056import org.ametys.core.user.UserIdentity;
057import org.ametys.plugins.core.user.UserHelper;
058import org.ametys.plugins.extraction.ExtractionConstants;
059import org.ametys.plugins.extraction.execution.Extraction;
060import org.ametys.plugins.extraction.execution.Extraction.ExtractionProfile;
061import org.ametys.plugins.extraction.execution.ExtractionDefinitionReader;
062
063/**
064 * Helper that manages the button that saves extraction's modifications
065 */
066public class SaveExtractionHelper extends AbstractLogEnabled implements Component, Serviceable
067{
068    /** The Avalon role name */
069    public static final String ROLE = SaveExtractionHelper.class.getName();
070    
071    private static final String EXTRACT_EXTRA_DATA_REGEX = "\\(([-_a-zA-Z]+)\\)";
072    
073    private SourceResolver _sourceResolver;
074    private ExtractionDefinitionReader _definitionReader;
075    private UserHelper _userHelper;
076    
077    public void service(ServiceManager serviceManager) throws ServiceException
078    {
079        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
080        _definitionReader = (ExtractionDefinitionReader) serviceManager.lookup(ExtractionDefinitionReader.ROLE);
081        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
082    }
083    
084    /**
085     * Saves modifications on extraction. Creates the definition file if it doesn't exist
086     * @param relativeDefinitionFilePath The extraction definition file path
087     * @param extractionComponents A <code>Map</code> containing definition informations
088     * @return <code>true</code> if extraction saving succeed, <code>false</code> otherwise
089     * @throws Exception if an error occurs
090     */
091    public boolean saveExtraction(String relativeDefinitionFilePath, Map<String, Object> extractionComponents) throws Exception
092    {
093        boolean errorOccurred = false;
094        
095        String backupFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath + ".tmp";
096        Source backupSrc = _sourceResolver.resolveURI(backupFilePath);
097        File backupFile = ((FileSource) backupSrc).getFile();
098        
099        String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath;
100        Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath);
101        File definitionFile = ((FileSource) definitionSrc).getFile();
102        
103        if (!definitionFile.exists())
104        {
105            throw new IllegalArgumentException("The file " + relativeDefinitionFilePath + " does not exist.");
106        }
107        
108        Extraction extraction = _definitionReader.readExtractionDefinitionFile(definitionFile);
109
110        // Create a backup file
111        try
112        {
113            Files.copy(definitionFile.toPath(), backupFile.toPath());
114        }
115        catch (IOException e)
116        {
117            if (getLogger().isErrorEnabled())
118            {
119                getLogger().error("Error when creating backup '" + definitionFilePath + "' file", e);
120            }
121        }
122        
123        try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath())))
124        {
125            // Create a transformer for saving sax into a file
126            TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
127            
128            StreamResult result = new StreamResult(os);
129            handler.setResult(result);
130
131            // create the format of result
132            Properties format = new Properties();
133            format.put(OutputKeys.METHOD, "xml");
134            format.put(OutputKeys.INDENT, "yes");
135            format.put(OutputKeys.ENCODING, "UTF-8");
136            format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
137            handler.getTransformer().setOutputProperties(format);
138
139            // sax the config
140            try
141            {
142                _saxExtraction(extraction, extractionComponents, handler);
143            }
144            catch (Exception e)
145            {
146                if (getLogger().isErrorEnabled())
147                {
148                    getLogger().error("Error when saxing the extraction defintion file '" + definitionFilePath + "'", e);
149                }
150                errorOccurred = true;
151            }
152        }
153        catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
154        {
155            if (getLogger().isErrorEnabled())
156            {
157                getLogger().error("Error when trying to modify the extraction definition file '" + definitionFilePath + "'", e);
158            }
159        }
160        
161        try
162        {
163            // Restore the file if an error previously occurred and delete backup
164            if (errorOccurred)
165            {
166                Files.copy(backupFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
167            }
168            Files.deleteIfExists(backupFile.toPath());
169        }
170        catch (IOException e)
171        {
172            if (getLogger().isErrorEnabled())
173            {
174                getLogger().error("Error when restoring backup '" + definitionFilePath + "' file", e);
175            }
176        }
177        
178        return !errorOccurred;
179    }
180
181    @SuppressWarnings("unchecked")
182    private void _saxExtraction(Extraction extraction, Map<String, Object> extractionComponents, TransformerHandler handler) throws SAXException
183    {
184        handler.startDocument();
185
186        // Set extraction's name attribute
187        Map<String, Object> extractionData = (Map<String, Object>) extractionComponents.get("data");
188        AttributesImpl attributes = new AttributesImpl();
189        if (MapUtils.isNotEmpty(extractionData))
190        {
191            attributes.addCDATAAttribute("name", (String) extractionData.get("name"));
192        }
193        XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG, attributes);
194        
195        // Set extraction's description
196        String descriptionId = extraction.getDescriptionId();
197        if (StringUtils.isNotEmpty(descriptionId))
198        {
199            AttributesImpl descriptionAttributes = new AttributesImpl();
200            descriptionAttributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId);
201            XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, descriptionAttributes);
202        }
203        
204        // Generate SAX events for extraction's rights
205        _saxRights(extraction, handler);
206
207        List<Map<String, Object>> children = (List<Map<String, Object>>) extractionComponents.get("children");
208        if (null != children)
209        {
210            for (Map<String, Object> child : children)
211            {
212                String tag = (String) child.get("tag");
213                switch (tag)
214                {
215                    case ExtractionConstants.CLAUSES_VARIABLES_TAG:
216                        _saxClausesVariables(child, handler);
217                        break;
218                    case ExtractionConstants.OPTIONAL_COLUMNS_TAG:
219                        _saxOptionalColumns(child, handler);
220                        break;
221                    case ExtractionConstants.QUERY_COMPONENT_TAG:
222                    case ExtractionConstants.THESAURUS_COMPONENT_TAG:
223                    case ExtractionConstants.COUNT_COMPONENT_TAG:
224                    case ExtractionConstants.MAPPING_QUERY_COMPONENT_TAG:
225                        _saxExtractionComponent(child, handler);
226                        break;
227                    default:
228                        break;
229                }
230            }
231        }
232        
233        XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG);
234        handler.endDocument();
235    }
236    
237    private void _saxRights(Extraction extraction, TransformerHandler handler) throws SAXException
238    {
239        // Visibility
240        XMLUtils.createElement(handler, ExtractionConstants.VISIBILITY_TAG, extraction.getVisibility().toString());
241        
242        // Author
243        UserIdentity author = extraction.getAuthor();
244        if (author != null)
245        {
246            _userHelper.saxUserIdentity(author, handler, ExtractionConstants.AUTHOR_TAG);
247        }
248        
249        // Read access
250        Set<GroupIdentity> readAccessGroups = extraction.getGrantedGroups(ExtractionProfile.READ_ACCESS);
251        Set<UserIdentity> readAccessUsers = extraction.getGrantedUsers(ExtractionProfile.READ_ACCESS);
252        if (!readAccessGroups.isEmpty() || !readAccessUsers.isEmpty())
253        {
254            XMLUtils.startElement(handler, ExtractionConstants.READ_ACCESS_TAG);
255            _saxGroups(readAccessGroups, handler);
256            _saxUsers(readAccessUsers, handler);
257            XMLUtils.endElement(handler, ExtractionConstants.READ_ACCESS_TAG);
258        }
259        
260        // Write access
261        Set<GroupIdentity> writeAccessGroups = extraction.getGrantedGroups(ExtractionProfile.WRITE_ACCESS);
262        Set<UserIdentity> writeAccessUsers = extraction.getGrantedUsers(ExtractionProfile.WRITE_ACCESS);
263        if (!writeAccessGroups.isEmpty() || !writeAccessUsers.isEmpty())
264        {
265            XMLUtils.startElement(handler, ExtractionConstants.WRITE_ACCESS_TAG);
266            _saxGroups(writeAccessGroups, handler);
267            _saxUsers(writeAccessUsers, handler);
268            XMLUtils.endElement(handler, ExtractionConstants.WRITE_ACCESS_TAG);
269        }
270    }
271
272    private void _saxGroups(Set<GroupIdentity> groups, TransformerHandler handler) throws SAXException
273    {
274        if (!groups.isEmpty())
275        {
276            XMLUtils.startElement(handler, ExtractionConstants.GROUPS_TAG);
277            
278            for (GroupIdentity group : groups)
279            {
280                AttributesImpl attributes = new AttributesImpl();
281                attributes.addCDATAAttribute("id", group.getId());
282                attributes.addCDATAAttribute("groupDirectory", group.getDirectoryId());
283                XMLUtils.createElement(handler, ExtractionConstants.GROUP_TAG, attributes);
284            }
285            
286            XMLUtils.endElement(handler, ExtractionConstants.GROUPS_TAG);
287        }
288    }
289    
290    private void _saxUsers(Set<UserIdentity> users, TransformerHandler handler) throws SAXException
291    {
292        if (!users.isEmpty())
293        {
294            XMLUtils.startElement(handler, ExtractionConstants.USERS_TAG);
295            
296            for (UserIdentity user : users)
297            {
298                AttributesImpl attributes = new AttributesImpl();
299                attributes.addCDATAAttribute("login", user.getLogin());
300                attributes.addCDATAAttribute("population", user.getPopulationId());
301                XMLUtils.createElement(handler, ExtractionConstants.USER_TAG, attributes);
302            }
303            
304            XMLUtils.endElement(handler, ExtractionConstants.USERS_TAG);
305        }
306    }
307    
308    @SuppressWarnings("unchecked")
309    private void _saxClausesVariables(Map<String, Object> child, TransformerHandler handler) throws SAXException
310    {
311        Map<String, Object> data = (Map<String, Object>) child.get("data");
312        
313        List<Map<String, Object>> variables = (List<Map<String, Object>>) data.get("variables");
314        if (!variables.isEmpty())
315        {
316            XMLUtils.startElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG);
317            for (Map<String, Object> variable : variables)
318            {
319                AttributesImpl attributes = new AttributesImpl();
320                attributes.addCDATAAttribute("name", (String) variable.get("name"));
321                attributes.addCDATAAttribute("contentType", (String) variable.get("contentType"));
322                XMLUtils.createElement(handler, "variable", attributes);
323            }
324            XMLUtils.endElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG);
325        }
326    }
327
328    @SuppressWarnings("unchecked")
329    private void _saxOptionalColumns(Map<String, Object> child, TransformerHandler handler) throws SAXException
330    {
331        Map<String, Object> data = (Map<String, Object>) child.get("data");
332        
333        List<String> names = (List<String>) data.get("names");
334        if (null != names && !names.isEmpty())
335        {
336            XMLUtils.startElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
337            for (String name : names)
338            {
339                XMLUtils.startElement(handler, "name");
340                XMLUtils.data(handler, name.trim());
341                XMLUtils.endElement(handler, "name");
342            }
343            XMLUtils.endElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG);
344        }
345    }
346
347    @SuppressWarnings("unchecked")
348    private void _saxExtractionComponent(Map<String, Object> component, TransformerHandler handler) throws SAXException
349    {
350        Map<String, Object> data = (Map<String, Object>) component.get("data");
351        
352        String tag = (String) component.get("tag");
353        AttributesImpl attributes = _getComponentAttibutes(data);
354        XMLUtils.startElement(handler, tag, attributes);
355        
356        // sax component's elements
357        _saxExtractionComponentClauses(data, handler);
358        _saxExtractionComponentGroupingFields(data, handler);
359        _saxExtractionComponentColumns(data, handler);
360        _saxExtractionComponentSorts(data, handler);
361        
362        // process children
363        if (component.get("children") != null)
364        {
365            List<Map<String, Object>> children = (List<Map<String, Object>>) component.get("children");
366            for (Map<String, Object> child : children)
367            {
368                _saxExtractionComponent(child, handler);
369            }
370        }
371        
372        XMLUtils.endElement(handler, tag);
373    }
374
375    @SuppressWarnings("unchecked")
376    private AttributesImpl _getComponentAttibutes(Map<String, Object> data)
377    {
378        AttributesImpl attributes = new AttributesImpl();
379        
380        Object componentTagName = data.get("componentTagName");
381        if (null != componentTagName && !StringUtils.isEmpty((String) componentTagName))
382        {
383            attributes.addCDATAAttribute("tagName", (String) componentTagName);
384        }
385        
386        List<String> contentTypes = (List<String>) data.get("contentTypes");
387        if (null != contentTypes && !contentTypes.isEmpty())
388        {
389            attributes.addCDATAAttribute("contentTypes", String.join(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER, contentTypes));
390        }
391        
392        Object queryReferenceId = data.get("queryReferenceId");
393        if (null != queryReferenceId && !StringUtils.isEmpty((String) queryReferenceId))
394        {
395            attributes.addCDATAAttribute("ref", (String) queryReferenceId);
396        }
397        
398        Object microThesaurusId = data.get("microThesaurusId");
399        if (null != microThesaurusId && !StringUtils.isEmpty((String) microThesaurusId))
400        {
401            attributes.addCDATAAttribute("microThesaurus", (String) microThesaurusId);
402        }
403        
404        Object maxLevel = data.get("maxLevel");
405        if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel)))
406        {
407            attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel));
408        }
409        
410        return attributes;
411    }
412    
413    @SuppressWarnings("unchecked")
414    private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException
415    {
416        List<String> clauses = (List<String>) data.get("clauses");
417        if (null != clauses && !clauses.isEmpty())
418        {
419            XMLUtils.startElement(handler, "clauses");
420            for (String clause : clauses)
421            {
422                XMLUtils.startElement(handler, "clause");
423                XMLUtils.data(handler, clause);
424                XMLUtils.endElement(handler, "clause");
425            }
426            XMLUtils.endElement(handler, "clauses");
427        }
428    }
429
430    private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException
431    {
432        Object groupingFields = data.get("groupingFields");
433        if (null == groupingFields || StringUtils.isEmpty((String) groupingFields))
434        {
435            return;
436        }
437        XMLUtils.startElement(handler, "grouping-fields");
438        XMLUtils.data(handler, (String) groupingFields);
439        XMLUtils.endElement(handler, "grouping-fields");
440    }
441
442    private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException
443    {
444        Object columnsObj = data.get("columns");
445        if (null == columnsObj || StringUtils.isEmpty((String) columnsObj))
446        {
447            return;
448        }
449        
450        Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj);
451        
452        if (!columns.isEmpty())
453        {
454            AttributesImpl columnsAttributes = new AttributesImpl();
455            Object overrideColumns = data.get("overrideColumns");
456            if (null != overrideColumns && (Boolean) overrideColumns)
457            {
458                columnsAttributes.addCDATAAttribute("override", "true");
459            }
460            XMLUtils.startElement(handler, "columns", columnsAttributes);
461            
462            for (Map.Entry<String, String> column : columns.entrySet())
463            {
464                AttributesImpl columnAttributes = new AttributesImpl();
465                if (column.getValue() != null)
466                {
467                    columnAttributes.addCDATAAttribute("optional", column.getValue());
468                }
469                XMLUtils.startElement(handler, "column", columnAttributes);
470                XMLUtils.data(handler, column.getKey());
471                XMLUtils.endElement(handler, "column");
472            }
473            
474            XMLUtils.endElement(handler, "columns");
475        }
476    }
477
478    private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException
479    {
480        Object sortsObj = data.get("sorts");
481        if (null == sortsObj || StringUtils.isEmpty((String) sortsObj))
482        {
483            return;
484        }
485        
486        Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj);
487        
488        if (!sorts.isEmpty())
489        {
490            AttributesImpl sortsAttributes = new AttributesImpl();
491            Object overrideSorts = data.get("overrideSorts");
492            if (null != overrideSorts && (Boolean) overrideSorts)
493            {
494                sortsAttributes.addCDATAAttribute("override", "true");
495            }
496            XMLUtils.startElement(handler, "sorts", sortsAttributes);
497            
498            for (Map.Entry<String, String> sort : sorts.entrySet())
499            {
500                AttributesImpl sortAttributes = new AttributesImpl();
501                if (sort.getValue() != null)
502                {
503                    sortAttributes.addCDATAAttribute("order", sort.getValue());
504                }
505                XMLUtils.startElement(handler, "sort", sortAttributes);
506                XMLUtils.data(handler, sort.getKey());
507                XMLUtils.endElement(handler, "sort");
508            }
509            
510            XMLUtils.endElement(handler, "sorts");
511        }
512    }
513    
514    private Map<String, String> _splitDataAndExtradataFromString(String str)
515    {
516        Map<String, String> result = new LinkedHashMap<>();
517        
518        for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER))
519        {
520            Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX);
521            Matcher matcher = pattern.matcher(data);
522            
523            String extra = null;
524            if (matcher.find())
525            {
526                extra = matcher.group(1);
527            }
528            
529            String finalData = data;
530            if (null != extra)
531            {
532                extra = extra.trim();
533                int indexOfExtra = data.indexOf("(");
534                finalData = data.substring(0, indexOfExtra);
535            }
536            
537            result.put(finalData.trim(), extra);
538        }
539        
540        return result;
541    }
542    
543}