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