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