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