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