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.Map; 026import java.util.Optional; 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.LocaleUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.excalibur.source.SourceResolver; 041import org.apache.excalibur.source.impl.FileSource; 042import org.xml.sax.ContentHandler; 043 044import org.ametys.cms.contenttype.ContentTypesHelper; 045import org.ametys.cms.repository.Content; 046import org.ametys.core.right.RightManager; 047import org.ametys.core.right.RightManager.RightResult; 048import org.ametys.core.user.CurrentUserProvider; 049import org.ametys.core.user.UserIdentity; 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.Extraction.ClausesVariable; 055import org.ametys.plugins.extraction.execution.Extraction.ClausesVariableType; 056import org.ametys.plugins.extraction.execution.pipeline.Pipeline; 057import org.ametys.plugins.extraction.execution.pipeline.PipelineDescriptor; 058import org.ametys.plugins.extraction.execution.pipeline.Pipelines; 059import org.ametys.plugins.repository.AmetysObjectResolver; 060import org.ametys.runtime.authentication.AccessDeniedException; 061import org.ametys.runtime.plugin.component.AbstractLogEnabled; 062import org.ametys.runtime.util.AmetysHomeHelper; 063 064import com.google.common.base.Predicates; 065 066/** 067 * Extracts query results form a XML definition file 068 */ 069public class ExtractionExecutor extends AbstractLogEnabled implements Component, Serviceable 070{ 071 /** The Avalon role. */ 072 public static final String ROLE = ExtractionExecutor.class.getName(); 073 074 private RightManager _rightManager; 075 private ExtractionDefinitionReader _reader; 076 private CurrentUserProvider _currentUserProvider; 077 private AmetysObjectResolver _resolver; 078 private ContentTypesHelper _contentTypesHelper; 079 private SourceResolver _sourceResolver; 080 private PathResolver _resultPathResolver; 081 082 @Override 083 public void service(ServiceManager serviceManager) throws ServiceException 084 { 085 _rightManager = (RightManager) serviceManager.lookup(RightManager.ROLE); 086 _reader = (ExtractionDefinitionReader) serviceManager.lookup(ExtractionDefinitionReader.ROLE); 087 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.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 String absoluteFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeFilePath; 132 133 FileSource src = null; 134 try 135 { 136 src = (FileSource) _sourceResolver.resolveURI(absoluteFilePath); 137 File file = src.getFile(); 138 139 if (!file.exists()) 140 { 141 throw new IllegalArgumentException("The file " + relativeFilePath + " does not exist."); 142 } 143 144 _checkRights(src); 145 146 Extraction extraction = _reader.readExtractionDefinitionFile(file); 147 148 String definitionFilename = file.getName(); 149 150 AttributesImpl attributes = _getAttrs(definitionFilename); 151 152 ExtractionExecutionContext context = _getContext(extraction, lang, parameters); 153 154 if (resultOutputStream != null) 155 { 156 _doExecuteForPathWithNoVar(attributes, extraction, context, resultOutputStream, pipeline); 157 return Set.of(); 158 } 159 else 160 { 161 return _doExecute(attributes, extraction, relativeFilePath, context, defaultResultFileName, pipeline); 162 } 163 } 164 finally 165 { 166 _sourceResolver.release(src); 167 } 168 } 169 170 private void _checkRights(FileSource src) 171 { 172 UserIdentity currentUser = _currentUserProvider.getUser(); 173 174 if (_rightManager.hasReadAccess(currentUser, src) 175 || _rightManager.hasRight(currentUser, ExtractionConstants.EXECUTE_EXTRACTION_RIGHT_ID, src) == RightResult.RIGHT_ALLOW) 176 { 177 return; 178 } 179 180 String errorMessage = "User " + _currentUserProvider.getUser() + " tried to execute extraction with no sufficient rights"; 181 getLogger().error(errorMessage); 182 throw new AccessDeniedException(errorMessage); 183 } 184 185 private AttributesImpl _getAttrs(String definitionFilename) 186 { 187 AttributesImpl attributes = new AttributesImpl(); 188 attributes.addCDATAAttribute("user", _currentUserProvider.getUser().getLogin()); 189 attributes.addCDATAAttribute("date", ZonedDateTime.now().format(DateUtils.getISODateTimeFormatter())); 190 attributes.addCDATAAttribute("name", definitionFilename); 191 return attributes; 192 } 193 194 private ExtractionExecutionContext _getContext(Extraction extraction, String lang, Map<String, Object> parameters) 195 { 196 ExtractionExecutionContext context = new ExtractionExecutionContext(); 197 if (StringUtils.isNotEmpty(lang)) 198 { 199 context.setDefaultLocale(LocaleUtils.toLocale(lang)); 200 } 201 context.setDisplayOptionalColumns(_getDisplayOptionalColumns(extraction.getDisplayOptionalColumnsNames(), parameters)); 202 context.setClausesVariablesValues(_getClausesVariablesValues(extraction.getClausesVariables(), parameters)); 203 return context; 204 } 205 206 Map<String, Boolean> _getDisplayOptionalColumns(List<String> displayOptionalColumnsNames, Map<String, Object> parameters) 207 { 208 Map<String, Boolean> result = new HashMap<>(); 209 for (String name : displayOptionalColumnsNames) 210 { 211 Boolean value = _getDipslayOptionalColumn(name, parameters); 212 if (value == null) 213 { 214 throw new IllegalArgumentException("Extraction - There is a variable named '" + name + "' but there is no corresponding value"); 215 } 216 else 217 { 218 result.put(name, value); 219 } 220 } 221 return result; 222 } 223 224 private Boolean _getDipslayOptionalColumn(String optionalColumnName, Map<String, Object> parameters) 225 { 226 Object value = parameters.get(optionalColumnName); 227 228 if (value instanceof Boolean) 229 { 230 return (Boolean) value; 231 } 232 else if (value instanceof String) 233 { 234 return Boolean.valueOf((String) value); 235 } 236 else 237 { 238 return null; 239 } 240 } 241 242 private Map<ClausesVariable, List<String>> _getClausesVariablesValues(List<ClausesVariable> clausesVariables, Map<String, Object> parameters) 243 { 244 Map<ClausesVariable, List<String>> result = new HashMap<>(); 245 for (ClausesVariable clausesVariable : clausesVariables) 246 { 247 if (ClausesVariableType.SELECT_CONTENTS.equals(clausesVariable.type())) 248 { 249 // Only one content type for SELECT_CONTENTS clauses variables 250 Optional<String> contentTypeId = clausesVariable.contentTypeIds() 251 .stream() 252 .findFirst(); 253 254 @SuppressWarnings("unchecked") 255 List<String> contentIds = (List<String>) parameters.get(clausesVariable.name()); 256 if (contentIds == null || contentIds.isEmpty()) 257 { 258 throw new IllegalArgumentException("Extraction - There is a variable named '" + clausesVariable.name() + "' but there is no corresponding value"); 259 } 260 261 // If a content type is provided, check all contents' types 262 if (contentTypeId.isPresent()) 263 { 264 for (String contentId : contentIds) 265 { 266 Content content = _resolver.resolveById(contentId); 267 if (!_contentTypesHelper.isInstanceOf(content, contentTypeId.get())) 268 { 269 throw new IllegalArgumentException("Extraction - content '" + contentId + "' is not an instance of content type '" + contentTypeId.get() + "', defined by the variable named '" + clausesVariable.name() + "'"); 270 } 271 } 272 } 273 274 result.put(clausesVariable, contentIds); 275 } 276 else 277 { 278 String solrRequest = (String) parameters.get(clausesVariable.name()); 279 if (StringUtils.isEmpty(solrRequest)) 280 { 281 throw new IllegalArgumentException("Extraction - There is a variable named '" + clausesVariable.name() + "' but there is no corresponding value"); 282 } 283 284 result.put(clausesVariable, List.of(solrRequest)); 285 } 286 } 287 return result; 288 } 289 290 private Set<Path> _doExecute(AttributesImpl attributes, Extraction extraction, String extractionFilePath, ExtractionExecutionContext context, String defaultResultFileName, PipelineDescriptor pipeline) throws Exception 291 { 292 String unresolvedPath = pipeline.getResultSubfolder(); 293 Path basePath = Paths.get(AmetysHomeHelper.getAmetysHomeData().toPath().toString(), ExtractionConstants.RESULT_EXTRACTION_DIR_NAME); 294 295 if (_resultPathResolver.hasVariable(unresolvedPath)) 296 { 297 List<ExtractionComponent> extractionComponents = extraction.getExtractionComponents(); 298 boolean allAreNotTwoStepsComponent = extractionComponents.stream() 299 .filter(Predicates.not(TwoStepsExecutingExtractionComponent.class::isInstance)) 300 .findFirst() 301 .isPresent(); 302 if (allAreNotTwoStepsComponent) 303 { 304 throw new IllegalArgumentException("The extraction " + extractionFilePath + " has an invalid component at first level which does not support a subfolder containing variables."); 305 } 306 307 List<TwoStepsExecutingExtractionComponent> components = extractionComponents.stream() 308 .map(TwoStepsExecutingExtractionComponent.class::cast) 309 .collect(Collectors.toList()); 310 return _doExecuteForPathWithVar(attributes, extraction, context, components, basePath, unresolvedPath, pipeline); 311 } 312 else 313 { 314 Path fileOrFolderPath = _resultPathResolver.resolvePath(unresolvedPath, null, extraction, basePath).keySet().iterator().next(); 315 Path filePath = _filePathWhenNoVar(fileOrFolderPath, defaultResultFileName); 316 try (OutputStream fileOs = Pipelines.getOutputStream(filePath);) 317 { 318 _doExecuteForPathWithNoVar(attributes, extraction, context, fileOs, pipeline); 319 return Set.of(filePath); 320 } 321 } 322 } 323 324 private Path _filePathWhenNoVar(Path fileOrFolderPath, String defaultFileName) 325 { 326 Path fileName = fileOrFolderPath.getFileName(); 327 if (fileName.toString().contains(".")) 328 { 329 // is already a file, do not use default file name 330 return fileOrFolderPath; 331 } 332 return Paths.get(fileOrFolderPath.toString(), defaultFileName); 333 } 334 335 private void _doExecuteForPathWithNoVar(AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context, OutputStream outputStream, PipelineDescriptor pipelineDescriptor) throws Exception 336 { 337 try (Pipeline pipeline = pipelineDescriptor.newPipeline(outputStream);) 338 { 339 ContentHandler contentHandler = pipeline.getHandler(); 340 _noVarExecute(contentHandler, attributes, extraction, context); 341 pipeline.serialize(); 342 } 343 } 344 345 private void _noVarExecute(ContentHandler contentHandler, AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context) throws Exception 346 { 347 contentHandler.startDocument(); 348 XMLUtils.startElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG, attributes); 349 350 for (ExtractionComponent component : extraction.getExtractionComponents()) 351 { 352 component.prepareComponentExecution(context); 353 component.execute(contentHandler, context); 354 } 355 356 XMLUtils.endElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG); 357 contentHandler.endDocument(); 358 } 359 360 private Set<Path> _doExecuteForPathWithVar(AttributesImpl attributes, Extraction extraction, ExtractionExecutionContext context, List<TwoStepsExecutingExtractionComponent> components, Path basePath, String unresolvedPath, PipelineDescriptor pipelineDescriptor) throws Exception 361 { 362 Map<TwoStepsExecutingExtractionComponent, List<Content>> firstLevelResultsByComponent = new HashMap<>(); 363 for (TwoStepsExecutingExtractionComponent component : components) 364 { 365 component.prepareComponentExecution(context); 366 List<Content> firstLevelResults = StreamSupport.stream(component.computeFirstLevelResults(context).spliterator(), false) 367 .collect(Collectors.toList()); 368 firstLevelResultsByComponent.put(component, firstLevelResults); 369 } 370 371 List<Content> allContents = firstLevelResultsByComponent.values() 372 .stream() 373 .flatMap(List::stream) 374 .collect(Collectors.toList()); 375 376 String unresolvedFilePath = _unresolvedFilePathWhenVar(unresolvedPath, pipelineDescriptor); 377 Map<Path, List<Content>> paths = _resultPathResolver.resolvePath(unresolvedFilePath, allContents, extraction, basePath); 378 for (Path filePath : paths.keySet()) 379 { 380 List<Content> involvedContentsForPath = paths.get(filePath); 381 try (OutputStream fileOs = Pipelines.getOutputStream(filePath); 382 Pipeline pipeline = pipelineDescriptor.newPipeline(fileOs);) 383 { 384 ContentHandler contentHandler = pipeline.getHandler(); 385 _withVarExecute(contentHandler, attributes, context, components, involvedContentsForPath, firstLevelResultsByComponent); 386 pipeline.serialize(); 387 } 388 } 389 390 return paths.keySet(); 391 } 392 393 private String _unresolvedFilePathWhenVar(String unresolvedFolderOrFilePath, PipelineDescriptor pipelineDescriptor) 394 { 395 if (_resultPathResolver.isFolder(unresolvedFolderOrFilePath)) 396 { 397 return unresolvedFolderOrFilePath + "/${title}." + pipelineDescriptor.getDefaultExtension(); 398 } 399 else 400 { 401 return unresolvedFolderOrFilePath; 402 } 403 } 404 405 private void _withVarExecute(ContentHandler contentHandler, AttributesImpl attributes, ExtractionExecutionContext context, List<TwoStepsExecutingExtractionComponent> components, List<Content> involvedContentsForPath, Map<TwoStepsExecutingExtractionComponent, List<Content>> firstLevelResultsByComponent) throws Exception 406 { 407 contentHandler.startDocument(); 408 XMLUtils.startElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG, attributes); 409 410 for (TwoStepsExecutingExtractionComponent component : components) 411 { 412 List<Content> firstLevelResults = firstLevelResultsByComponent.get(component); 413 List<Content> involvedFirstLevelResults = ListUtils.intersection(firstLevelResults, involvedContentsForPath); 414 component.executeFor(contentHandler, involvedFirstLevelResults, context); 415 } 416 417 XMLUtils.endElement(contentHandler, ExtractionConstants.RESULT_EXTRACTION_TAG); 418 contentHandler.endDocument(); 419 } 420}