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.Paths; 023import java.nio.file.StandardCopyOption; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Properties; 028import java.util.regex.Matcher; 029import java.util.regex.Pattern; 030 031import javax.xml.transform.OutputKeys; 032import javax.xml.transform.TransformerConfigurationException; 033import javax.xml.transform.TransformerFactory; 034import javax.xml.transform.TransformerFactoryConfigurationError; 035import javax.xml.transform.sax.SAXTransformerFactory; 036import javax.xml.transform.sax.TransformerHandler; 037import javax.xml.transform.stream.StreamResult; 038 039import org.apache.avalon.framework.component.Component; 040import org.apache.avalon.framework.logger.AbstractLogEnabled; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.avalon.framework.service.Serviceable; 044import org.apache.cocoon.xml.AttributesImpl; 045import org.apache.cocoon.xml.XMLUtils; 046import org.apache.commons.collections4.MapUtils; 047import org.apache.commons.lang3.StringUtils; 048import org.apache.excalibur.source.Source; 049import org.apache.excalibur.source.SourceResolver; 050import org.apache.excalibur.source.impl.FileSource; 051import org.apache.xml.serializer.OutputPropertiesFactory; 052import org.xml.sax.SAXException; 053 054import org.ametys.core.user.UserIdentity; 055import org.ametys.plugins.core.user.UserHelper; 056import org.ametys.plugins.extraction.ExtractionConstants; 057import org.ametys.plugins.extraction.execution.Extraction; 058import org.ametys.plugins.extraction.execution.ExtractionDefinitionReader; 059 060/** 061 * Helper that manages the button that saves extraction's modifications 062 */ 063public class SaveExtractionHelper extends AbstractLogEnabled implements Component, Serviceable 064{ 065 /** The Avalon role name */ 066 public static final String ROLE = SaveExtractionHelper.class.getName(); 067 068 private static final String EXTRACT_EXTRA_DATA_REGEX = "\\(([-_a-zA-Z]+)\\)"; 069 070 private SourceResolver _sourceResolver; 071 private ExtractionDefinitionReader _definitionReader; 072 private UserHelper _userHelper; 073 074 public void service(ServiceManager serviceManager) throws ServiceException 075 { 076 _sourceResolver = (SourceResolver) serviceManager.lookup(SourceResolver.ROLE); 077 _definitionReader = (ExtractionDefinitionReader) serviceManager.lookup(ExtractionDefinitionReader.ROLE); 078 _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE); 079 } 080 081 /** 082 * Saves modifications on extraction. Creates the definition file if it doesn't exist 083 * @param relativeDefinitionFilePath The extraction definition file path 084 * @param extractionComponents A <code>Map</code> containing definition informations 085 * @return <code>true</code> if extraction saving succeed, <code>false</code> otherwise 086 * @throws Exception if an error occurs 087 */ 088 public boolean saveExtraction(String relativeDefinitionFilePath, Map<String, Object> extractionComponents) throws Exception 089 { 090 boolean errorOccurred = false; 091 092 String backupFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath + ".tmp"; 093 Source backupSrc = _sourceResolver.resolveURI(backupFilePath); 094 File backupFile = ((FileSource) backupSrc).getFile(); 095 096 String definitionFilePath = ExtractionConstants.DEFINITIONS_DIR + relativeDefinitionFilePath; 097 Source definitionSrc = _sourceResolver.resolveURI(definitionFilePath); 098 File definitionFile = ((FileSource) definitionSrc).getFile(); 099 100 if (!definitionFile.exists()) 101 { 102 throw new IllegalArgumentException("The file " + relativeDefinitionFilePath + " does not exist."); 103 } 104 105 Extraction extraction = _definitionReader.readExtractionDefinitionFile(definitionFile); 106 107 // Create a backup file 108 try 109 { 110 Files.copy(definitionFile.toPath(), backupFile.toPath()); 111 } 112 catch (IOException e) 113 { 114 if (getLogger().isErrorEnabled()) 115 { 116 getLogger().error("Error when creating backup '" + definitionFilePath + "' file", e); 117 } 118 } 119 120 try (OutputStream os = Files.newOutputStream(Paths.get(definitionFile.getAbsolutePath()))) 121 { 122 // Create a transformer for saving sax into a file 123 TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 124 125 StreamResult result = new StreamResult(os); 126 handler.setResult(result); 127 128 // create the format of result 129 Properties format = new Properties(); 130 format.put(OutputKeys.METHOD, "xml"); 131 format.put(OutputKeys.INDENT, "yes"); 132 format.put(OutputKeys.ENCODING, "UTF-8"); 133 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 134 handler.getTransformer().setOutputProperties(format); 135 136 // sax the config 137 try 138 { 139 _saxExtraction(extraction, extractionComponents, handler); 140 } 141 catch (Exception e) 142 { 143 if (getLogger().isErrorEnabled()) 144 { 145 getLogger().error("Error when saxing the extraction defintion file '" + definitionFilePath + "'", e); 146 } 147 errorOccurred = true; 148 } 149 } 150 catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e) 151 { 152 if (getLogger().isErrorEnabled()) 153 { 154 getLogger().error("Error when trying to modify the extraction definition file '" + definitionFilePath + "'", e); 155 } 156 } 157 158 try 159 { 160 // Restore the file if an error previously occurred and delete backup 161 if (errorOccurred) 162 { 163 Files.copy(backupFile.toPath(), definitionFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 164 } 165 Files.deleteIfExists(backupFile.toPath()); 166 } 167 catch (IOException e) 168 { 169 if (getLogger().isErrorEnabled()) 170 { 171 getLogger().error("Error when restoring backup '" + definitionFilePath + "' file", e); 172 } 173 } 174 175 return !errorOccurred; 176 } 177 178 @SuppressWarnings("unchecked") 179 private void _saxExtraction(Extraction extraction, Map<String, Object> extractionComponents, TransformerHandler handler) throws SAXException 180 { 181 handler.startDocument(); 182 183 // Set extraction's name attribute 184 Map<String, Object> extractionData = (Map<String, Object>) extractionComponents.get("data"); 185 AttributesImpl attributes = new AttributesImpl(); 186 if (MapUtils.isNotEmpty(extractionData)) 187 { 188 attributes.addCDATAAttribute("name", (String) extractionData.get("name")); 189 } 190 XMLUtils.startElement(handler, ExtractionConstants.EXTRACTION_TAG, attributes); 191 192 // Set extraction's description 193 String descriptionId = extraction.getDescriptionId(); 194 if (StringUtils.isNotEmpty(descriptionId)) 195 { 196 AttributesImpl descriptionAttributes = new AttributesImpl(); 197 descriptionAttributes.addCDATAAttribute(ExtractionConstants.DESCRIPTION_IDENTIFIER_ATTRIBUTE_NAME, descriptionId); 198 XMLUtils.createElement(handler, ExtractionConstants.DESCRIPTION_TAG, descriptionAttributes); 199 } 200 201 // Set extraction's author 202 _saxAuthor(extraction, handler); 203 204 List<Map<String, Object>> children = (List<Map<String, Object>>) extractionComponents.get("children"); 205 if (null != children) 206 { 207 for (Map<String, Object> child : children) 208 { 209 String tag = (String) child.get("tag"); 210 switch (tag) 211 { 212 case ExtractionConstants.CLAUSES_VARIABLES_TAG: 213 _saxClausesVariables(child, handler); 214 break; 215 case ExtractionConstants.OPTIONAL_COLUMNS_TAG: 216 _saxOptionalColumns(child, handler); 217 break; 218 case ExtractionConstants.QUERY_COMPONENT_TAG: 219 case ExtractionConstants.THESAURUS_COMPONENT_TAG: 220 case ExtractionConstants.COUNT_COMPONENT_TAG: 221 case ExtractionConstants.MAPPING_QUERY_COMPONENT_TAG: 222 _saxExtractionComponent(child, handler); 223 break; 224 default: 225 break; 226 } 227 } 228 } 229 230 XMLUtils.endElement(handler, ExtractionConstants.EXTRACTION_TAG); 231 handler.endDocument(); 232 } 233 234 private void _saxAuthor(Extraction extraction, TransformerHandler handler) throws SAXException 235 { 236 // Author 237 UserIdentity author = extraction.getAuthor(); 238 if (author != null) 239 { 240 _userHelper.saxUserIdentity(author, handler, ExtractionConstants.AUTHOR_TAG); 241 } 242 } 243 244 @SuppressWarnings("unchecked") 245 private void _saxClausesVariables(Map<String, Object> child, TransformerHandler handler) throws SAXException 246 { 247 Map<String, Object> data = (Map<String, Object>) child.get("data"); 248 249 List<Map<String, Object>> variables = (List<Map<String, Object>>) data.get("variables"); 250 if (!variables.isEmpty()) 251 { 252 XMLUtils.startElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG); 253 for (Map<String, Object> variable : variables) 254 { 255 AttributesImpl attributes = new AttributesImpl(); 256 attributes.addCDATAAttribute("name", (String) variable.get("name")); 257 attributes.addCDATAAttribute("contentType", (String) variable.get("contentType")); 258 XMLUtils.createElement(handler, "variable", attributes); 259 } 260 XMLUtils.endElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG); 261 } 262 } 263 264 @SuppressWarnings("unchecked") 265 private void _saxOptionalColumns(Map<String, Object> child, TransformerHandler handler) throws SAXException 266 { 267 Map<String, Object> data = (Map<String, Object>) child.get("data"); 268 269 List<String> names = (List<String>) data.get("names"); 270 if (null != names && !names.isEmpty()) 271 { 272 XMLUtils.startElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG); 273 for (String name : names) 274 { 275 XMLUtils.startElement(handler, "name"); 276 XMLUtils.data(handler, name.trim()); 277 XMLUtils.endElement(handler, "name"); 278 } 279 XMLUtils.endElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG); 280 } 281 } 282 283 @SuppressWarnings("unchecked") 284 private void _saxExtractionComponent(Map<String, Object> component, TransformerHandler handler) throws SAXException 285 { 286 Map<String, Object> data = (Map<String, Object>) component.get("data"); 287 288 String tag = (String) component.get("tag"); 289 AttributesImpl attributes = _getComponentAttibutes(data); 290 XMLUtils.startElement(handler, tag, attributes); 291 292 // sax component's elements 293 _saxExtractionComponentClauses(data, handler); 294 _saxExtractionComponentGroupingFields(data, handler); 295 _saxExtractionComponentColumns(data, handler); 296 _saxExtractionComponentSorts(data, handler); 297 298 // process children 299 if (component.get("children") != null) 300 { 301 List<Map<String, Object>> children = (List<Map<String, Object>>) component.get("children"); 302 for (Map<String, Object> child : children) 303 { 304 _saxExtractionComponent(child, handler); 305 } 306 } 307 308 XMLUtils.endElement(handler, tag); 309 } 310 311 @SuppressWarnings("unchecked") 312 private AttributesImpl _getComponentAttibutes(Map<String, Object> data) 313 { 314 AttributesImpl attributes = new AttributesImpl(); 315 316 Object componentTagName = data.get("componentTagName"); 317 if (null != componentTagName && !StringUtils.isEmpty((String) componentTagName)) 318 { 319 attributes.addCDATAAttribute("tagName", (String) componentTagName); 320 } 321 322 List<String> contentTypes = (List<String>) data.get("contentTypes"); 323 if (null != contentTypes && !contentTypes.isEmpty()) 324 { 325 attributes.addCDATAAttribute("contentTypes", String.join(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER, contentTypes)); 326 } 327 328 Object queryReferenceId = data.get("queryReferenceId"); 329 if (null != queryReferenceId && !StringUtils.isEmpty((String) queryReferenceId)) 330 { 331 attributes.addCDATAAttribute("ref", (String) queryReferenceId); 332 } 333 334 Object microThesaurusId = data.get("microThesaurusId"); 335 if (null != microThesaurusId && !StringUtils.isEmpty((String) microThesaurusId)) 336 { 337 attributes.addCDATAAttribute("microThesaurus", (String) microThesaurusId); 338 } 339 340 Object maxLevel = data.get("maxLevel"); 341 if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel))) 342 { 343 attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel)); 344 } 345 346 return attributes; 347 } 348 349 @SuppressWarnings("unchecked") 350 private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException 351 { 352 List<String> clauses = (List<String>) data.get("clauses"); 353 if (null != clauses && !clauses.isEmpty()) 354 { 355 XMLUtils.startElement(handler, "clauses"); 356 for (String clause : clauses) 357 { 358 XMLUtils.startElement(handler, "clause"); 359 XMLUtils.data(handler, clause); 360 XMLUtils.endElement(handler, "clause"); 361 } 362 XMLUtils.endElement(handler, "clauses"); 363 } 364 } 365 366 private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException 367 { 368 Object groupingFields = data.get("groupingFields"); 369 if (null == groupingFields || StringUtils.isEmpty((String) groupingFields)) 370 { 371 return; 372 } 373 XMLUtils.startElement(handler, "grouping-fields"); 374 XMLUtils.data(handler, (String) groupingFields); 375 XMLUtils.endElement(handler, "grouping-fields"); 376 } 377 378 private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException 379 { 380 Object columnsObj = data.get("columns"); 381 if (null == columnsObj || StringUtils.isEmpty((String) columnsObj)) 382 { 383 return; 384 } 385 386 Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj); 387 388 if (!columns.isEmpty()) 389 { 390 AttributesImpl columnsAttributes = new AttributesImpl(); 391 Object overrideColumns = data.get("overrideColumns"); 392 if (null != overrideColumns && (Boolean) overrideColumns) 393 { 394 columnsAttributes.addCDATAAttribute("override", "true"); 395 } 396 XMLUtils.startElement(handler, "columns", columnsAttributes); 397 398 for (Map.Entry<String, String> column : columns.entrySet()) 399 { 400 AttributesImpl columnAttributes = new AttributesImpl(); 401 if (column.getValue() != null) 402 { 403 columnAttributes.addCDATAAttribute("optional", column.getValue()); 404 } 405 XMLUtils.startElement(handler, "column", columnAttributes); 406 XMLUtils.data(handler, column.getKey()); 407 XMLUtils.endElement(handler, "column"); 408 } 409 410 XMLUtils.endElement(handler, "columns"); 411 } 412 } 413 414 private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException 415 { 416 Object sortsObj = data.get("sorts"); 417 if (null == sortsObj || StringUtils.isEmpty((String) sortsObj)) 418 { 419 return; 420 } 421 422 Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj); 423 424 if (!sorts.isEmpty()) 425 { 426 AttributesImpl sortsAttributes = new AttributesImpl(); 427 Object overrideSorts = data.get("overrideSorts"); 428 if (null != overrideSorts && (Boolean) overrideSorts) 429 { 430 sortsAttributes.addCDATAAttribute("override", "true"); 431 } 432 XMLUtils.startElement(handler, "sorts", sortsAttributes); 433 434 for (Map.Entry<String, String> sort : sorts.entrySet()) 435 { 436 AttributesImpl sortAttributes = new AttributesImpl(); 437 if (sort.getValue() != null) 438 { 439 sortAttributes.addCDATAAttribute("order", sort.getValue()); 440 } 441 XMLUtils.startElement(handler, "sort", sortAttributes); 442 XMLUtils.data(handler, sort.getKey()); 443 XMLUtils.endElement(handler, "sort"); 444 } 445 446 XMLUtils.endElement(handler, "sorts"); 447 } 448 } 449 450 private Map<String, String> _splitDataAndExtradataFromString(String str) 451 { 452 Map<String, String> result = new LinkedHashMap<>(); 453 454 for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER)) 455 { 456 Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX); 457 Matcher matcher = pattern.matcher(data); 458 459 String extra = null; 460 if (matcher.find()) 461 { 462 extra = matcher.group(1); 463 } 464 465 String finalData = data; 466 if (null != extra) 467 { 468 extra = extra.trim(); 469 int indexOfExtra = data.indexOf("("); 470 finalData = data.substring(0, indexOfExtra); 471 } 472 473 result.put(finalData.trim(), extra); 474 } 475 476 return result; 477 } 478 479}