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