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