001/*
002 *  Copyright 2018 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.execution;
017
018import java.io.File;
019import java.io.OutputStream;
020import java.nio.file.Path;
021import java.nio.file.Paths;
022import java.time.ZonedDateTime;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027import java.util.stream.Collectors;
028import java.util.stream.StreamSupport;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.cocoon.xml.AttributesImpl;
035import org.apache.cocoon.xml.XMLUtils;
036import org.apache.commons.collections4.ListUtils;
037import org.apache.commons.lang3.StringUtils;
038import org.apache.commons.lang3.tuple.Pair;
039import org.apache.excalibur.source.Source;
040import org.apache.excalibur.source.SourceResolver;
041import org.apache.excalibur.source.impl.FileSource;
042import org.xml.sax.ContentHandler;
043
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.contenttype.ContentTypesHelper;
046import org.ametys.cms.repository.Content;
047import org.ametys.core.right.RightManager;
048import org.ametys.core.right.RightManager.RightResult;
049import org.ametys.core.user.CurrentUserProvider;
050import org.ametys.core.util.DateUtils;
051import org.ametys.plugins.extraction.ExtractionConstants;
052import org.ametys.plugins.extraction.component.ExtractionComponent;
053import org.ametys.plugins.extraction.component.TwoStepsExecutingExtractionComponent;
054import org.ametys.plugins.extraction.execution.pipeline.Pipeline;
055import org.ametys.plugins.extraction.execution.pipeline.PipelineDescriptor;
056import org.ametys.plugins.extraction.execution.pipeline.Pipelines;
057import org.ametys.plugins.repository.AmetysObjectResolver;
058import org.ametys.runtime.plugin.component.AbstractLogEnabled;
059import org.ametys.runtime.util.AmetysHomeHelper;
060
061import com.google.common.base.Predicates;
062
063/**
064 * Extracts query results form a XML definition file
065 */
066public class ExtractionExecutor extends AbstractLogEnabled implements Component, Serviceable
067{
068    /** The Avalon role. */
069    public static final String ROLE = ExtractionExecutor.class.getName();
070    
071    /** The directory under ametys home data directory for extraction results */
072    public static final String EXTRACTION_DIR_NAME = "extraction";
073
074    private RightManager _rightManager;
075    private ExtractionDefinitionReader _reader;
076    private CurrentUserProvider _currentUserProvider;
077    private ContentTypeExtensionPoint _contentTypeExtensionPoint;
078    private AmetysObjectResolver _resolver;
079    private ContentTypesHelper _contentTypesHelper;
080    private SourceResolver _sourceResolver;
081    private PathResolver _resultPathResolver;
082
083    @Override
084    public void service(ServiceManager serviceManager) throws ServiceException
085    {
086        _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE);
087        _reader = (ExtractionDefinitionReader) serviceManager.lookup(ExtractionDefinitionReader.ROLE);
088        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
089        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE);
090        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
091        _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE);
092        _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE);
093        _resultPathResolver = (PathResolver) serviceManager.lookup(PathResolver.ROLE);
094    }
095    
096    /**
097     * Executes the given extraction
098     * @param fileName The name of the extraction file to execute
099     * @param defaultResultFileName The default file name for the result (it can be unused, for instance if resultSubFolder is a file, i.e. contains a '.' in its last element)
100     * @param lang The lang
101     * @param parameters The parameters
102     * @param pipeline The execution pipeline
103     * @throws Exception if an errors occured
104     */
105    public void execute(String fileName, String defaultResultFileName, String lang, Map<String, Object> parameters, PipelineDescriptor pipeline) throws Exception
106    {
107        _execute(fileName, defaultResultFileName, null, lang, parameters, pipeline);
108    }
109    
110    /**
111     * Executes the given extraction
112     * @param fileName The name of the extraction file to execute
113     * @param resultOutputStream The result output stream
114     * @param lang The lang
115     * @param parameters The parameters
116     * @param pipeline The execution pipeline
117     * @throws Exception if an errors occured
118     */
119    public void execute(String fileName, OutputStream resultOutputStream, String lang, Map<String, Object> parameters, PipelineDescriptor pipeline) throws Exception
120    {
121        _execute(fileName, null, resultOutputStream, lang, parameters, pipeline);
122    }
123    
124    private void _execute(
125            String fileName,
126            String defaultResultFileName, 
127            OutputStream resultOutputStream, 
128            String lang, 
129            Map<String, Object> parameters, 
130            PipelineDescriptor pipeline) throws Exception
131    {
132        _checkRights();
133        
134        Pair<Extraction, String> extractionAndName = _getExtraction(fileName);
135        Extraction extraction = extractionAndName.getLeft();
136        String definitionFilename = extractionAndName.getRight();
137        
138        AttributesImpl attributes = _getAttrs(definitionFilename);
139
140        ExtractionExecutionContext context = _getContext(extraction, lang, parameters);
141        
142        if (resultOutputStream != null)
143        {
144            _doExecuteForPathWithNoVar(attributes, extraction, context, resultOutputStream, pipeline);
145        }
146        else
147        {
148            _doExecute(attributes, extraction, fileName, context, defaultResultFileName, pipeline);
149        }
150    }
151    
152    private void _checkRights()
153    {
154        if (_rightManager.hasRight(_currentUserProvider.getUser(), ExtractionConstants.EXECUTE_EXTRACTION_RIGHT_ID, "/admin") != RightResult.RIGHT_ALLOW)
155        {
156            String errorMessage = "User " + _currentUserProvider.getUser() + " tried to execute extraction with no sufficient rights"; 
157            getLogger().error(errorMessage);
158            throw new IllegalStateException(errorMessage);
159        }
160    }
161    
162    private Pair<Extraction, String> _getExtraction(String fileName) throws Exception
163    {
164        String filePath = ExtractionConstants.DEFINITIONS_DIR + fileName;
165        
166        Source src = _sourceResolver.resolveURI(filePath);
167        Extraction extraction = null;
168        String definitionFilename = null;
169        try
170        {
171            File file = ((FileSource) src).getFile();
172            
173            if (!file.exists())
174            {
175                throw new IllegalArgumentException("The file " + filePath + " does not exist.");
176            }
177            
178            extraction = _reader.readExtractionDefinitionFile(file);
179            definitionFilename = file.getName();
180            return Pair.of(extraction, definitionFilename);
181        }
182        catch (Exception e)
183        {
184            throw new IllegalStateException("An unexpected error occured.", e);
185        }
186        finally
187        {
188            _sourceResolver.release(src);
189        }
190    }
191    
192    private AttributesImpl _getAttrs(String definitionFilename)
193    {
194        AttributesImpl attributes = new AttributesImpl();
195        attributes.addCDATAAttribute("user", _currentUserProvider.getUser().getLogin());
196        attributes.addCDATAAttribute("date", ZonedDateTime.now().format(DateUtils.getISODateTimeFormatter()));
197        attributes.addCDATAAttribute("name", definitionFilename);
198        return attributes;
199    }
200    
201    private ExtractionExecutionContext _getContext(Extraction extraction, String lang, Map<String, Object> parameters)
202    {
203        ExtractionExecutionContext context = new ExtractionExecutionContext();
204        if (StringUtils.isNotEmpty(lang))
205        {
206            context.setDefaultLocale(new Locale(lang));
207        }
208        context.setDisplayOptionalColumns(_getDisplayOptionalColumns(extraction.getDisplayOptionalColumnsNames(), parameters));
209        context.setClauseVariables(_getQueryVariables(extraction.getQueryVariablesNamesAndContentTypes(), parameters));
210        return context;
211    }
212
213    Map<String, Boolean> _getDisplayOptionalColumns(List<String> displayOptionalColumnsNames, Map<String, Object> parameters)
214    {
215        Map<String, Boolean> result = new HashMap<>();
216        for (String name : displayOptionalColumnsNames)
217        {
218            Boolean value = _getDipslayOptionalColumn(name, parameters);
219            if (value == null)
220            {
221                throw new IllegalArgumentException("Extraction - There is a variable named '" + name + "' but there is no corresponding value");
222            }
223            else
224            {
225                result.put(name, value);
226            }
227        }
228        return result;
229    }
230    
231    private Boolean _getDipslayOptionalColumn(String optionalColumnName, Map<String, Object> parameters)
232    {
233        Object value = parameters.get(optionalColumnName);
234        
235        if (value instanceof Boolean)
236        {
237            return (Boolean) value;
238        }
239        else if (value instanceof String)
240        {
241            return Boolean.valueOf((String) value);
242        }
243        else
244        {
245            return null;
246        }
247    }
248
249    private Map<String, String> _getQueryVariables(Map<String, String> queryVariablesNamesAndContentTypes, Map<String, Object> parameters)
250    {
251        Map<String, String> result = new HashMap<>();
252        for (Map.Entry<String, String> entry : queryVariablesNamesAndContentTypes.entrySet())
253        {
254            String name = entry.getKey();
255            String contentTypeId = entry.getValue();
256            if (!_contentTypeExtensionPoint.hasExtension(contentTypeId))
257            {
258                throw new IllegalArgumentException("Extraction - content type '" + contentTypeId + "' used in variable '" + name + "' definition does not exist");
259            }
260            
261            String contentId = (String) parameters.get(name);
262            if (null == contentId)
263            {
264                throw new IllegalArgumentException("Extraction - There is a variable named '" + name + "' but there is no corresponding value");
265            }
266            
267            Content content = _resolver.resolveById(contentId);
268            if (!_contentTypesHelper.isInstanceOf(content, contentTypeId))
269            {
270                throw new IllegalArgumentException("Extraction - content '" + contentId + "' is not an instance of content type '" + contentTypeId + "', defined by the variable named '" + name + "'");
271            }
272            
273            result.put(name, contentId);
274        }
275        return result;
276    }
277    
278    private void _doExecute(AttributesImpl attributes, Extraction extraction, String extractionName, ExtractionExecutionContext context, String defaultResultFileName, PipelineDescriptor pipeline) throws Exception
279    {
280        String unresolvedPath = pipeline.getResultSubfolder();
281        Path basePath = Paths.get(AmetysHomeHelper.getAmetysHomeData().toPath().toString(), EXTRACTION_DIR_NAME);
282        if (_resultPathResolver.hasVariable(unresolvedPath))
283        {
284            List<ExtractionComponent> extractionComponents = extraction.getExtractionComponents();
285            boolean allAreNotTwoStepsComponent = extractionComponents.stream()
286                    .filter(Predicates.not(TwoStepsExecutingExtractionComponent.class::isInstance))
287                    .findFirst()
288                    .isPresent();
289            if (allAreNotTwoStepsComponent)
290            {
291                throw new IllegalArgumentException("The extraction " + extractionName + " has an invalid component at first level which does not support a subfolder containing variables.");
292            }
293            
294            List<TwoStepsExecutingExtractionComponent> components = extractionComponents.stream()
295                    .map(TwoStepsExecutingExtractionComponent.class::cast)
296                    .collect(Collectors.toList());
297            _doExecuteForPathWithVar(attributes, extraction, context, components, basePath, unresolvedPath, pipeline);
298        }
299        else
300        {
301            Path fileOrFolderPath = _resultPathResolver.resolvePath(unresolvedPath, null, extraction, basePath).keySet().iterator().next();
302            Path filePath = _filePathWhenNoVar(fileOrFolderPath, defaultResultFileName);
303            try (OutputStream fileOs = Pipelines.getOutputStream(filePath);)
304            {
305                _doExecuteForPathWithNoVar(attributes, extraction, context, fileOs, pipeline);
306            }
307        }
308    }
309    
310    private Path _filePathWhenNoVar(Path fileOrFolderPath, String defaultFileName)
311    {
312        Path fileName = fileOrFolderPath.getFileName();
313        if (fileName.toString().contains("."))
314        {
315            // is already a file, do not use default file name
316            return fileOrFolderPath;
317        }
318        return Paths.get(fileOrFolderPath.toString(), defaultFileName);
319    }
320    
321    private void _doExecuteForPathWithNoVar(AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context, OutputStream outputStream, PipelineDescriptor pipelineDescriptor) throws Exception
322    {
323        try (Pipeline pipeline = pipelineDescriptor.newPipeline(outputStream);)
324        {
325            ContentHandler contentHandler = pipeline.getHandler();
326            _noVarExecute(contentHandler, attributes, extraction, context);
327            pipeline.serialize();
328        }
329    }
330    
331    private void _noVarExecute(ContentHandler contentHandler, AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context) throws Exception
332    {
333        contentHandler.startDocument();
334        XMLUtils.startElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG, attributes);
335        
336        for (ExtractionComponent component : extraction.getExtractionComponents())
337        {
338            component.prepareComponentExecution(context);
339            component.execute(contentHandler, context);
340        }
341        
342        XMLUtils.endElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG);
343        contentHandler.endDocument();
344    }
345    
346    private void _doExecuteForPathWithVar(AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context, List<TwoStepsExecutingExtractionComponent> components, Path basePath, String unresolvedPath, PipelineDescriptor pipelineDescriptor) throws Exception
347    {
348        Map<TwoStepsExecutingExtractionComponent, List<Content>> firstLevelResultsByComponent = new HashMap<>();
349        for (TwoStepsExecutingExtractionComponent component : components)
350        {
351            component.prepareComponentExecution(context);
352            List<Content> firstLevelResults = StreamSupport.stream(component.computeFirstLevelResults(context).spliterator(), false)
353                    .collect(Collectors.toList());
354            firstLevelResultsByComponent.put(component, firstLevelResults);
355        }
356        
357        List<Content> allContents = firstLevelResultsByComponent.values()
358                .stream()
359                .flatMap(List::stream)
360                .collect(Collectors.toList());
361        
362        String unresolvedFilePath = _unresolvedFilePathWhenVar(unresolvedPath, pipelineDescriptor);
363        Map<Path, List<Content>> paths = _resultPathResolver.resolvePath(unresolvedFilePath, allContents, extraction, basePath);
364        for (Path filePath : paths.keySet())
365        {
366            List<Content> involvedContentsForPath = paths.get(filePath);
367            try (OutputStream fileOs = Pipelines.getOutputStream(filePath);
368                 Pipeline pipeline = pipelineDescriptor.newPipeline(fileOs);)
369            {
370                ContentHandler contentHandler = pipeline.getHandler();
371                _withVarExecute(contentHandler, attributes, context, components, involvedContentsForPath, firstLevelResultsByComponent);
372                pipeline.serialize();
373            }
374        }
375    }
376    
377    private String _unresolvedFilePathWhenVar(String unresolvedFolderOrFilePath, PipelineDescriptor pipelineDescriptor)
378    {
379        if (_resultPathResolver.isFolder(unresolvedFolderOrFilePath))
380        {
381            return unresolvedFolderOrFilePath + "/${title}." + pipelineDescriptor.getDefaultExtension();
382        }
383        else
384        {
385            return unresolvedFolderOrFilePath;
386        }
387    }
388    
389    private void _withVarExecute(ContentHandler contentHandler, AttributesImpl attributes, ExtractionExecutionContext context, List<TwoStepsExecutingExtractionComponent> components, List<Content> involvedContentsForPath, Map<TwoStepsExecutingExtractionComponent, List<Content>> firstLevelResultsByComponent) throws Exception
390    {
391        contentHandler.startDocument();
392        XMLUtils.startElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG, attributes);
393        
394        for (TwoStepsExecutingExtractionComponent component : components)
395        {
396            List<Content> firstLevelResults = firstLevelResultsByComponent.get(component);
397            List<Content> involvedFirstLevelResults = ListUtils.intersection(firstLevelResults, involvedContentsForPath); 
398            component.executeFor(contentHandler, involvedFirstLevelResults, context);
399        }
400        
401        XMLUtils.endElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG);
402        contentHandler.endDocument();
403    }
404}