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 definition 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("type", (String) variable.get("type")); 258 259 XMLUtils.startElement(handler, "variable", attributes); 260 261 List<String> contentTypeIds = (List<String>) variable.get("contentTypeIds"); 262 if (contentTypeIds != null && !contentTypeIds.isEmpty()) 263 { 264 XMLUtils.startElement(handler, "content-types"); 265 266 for (String contentTypeId : contentTypeIds) 267 { 268 AttributesImpl contentTypeAttributes = new AttributesImpl(); 269 contentTypeAttributes.addCDATAAttribute("id", contentTypeId); 270 XMLUtils.createElement(handler, "content-type", contentTypeAttributes); 271 } 272 273 XMLUtils.endElement(handler, "content-types"); 274 } 275 276 String solrRequest = (String) variable.get("solrRequest"); 277 if (StringUtils.isNotEmpty(solrRequest)) 278 { 279 XMLUtils.createElement(handler, "solr-request", solrRequest); 280 } 281 282 XMLUtils.endElement(handler, "variable"); 283 } 284 XMLUtils.endElement(handler, ExtractionConstants.CLAUSES_VARIABLES_TAG); 285 } 286 } 287 288 @SuppressWarnings("unchecked") 289 private void _saxOptionalColumns(Map<String, Object> child, TransformerHandler handler) throws SAXException 290 { 291 Map<String, Object> data = (Map<String, Object>) child.get("data"); 292 293 List<String> names = (List<String>) data.get("names"); 294 if (null != names && !names.isEmpty()) 295 { 296 XMLUtils.startElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG); 297 for (String name : names) 298 { 299 XMLUtils.startElement(handler, "name"); 300 XMLUtils.data(handler, name.trim()); 301 XMLUtils.endElement(handler, "name"); 302 } 303 XMLUtils.endElement(handler, ExtractionConstants.OPTIONAL_COLUMNS_TAG); 304 } 305 } 306 307 @SuppressWarnings("unchecked") 308 private void _saxExtractionComponent(Map<String, Object> component, TransformerHandler handler) throws SAXException 309 { 310 Map<String, Object> data = (Map<String, Object>) component.get("data"); 311 312 String tag = (String) component.get("tag"); 313 AttributesImpl attributes = _getComponentAttibutes(data); 314 XMLUtils.startElement(handler, tag, attributes); 315 316 // sax component's elements 317 _saxExtractionComponentClauses(data, handler); 318 _saxExtractionComponentGroupingFields(data, handler); 319 _saxExtractionComponentColumns(data, handler); 320 _saxExtractionComponentSorts(data, handler); 321 322 // process children 323 if (component.get("children") != null) 324 { 325 List<Map<String, Object>> children = (List<Map<String, Object>>) component.get("children"); 326 for (Map<String, Object> child : children) 327 { 328 _saxExtractionComponent(child, handler); 329 } 330 } 331 332 XMLUtils.endElement(handler, tag); 333 } 334 335 @SuppressWarnings("unchecked") 336 private AttributesImpl _getComponentAttibutes(Map<String, Object> data) 337 { 338 AttributesImpl attributes = new AttributesImpl(); 339 340 Object componentTagName = data.get("componentTagName"); 341 if (null != componentTagName && !StringUtils.isEmpty((String) componentTagName)) 342 { 343 attributes.addCDATAAttribute("tagName", (String) componentTagName); 344 } 345 346 List<String> contentTypes = (List<String>) data.get("contentTypes"); 347 if (null != contentTypes && !contentTypes.isEmpty()) 348 { 349 attributes.addCDATAAttribute("contentTypes", String.join(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER, contentTypes)); 350 } 351 352 Object queryReferenceId = data.get("queryReferenceId"); 353 if (null != queryReferenceId && !StringUtils.isEmpty((String) queryReferenceId)) 354 { 355 attributes.addCDATAAttribute("ref", (String) queryReferenceId); 356 } 357 358 Object microThesaurusId = data.get("microThesaurusId"); 359 if (null != microThesaurusId && !StringUtils.isEmpty((String) microThesaurusId)) 360 { 361 attributes.addCDATAAttribute("microThesaurus", (String) microThesaurusId); 362 } 363 364 Object maxLevel = data.get("maxLevel"); 365 if (null != maxLevel && !StringUtils.isEmpty(String.valueOf(maxLevel))) 366 { 367 attributes.addCDATAAttribute("max-level", String.valueOf(maxLevel)); 368 } 369 370 return attributes; 371 } 372 373 @SuppressWarnings("unchecked") 374 private void _saxExtractionComponentClauses(Map<String, Object> data, TransformerHandler handler) throws SAXException 375 { 376 List<String> clauses = (List<String>) data.get("clauses"); 377 if (null != clauses && !clauses.isEmpty()) 378 { 379 XMLUtils.startElement(handler, "clauses"); 380 for (String clause : clauses) 381 { 382 XMLUtils.startElement(handler, "clause"); 383 XMLUtils.data(handler, clause); 384 XMLUtils.endElement(handler, "clause"); 385 } 386 XMLUtils.endElement(handler, "clauses"); 387 } 388 } 389 390 private void _saxExtractionComponentGroupingFields(Map<String, Object> data, TransformerHandler handler) throws SAXException 391 { 392 Object groupingFields = data.get("groupingFields"); 393 if (null == groupingFields || StringUtils.isEmpty((String) groupingFields)) 394 { 395 return; 396 } 397 XMLUtils.startElement(handler, "grouping-fields"); 398 XMLUtils.data(handler, (String) groupingFields); 399 XMLUtils.endElement(handler, "grouping-fields"); 400 } 401 402 private void _saxExtractionComponentColumns(Map<String, Object> data, TransformerHandler handler) throws SAXException 403 { 404 Object columnsObj = data.get("columns"); 405 if (null == columnsObj || StringUtils.isEmpty((String) columnsObj)) 406 { 407 return; 408 } 409 410 Map<String, String> columns = _splitDataAndExtradataFromString((String) columnsObj); 411 412 if (!columns.isEmpty()) 413 { 414 AttributesImpl columnsAttributes = new AttributesImpl(); 415 Object overrideColumns = data.get("overrideColumns"); 416 if (null != overrideColumns && (Boolean) overrideColumns) 417 { 418 columnsAttributes.addCDATAAttribute("override", "true"); 419 } 420 XMLUtils.startElement(handler, "columns", columnsAttributes); 421 422 for (Map.Entry<String, String> column : columns.entrySet()) 423 { 424 AttributesImpl columnAttributes = new AttributesImpl(); 425 if (column.getValue() != null) 426 { 427 columnAttributes.addCDATAAttribute("optional", column.getValue()); 428 } 429 XMLUtils.startElement(handler, "column", columnAttributes); 430 XMLUtils.data(handler, column.getKey()); 431 XMLUtils.endElement(handler, "column"); 432 } 433 434 XMLUtils.endElement(handler, "columns"); 435 } 436 } 437 438 private void _saxExtractionComponentSorts(Map<String, Object> data, TransformerHandler handler) throws SAXException 439 { 440 Object sortsObj = data.get("sorts"); 441 if (null == sortsObj || StringUtils.isEmpty((String) sortsObj)) 442 { 443 return; 444 } 445 446 Map<String, String> sorts = _splitDataAndExtradataFromString((String) sortsObj); 447 448 if (!sorts.isEmpty()) 449 { 450 AttributesImpl sortsAttributes = new AttributesImpl(); 451 Object overrideSorts = data.get("overrideSorts"); 452 if (null != overrideSorts && (Boolean) overrideSorts) 453 { 454 sortsAttributes.addCDATAAttribute("override", "true"); 455 } 456 XMLUtils.startElement(handler, "sorts", sortsAttributes); 457 458 for (Map.Entry<String, String> sort : sorts.entrySet()) 459 { 460 AttributesImpl sortAttributes = new AttributesImpl(); 461 if (sort.getValue() != null) 462 { 463 sortAttributes.addCDATAAttribute("order", sort.getValue()); 464 } 465 XMLUtils.startElement(handler, "sort", sortAttributes); 466 XMLUtils.data(handler, sort.getKey()); 467 XMLUtils.endElement(handler, "sort"); 468 } 469 470 XMLUtils.endElement(handler, "sorts"); 471 } 472 } 473 474 private Map<String, String> _splitDataAndExtradataFromString(String str) 475 { 476 Map<String, String> result = new LinkedHashMap<>(); 477 478 for (String data : str.split(ExtractionConstants.STRING_COLLECTIONS_INPUT_DELIMITER)) 479 { 480 Pattern pattern = Pattern.compile(EXTRACT_EXTRA_DATA_REGEX); 481 Matcher matcher = pattern.matcher(data); 482 483 String extra = null; 484 if (matcher.find()) 485 { 486 extra = matcher.group(1); 487 } 488 489 String finalData = data; 490 if (null != extra) 491 { 492 extra = extra.trim(); 493 int indexOfExtra = data.indexOf("("); 494 finalData = data.substring(0, indexOfExtra); 495 } 496 497 result.put(finalData.trim(), extra); 498 } 499 500 return result; 501 } 502 503}