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.Path; 023import java.nio.file.Paths; 024import java.nio.file.StandardCopyOption; 025import java.util.Collections; 026import java.util.List; 027import java.util.Map; 028import java.util.Properties; 029import java.util.function.BiConsumer; 030import java.util.stream.Collectors; 031import java.util.stream.IntStream; 032import java.util.stream.Stream; 033 034import javax.xml.parsers.DocumentBuilder; 035import javax.xml.parsers.DocumentBuilderFactory; 036import javax.xml.transform.OutputKeys; 037import javax.xml.transform.Transformer; 038import javax.xml.transform.TransformerFactory; 039import javax.xml.transform.dom.DOMSource; 040import javax.xml.transform.sax.SAXTransformerFactory; 041import javax.xml.transform.sax.TransformerHandler; 042import javax.xml.transform.stream.StreamResult; 043 044import org.apache.avalon.framework.service.ServiceException; 045import org.apache.avalon.framework.service.ServiceManager; 046import org.apache.cocoon.xml.AttributesImpl; 047import org.apache.cocoon.xml.XMLUtils; 048import org.apache.commons.lang3.StringUtils; 049import org.apache.excalibur.source.Source; 050import org.apache.excalibur.source.SourceResolver; 051import org.apache.excalibur.source.TraversableSource; 052import org.apache.excalibur.source.impl.FileSource; 053import org.apache.xml.serializer.OutputPropertiesFactory; 054import org.w3c.dom.Document; 055import org.w3c.dom.Element; 056import org.w3c.dom.NodeList; 057 058import org.ametys.cms.repository.ContentDAO; 059import org.ametys.cms.workflow.ContentWorkflowHelper; 060import org.ametys.core.ui.Callable; 061import org.ametys.core.ui.StaticClientSideElement; 062import org.ametys.core.user.UserIdentity; 063import org.ametys.core.util.I18nUtils; 064import org.ametys.plugins.core.user.UserHelper; 065import org.ametys.plugins.extraction.ExtractionConstants; 066import org.ametys.plugins.extraction.execution.ExtractionDAO; 067import org.ametys.plugins.extraction.rights.ExtractionAccessController; 068import org.ametys.runtime.authentication.AccessDeniedException; 069import org.ametys.runtime.i18n.I18nizableText; 070 071/** 072 * This client site element manages a button to create an extraction definition file 073 */ 074public class EditExtractionClientSideElement extends StaticClientSideElement 075{ 076 /** The Avalon role name */ 077 public static final String ROLE = EditExtractionClientSideElement.class.getName(); 078 079 private SourceResolver _sourceResolver; 080 private ContentWorkflowHelper _contentWorkflowHelper; 081 private I18nUtils _i18nUtils; 082 private ContentDAO _contentDAO; 083 private UserHelper _userHelper; 084 private ExtractionDAO _extractionDAO; 085 086 @Override 087 public void service(ServiceManager serviceManager) throws ServiceException 088 { 089 super.service(serviceManager); 090 _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE); 091 _contentWorkflowHelper = (ContentWorkflowHelper) serviceManager.lookup(ContentWorkflowHelper.ROLE); 092 _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE); 093 _contentDAO = (ContentDAO) serviceManager.lookup(ContentDAO.ROLE); 094 _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE); 095 _extractionDAO = (ExtractionDAO) serviceManager.lookup(ExtractionDAO.ROLE); 096 097 } 098 099 /** 100 * Creates an extraction definition file. 101 * @param relativeDefinitionFilePath The path of the extraction definition file to create. This path has to be relative to the base definition directory. 102 * @param language the language used to create the description 103 * @return Map containing success boolean and the created extraction informations, or error codes if one occurs 104 * @throws Exception if an error occurs 105 */ 106 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 107 public Map<String, Object> createExtraction(String relativeDefinitionFilePath, String language) throws Exception 108 { 109 // Create extraction definitions directory 110 Source definitionsSrc = _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR); 111 File definitionsDir = ((FileSource) definitionsSrc).getFile(); 112 definitionsDir.mkdirs(); 113 114 String absoluteDefinitionFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath; 115 TraversableSource definitionSrc = (TraversableSource) _sourceResolver.resolveURI(absoluteDefinitionFilePath); 116 File definitionFile = ((FileSource) definitionSrc).getFile(); 117 118 if (definitionFile.exists()) 119 { 120 getLogger().error("A definition file already exists at path '{}'", relativeDefinitionFilePath); 121 return Map.of( 122 "success", false, 123 "error", "already-exists"); 124 } 125 126 if (!_extractionDAO.canWrite(_currentUserProvider.getUser(), (TraversableSource) definitionSrc.getParent())) 127 { 128 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access extraction feature without convenient right"); 129 } 130 131 try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath()))) 132 { 133 // Create the description content 134 String extractionName = _getExtractionNameFromFileName(definitionFile.getName()); 135 I18nizableText descriptionTitle = new I18nizableText(ExtractionConstants.PLUGIN_NAME, ExtractionConstants.DESCRIPTION_DEFAULT_TITLE_KEY, Collections.singletonList(extractionName)); 136 137 Map<String, Object> contentInfos = _contentWorkflowHelper.createContent( 138 ExtractionConstants.DESCRIPTION_CONTENT_WORKFLOW_NAME, 139 ExtractionConstants.DESCRIPTION_CONTENT_INITIAL_ACTION_ID, 140 extractionName, 141 _i18nUtils.translate(descriptionTitle, language), 142 new String[] {ExtractionConstants.DESCRIPTION_CONTENT_TYPE_ID}, 143 new String[0], 144 language 145 ); 146 String descriptionId = (String) contentInfos.get("contentId"); 147 148 // Create a transformer for saving sax into a file 149 TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 150 151 StreamResult result = new StreamResult(os); 152 handler.setResult(result); 153 154 // create the format of result 155 Properties format = new Properties(); 156 format.put(OutputKeys.METHOD, "xml"); 157 format.put(OutputKeys.INDENT, "yes"); 158 format.put(OutputKeys.ENCODING, "UTF-8"); 159 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 160 handler.getTransformer().setOutputProperties(format); 161 handler.startDocument(); 162 163 // sax skeleton 164 XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG); 165 166 // Generate SAX events for the extraction's description 167 AttributesImpl attributes = new AttributesImpl(); 168 attributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId); 169 XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, attributes); 170 171 // Generate SAX events for the extraction's author 172 UserIdentity author = _currentUserProvider.getUser(); 173 _userHelper.saxUserIdentity(author, handler, ExtractionConstants.AUTHOR_TAG); 174 175 XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG); 176 177 handler.endDocument(); 178 179 return Map.of( 180 "success", true, 181 "path", relativeDefinitionFilePath, 182 "name", definitionFile.getName()); 183 } 184 catch (Exception e) 185 { 186 getLogger().error("Error when trying to create the extraction definition file '{}'", relativeDefinitionFilePath, e); 187 return Map.of( 188 "success", false, 189 "error", "other-error"); 190 } 191 } 192 193 private String _getExtractionNameFromFileName(String fileName) 194 { 195 return fileName.contains(".") ? fileName.substring(0, fileName.lastIndexOf(".")) : fileName; 196 } 197 198 /** 199 * Adds a description to an extraction. 200 * @param definitionFileName The extraction definition file name 201 * @param descriptionId the identifier of the description 202 * @return Map containing success boolean and error codes if one occurs 203 * @throws Exception if an error occurs 204 */ 205 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 206 public Map<String, Object> addDescription(String definitionFileName, String descriptionId) throws Exception 207 { 208 String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName; 209 FileSource definitionSrc = (FileSource) _sourceResolver.resolveURI(definitionFilePath); 210 211 if (!_extractionDAO.canWrite(_currentUserProvider.getUser(), definitionSrc)) 212 { 213 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access extraction feature without convenient right"); 214 } 215 216 return _modifyDefinitionFile(definitionSrc, descriptionId, this::_insertDescriptionInDocument); 217 } 218 219 private void _insertDescriptionInDocument(Document document, String descriptionId) 220 { 221 Element extractionRoot = document.getDocumentElement(); 222 223 // Delete the current description node if exists 224 _getElements(document.getDocumentElement(), ExtractionConstants.DESCRIPTION_TAG) 225 .forEach(extractionRoot::removeChild); 226 227 // Insert the description in the extraction root node 228 Element description = document.createElement(ExtractionConstants.DESCRIPTION_TAG); 229 description.setAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId); 230 extractionRoot.insertBefore(description, extractionRoot.getFirstChild()); 231 } 232 233 private <T> Map<String, Object> _modifyDefinitionFile(FileSource definitionSrc, T dataToModify, BiConsumer<Document, T> modifyingConsumer) throws Exception 234 { 235 File definitionFile = definitionSrc.getFile(); 236 237 if (!definitionFile.exists()) 238 { 239 getLogger().error("Error while adding a description to the extraction '{}': this definition file doesn't exist.", definitionSrc.getURI()); 240 return Map.of( 241 "success", false, 242 "error", "unexisting"); 243 } 244 245 String tmpFilePath = ExtractionConstants.DEFINITIONS_DIR + definitionSrc.getName() + ".tmp"; 246 Source tmpSrc = _sourceResolver.resolveURI(tmpFilePath); 247 File tmpFile = ((FileSource) tmpSrc).getFile(); 248 249 try (OutputStream os = Files.newOutputStream(Paths.get(tmpFile.getAbsolutePath()))) 250 { 251 // Parse existing definition file 252 Document document = _parseDefinitionFile(definitionFile); 253 254 // Apply the modification 255 modifyingConsumer.accept(document, dataToModify); 256 257 // Write the updated definition file 258 DOMSource source = new DOMSource(document); 259 Transformer transformer = TransformerFactory.newInstance().newTransformer(); 260 StreamResult result = new StreamResult(os); 261 transformer.transform(source, result); 262 263 Files.copy(tmpFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 264 265 return Map.of("success", true); 266 } 267 catch (Exception e) 268 { 269 getLogger().error("Error when trying to modify the extraction '{}'", definitionSrc.getURI(), e); 270 return Map.of( 271 "success", false, 272 "error", "other-error"); 273 } 274 finally 275 { 276 // delete the temporary file to keep the original one 277 _deleteTemporaryFile(definitionSrc.getURI(), tmpFile.toPath()); 278 } 279 } 280 281 private void _deleteTemporaryFile(String definitionFileName, Path temporaryFilePath) 282 { 283 try 284 { 285 Files.deleteIfExists(temporaryFilePath); 286 } 287 catch (IOException e) 288 { 289 getLogger().error("Error when deleting the temporary file for '{}'", definitionFileName, e); 290 } 291 } 292 293 /** 294 * Renames an extraction definition file. 295 * @param relativeOldFilePath The extraction definition old file path, relative to the base definitions directory 296 * @param newFileName The extraction definition new file name 297 * @return Map containing success boolean and error codes if one occurs 298 * @throws Exception if an error occurs 299 */ 300 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 301 public Map<String, Object> renameExtraction(String relativeOldFilePath, String newFileName) throws Exception 302 { 303 String fileUri = ExtractionConstants.DEFINITIONS_DIR + relativeOldFilePath; 304 FileSource file = (FileSource) _sourceResolver.resolveURI(fileUri); 305 306 if (!_extractionDAO.canWrite(_currentUserProvider.getUser(), file)) 307 { 308 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access extraction feature without convenient right"); 309 } 310 311 String relativeParentPath = StringUtils.removeEnd(relativeOldFilePath, file.getName()); 312 String relativeNewFilePath = relativeParentPath + newFileName; 313 return _extractionDAO.moveOrRenameExtractionDefinitionFile(relativeOldFilePath, relativeNewFilePath); 314 } 315 316 /** 317 * Deletes an extraction definition file. 318 * @param definitionFileName The extraction definition file to delete 319 * @return <code>true</code> if extraction deletion succeed, <code>false</code> otherwise 320 * @throws Exception if an error occurs 321 */ 322 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION) 323 public boolean deleteExtraction(String definitionFileName) throws Exception 324 { 325 String filePath = ExtractionConstants.DEFINITIONS_DIR + definitionFileName; 326 FileSource source = (FileSource) _sourceResolver.resolveURI(filePath); 327 328 if (!source.exists()) 329 { 330 throw new IllegalArgumentException("Error while deleting '" + definitionFileName + "': this definition file doesn't exist."); 331 } 332 333 if (!_extractionDAO.canDelete(_currentUserProvider.getUser(), source)) 334 { 335 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access extraction feature without convenient right"); 336 } 337 338 try 339 { 340 // Get description identifiers 341 File file = source.getFile(); 342 Document document = _parseDefinitionFile(file); 343 List<String> descriptionIds = _getElements(document.getDocumentElement(), ExtractionConstants.DESCRIPTION_TAG) 344 .map(description -> description.getAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME)) 345 .collect(Collectors.toList()); 346 347 // Delete descriptions contents 348 _contentDAO.deleteContents(descriptionIds, ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID); 349 350 Files.deleteIfExists(file.toPath()); 351 352 String context = ExtractionAccessController.ROOT_CONTEXT + "/" + definitionFileName; 353 _extractionDAO.deleteRights(context); 354 } 355 catch (IOException e) 356 { 357 if (getLogger().isErrorEnabled()) 358 { 359 getLogger().error("Error while deleting definition file '" + definitionFileName + "'.", e); 360 } 361 throw e; 362 } 363 364 return true; 365 } 366 367 private Document _parseDefinitionFile(File definitionFile) throws Exception 368 { 369 DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); 370 DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); 371 return documentBuilder.parse(definitionFile); 372 } 373 374 private Stream<Element> _getElements(Element extractionRoot, String nodeTagName) 375 { 376 NodeList nodeList = extractionRoot.getElementsByTagName(nodeTagName); 377 return IntStream.range(0, nodeList.getLength()) 378 .mapToObj(nodeList::item) 379 .map(Element.class::cast); 380 } 381 382}