001/* 002 * Copyright 2016 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.core.datasource; 017 018import java.io.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.Date; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Properties; 030import java.util.Set; 031 032import javax.xml.transform.OutputKeys; 033import javax.xml.transform.TransformerConfigurationException; 034import javax.xml.transform.TransformerFactory; 035import javax.xml.transform.sax.SAXTransformerFactory; 036import javax.xml.transform.sax.TransformerHandler; 037import javax.xml.transform.stream.StreamResult; 038 039import org.apache.avalon.framework.activity.Initializable; 040import org.apache.avalon.framework.component.Component; 041import org.apache.avalon.framework.configuration.Configuration; 042import org.apache.avalon.framework.configuration.ConfigurationException; 043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 044import org.apache.avalon.framework.service.ServiceException; 045import org.apache.avalon.framework.service.ServiceManager; 046import org.apache.avalon.framework.service.Serviceable; 047import org.apache.cocoon.xml.AttributesImpl; 048import org.apache.cocoon.xml.XMLUtils; 049import org.xml.sax.ContentHandler; 050import org.xml.sax.SAXException; 051 052import org.ametys.core.ObservationConstants; 053import org.ametys.core.observation.Event; 054import org.ametys.core.observation.ObservationManager; 055import org.ametys.core.user.CurrentUserProvider; 056import org.ametys.core.util.StringUtils; 057import org.ametys.runtime.i18n.I18nizableText; 058import org.ametys.runtime.parameter.ParameterCheckerTestFailureException; 059import org.ametys.runtime.parameter.ParameterHelper; 060import org.ametys.runtime.plugin.PluginsManager; 061import org.ametys.runtime.plugin.component.AbstractLogEnabled; 062 063/** 064 * Abstract component to handle data source 065 */ 066public abstract class AbstractDataSourceManager extends AbstractLogEnabled implements Component, Initializable, Serviceable 067{ 068 /** The suffix of any default data source */ 069 public static final String DEFAULT_DATASOURCE_SUFFIX = "default-datasource"; 070 071 /** The observation manager */ 072 protected ObservationManager _observationManager; 073 /** The current user provider */ 074 protected CurrentUserProvider _currentUserProvider; 075 076 /** The data source definitions */ 077 protected Map<String, DataSourceDefinition> _dataSourcesDef; 078 079 private long _lastUpdate; 080 081 private DataSourceConsumerExtensionPoint _dataSourceConsumerEP; 082 083 @Override 084 public void service(ServiceManager serviceManager) throws ServiceException 085 { 086 _dataSourceConsumerEP = (DataSourceConsumerExtensionPoint) serviceManager.lookup(DataSourceConsumerExtensionPoint.ROLE); 087 try 088 { 089 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 090 } 091 catch (ServiceException e) 092 { 093 // Not a safe component... ignore it 094 } 095 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 096 } 097 098 @Override 099 public void initialize() throws Exception 100 { 101 _dataSourcesDef = new HashMap<>(); 102 103 readConfiguration(); 104 105 // Check parameters and create data source 106 for (DataSourceDefinition def : _dataSourcesDef.values()) 107 { 108 // Validate the used data sources if not already in safe mode 109 boolean isInUse = _dataSourceConsumerEP.isInUse(def.getId()) || (def.isDefault() && _dataSourceConsumerEP.isInUse(getDefaultDataSourceId())); 110 if (!PluginsManager.getInstance().isSafeMode() && isInUse) 111 { 112 checkParameters (def.getParameters()); 113 } 114 115 createDataSource (def); 116 } 117 118 if (getDefaultDataSourceDefinition() == null) 119 { 120 // Force a default data source at start-up if not present 121 internalSetDefaultDataSource(); 122 } 123 124 checkDataSources(); 125 } 126 127 /** 128 * Get the file configuration of data sources 129 * @return the file 130 */ 131 public abstract File getFileConfiguration(); 132 133 /** 134 * Get the prefix for data source identifier 135 * @return the id prefix 136 */ 137 protected abstract String getDataSourcePrefixId(); 138 139 /** 140 * Checks the parameters of a data source 141 * @param rawParameters the parameters of the data source 142 * @throws ParameterCheckerTestFailureException if the test failed 143 */ 144 public abstract void checkParameters(Map<String, String> rawParameters) throws ParameterCheckerTestFailureException; 145 146 /** 147 * Creates a data source from its configuration 148 * @param dataSource the data source configuration 149 */ 150 protected abstract void createDataSource(DataSourceDefinition dataSource); 151 152 /** 153 * Edit a data source from its configuration 154 * @param dataSource the data source configuration 155 */ 156 protected abstract void editDataSource(DataSourceDefinition dataSource); 157 158 /** 159 * Deletes a data source 160 * @param dataSource the data source configuration 161 */ 162 protected abstract void deleteDataSource(DataSourceDefinition dataSource); 163 164 /** 165 * Set a default data source internally 166 */ 167 protected abstract void internalSetDefaultDataSource(); 168 169 /** 170 * Get the data source definitions 171 * @param includePrivate true to include private data sources 172 * @param includeInternal true to include internal data sources. Not used by default. 173 * @param includeDefault true to include an additional data source definition for each default data source 174 * @return the data source definitions 175 */ 176 public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault) 177 { 178 readConfiguration(); 179 180 Map<String, DataSourceDefinition> dataSourceDefinitions = new HashMap<> (); 181 if (includeDefault) 182 { 183 DataSourceDefinition defaultDataSourceDefinition = getDefaultDataSourceDefinition(); 184 if (defaultDataSourceDefinition != null) 185 { 186 dataSourceDefinitions.put(getDefaultDataSourceId(), defaultDataSourceDefinition); 187 } 188 } 189 190 if (includePrivate) 191 { 192 dataSourceDefinitions.putAll(_dataSourcesDef); 193 return dataSourceDefinitions; 194 } 195 else 196 { 197 Map<String, DataSourceDefinition> publicDatasources = new HashMap<>(); 198 for (DataSourceDefinition definition : _dataSourcesDef.values()) 199 { 200 if (!definition.isPrivate()) 201 { 202 publicDatasources.put(definition.getId(), definition); 203 } 204 } 205 206 dataSourceDefinitions.putAll(publicDatasources); 207 return dataSourceDefinitions; 208 } 209 } 210 211 /** 212 * Get the data source definition or null if not found 213 * @param id the id of data source 214 * @return the data source definition or null if not found 215 */ 216 public DataSourceDefinition getDataSourceDefinition(String id) 217 { 218 readConfiguration(); 219 220 if (getDefaultDataSourceId().equals(id)) 221 { 222 return getDefaultDataSourceDefinition(); 223 } 224 225 return _dataSourcesDef.get(id); 226 } 227 228 /** 229 * Add a data source 230 * @param name the name 231 * @param description the description 232 * @param parameters the parameters 233 * @param isPrivate true if private 234 * @return the created data source definition 235 */ 236 public DataSourceDefinition add(I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate) 237 { 238 readConfiguration(); 239 240 Map<String, String> rawParameters = new HashMap<>(); 241 for (String paramName : parameters.keySet()) 242 { 243 rawParameters.put(paramName, ParameterHelper.valueToString(parameters.get(paramName))); 244 } 245 246 String id = getDataSourcePrefixId() + StringUtils.generateKey(); 247 DataSourceDefinition ds = new DataSourceDefinition(id, name, description, rawParameters, isPrivate, false); 248 _dataSourcesDef.put(id, ds); 249 250 saveConfiguration(); 251 252 createDataSource(ds); 253 254 if (getDataSourceDefinitions(true, true, false).size() == 1) 255 { 256 internalSetDefaultDataSource(); 257 } 258 259 if (_observationManager != null) 260 { 261 Map<String, Object> eventParams = new HashMap<>(); 262 eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId())); 263 _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_ADDED, _currentUserProvider.getUser(), eventParams)); 264 } 265 266 return ds; 267 } 268 269 /** 270 * Edit a data source 271 * @param id the id 272 * @param name the name 273 * @param description the description 274 * @param parameters the parameters 275 * @param isPrivate true if private 276 * @return the edited data source definition 277 */ 278 public DataSourceDefinition edit(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate) 279 { 280 readConfiguration(); 281 282 if (_dataSourcesDef.containsKey(id)) 283 { 284 Map<String, String> rawParameters = new HashMap<>(); 285 for (String paramName : parameters.keySet()) 286 { 287 rawParameters.put(paramName, ParameterHelper.valueToString(parameters.get(paramName))); 288 } 289 290 boolean isDefault = _dataSourcesDef.get(id).isDefault(); 291 DataSourceDefinition ds = new DataSourceDefinition(id, name, description, rawParameters, isPrivate, isDefault); 292 _dataSourcesDef.put(id, ds); 293 294 saveConfiguration(); 295 296 editDataSource(ds); 297 298 if (_observationManager != null) 299 { 300 Map<String, Object> eventParams = new HashMap<>(); 301 eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId())); 302 _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_UPDATED, _currentUserProvider.getUser(), eventParams)); 303 } 304 305 return ds; 306 } 307 308 throw new RuntimeException("The data source with id '" + id + "' was not found. Unable to edit it."); 309 } 310 311 /** 312 * Delete data sources 313 * @param dataSourceIds the ids of the data sources to delete 314 * @param forceDeletion Force the remove event the datasource seems to be in use 315 */ 316 public void delete(Collection<String> dataSourceIds, boolean forceDeletion) 317 { 318 readConfiguration(); 319 320 for (String id : dataSourceIds) 321 { 322 DataSourceDefinition dataSourceDef = _dataSourcesDef.get(id); 323 if (!forceDeletion && (_dataSourceConsumerEP.isInUse(id) || (dataSourceDef.isDefault() && _dataSourceConsumerEP.isInUse(getDefaultDataSourceId())))) 324 { 325 throw new IllegalStateException("The data source '" + id + "' is currently in use. The deletion process has been aborted."); 326 } 327 328 if (id.equals(SQLDataSourceManager.AMETYS_INTERNAL_DATASOURCE_ID)) 329 { 330 throw new IllegalStateException("The data source '" + id + "' is an internal data source. The deletion process has been aborted."); 331 } 332 333 deleteDataSource (dataSourceDef); 334 _dataSourcesDef.remove(id); 335 } 336 337 saveConfiguration(); 338 339 if (getDataSourceDefinitions(true, true, false).size() == 1) 340 { 341 internalSetDefaultDataSource(); 342 } 343 344 if (_observationManager != null) 345 { 346 Map<String, Object> eventParams = new HashMap<>(); 347 eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, dataSourceIds); 348 _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_DELETED, _currentUserProvider.getUser(), eventParams)); 349 } 350 } 351 352 /** 353 * Set the data source with the given id as the default data source 354 * @param id the id of the data source 355 * @return the {@link DataSourceDefinition} of the data source set as default 356 */ 357 public DataSourceDefinition setDefaultDataSource(String id) 358 { 359 readConfiguration(); 360 361 if (!id.startsWith(getDataSourcePrefixId())) 362 { 363 throw new RuntimeException("The data source with id '" + id + "' is not of the appropriate type to set is as default."); 364 } 365 366 // Remove the default attribute from the previous default data source (if any) 367 DataSourceDefinition oldDefaultDataSource = getDefaultDataSourceDefinition(); 368 if (oldDefaultDataSource != null) 369 { 370 oldDefaultDataSource.setDefault(false); 371 _dataSourcesDef.put(oldDefaultDataSource.getId(), oldDefaultDataSource); 372 373 saveConfiguration(); 374 editDataSource(oldDefaultDataSource); 375 } 376 377 if (_dataSourcesDef.containsKey(id)) 378 { 379 // Set the data source as the default one 380 DataSourceDefinition newDefaultDataSource = getDataSourceDefinition(id); 381 newDefaultDataSource.setDefault(true); 382 _dataSourcesDef.put(id, newDefaultDataSource); 383 384 saveConfiguration(); 385 editDataSource(newDefaultDataSource); 386 387 return newDefaultDataSource; 388 } 389 390 throw new RuntimeException("The data source with id '" + id + "' was not found. Unable to set it as the default data source."); 391 } 392 393 /** 394 * Get the default data source for this type 395 * @return the definition object of the default data source. Can return null if no datasource is defined. 396 */ 397 public DataSourceDefinition getDefaultDataSourceDefinition() 398 { 399 List<DataSourceDefinition> defaultDataSourceDefinitions = new ArrayList<> (); 400 for (DataSourceDefinition definition : _dataSourcesDef.values()) 401 { 402 if (definition.getId().startsWith(getDataSourcePrefixId()) && definition.isDefault()) 403 { 404 defaultDataSourceDefinitions.add(definition); 405 } 406 } 407 408 if (defaultDataSourceDefinitions.isEmpty()) 409 { 410 return null; 411 } 412 else if (defaultDataSourceDefinitions.size() > 1) 413 { 414 throw new IllegalStateException("Found more than one default data source definition."); 415 } 416 else 417 { 418 return defaultDataSourceDefinitions.get(0); 419 } 420 } 421 422 /** 423 * Get the id of the default data source 424 * @return the id of the default data source 425 */ 426 public String getDefaultDataSourceId() 427 { 428 return getDataSourcePrefixId() + DEFAULT_DATASOURCE_SUFFIX; 429 } 430 431 /** 432 * Read and update the data sources configuration 433 */ 434 protected void readConfiguration() 435 { 436 File file = getFileConfiguration(); 437 if (file.exists() && file.lastModified() > _lastUpdate) 438 { 439 _lastUpdate = new Date().getTime(); 440 _dataSourcesDef = readDataSourceDefinition(file); 441 } 442 } 443 444 /** 445 * Read the read source definitions 446 * @param file The configuration file 447 * @return the data source definitions 448 */ 449 public static Map<String, DataSourceDefinition> readDataSourceDefinition (File file) 450 { 451 Map<String, DataSourceDefinition> definitions = new HashMap<>(); 452 453 try 454 { 455 if (file.exists()) 456 { 457 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(file); 458 for (Configuration dsConfig : configuration.getChildren("datasource")) 459 { 460 String id = dsConfig.getAttribute("id"); 461 462 I18nizableText name = I18nizableText.parseI18nizableText(dsConfig.getChild("name"), "plugin.core"); 463 I18nizableText description = I18nizableText.parseI18nizableText(dsConfig.getChild("description"), "plugin.core", ""); 464 465 boolean isPrivate = dsConfig.getAttributeAsBoolean("private", false); 466 boolean isDefault = dsConfig.getAttributeAsBoolean("default", false); 467 468 Map<String, String> parameters = new HashMap<>(); 469 470 Configuration[] paramsConfig = dsConfig.getChild("parameters").getChildren(); 471 for (Configuration paramConfig : paramsConfig) 472 { 473 String value = paramConfig.getValue(""); 474 parameters.put(paramConfig.getName(), value); 475 } 476 477 DataSourceDefinition dataSource = new DataSourceDefinition(id, name, description, parameters, isPrivate, isDefault); 478 definitions.put(id, dataSource); 479 } 480 } 481 482 return definitions; 483 } 484 catch (IOException | ConfigurationException | SAXException e) 485 { 486 throw new RuntimeException("Unable to parse datasource configuration file.", e); 487 } 488 } 489 490 /** 491 * Save the configured data sources 492 */ 493 protected void saveConfiguration() 494 { 495 File file = getFileConfiguration(); 496 try 497 { 498 // Create file if it does not already exist 499 if (!file.exists()) 500 { 501 file.getParentFile().mkdirs(); 502 file.createNewFile(); 503 } 504 505 // create a transformer for saving sax into a file 506 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 507 508 // create the result where to write 509 try (OutputStream os = new FileOutputStream(file)) 510 { 511 StreamResult sResult = new StreamResult(os); 512 th.setResult(sResult); 513 514 // create the format of result 515 Properties format = new Properties(); 516 format.put(OutputKeys.METHOD, "xml"); 517 format.put(OutputKeys.INDENT, "yes"); 518 format.put(OutputKeys.ENCODING, "UTF-8"); 519 th.getTransformer().setOutputProperties(format); 520 521 // Send SAX events 522 th.startDocument(); 523 XMLUtils.startElement(th, "datasources"); 524 525 for (DataSourceDefinition datasource : _dataSourcesDef.values()) 526 { 527 saxDataSource(th, datasource); 528 } 529 530 XMLUtils.endElement(th, "datasources"); 531 th.endDocument(); 532 } 533 } 534 catch (SAXException | IOException | TransformerConfigurationException e) 535 { 536 throw new RuntimeException("Unable to save the configuration of data sources", e); 537 } 538 } 539 540 /** 541 * SAX an instance of data source 542 * @param handler the content handler to sax into 543 * @param dataSource the data source 544 * @throws SAXException if an error occurred while SAXing 545 */ 546 protected void saxDataSource(ContentHandler handler, DataSourceDefinition dataSource) throws SAXException 547 { 548 AttributesImpl attrs = new AttributesImpl(); 549 550 attrs.addCDATAAttribute("id", dataSource.getId()); 551 attrs.addCDATAAttribute("private", String.valueOf(dataSource.isPrivate())); 552 attrs.addCDATAAttribute("default", String.valueOf(dataSource.isDefault())); 553 554 XMLUtils.startElement(handler, "datasource", attrs); 555 556 dataSource.getName().toSAX(handler, "name"); 557 dataSource.getDescription().toSAX(handler, "description"); 558 559 XMLUtils.startElement(handler, "parameters"); 560 Map<String, String> parameters = dataSource.getParameters(); 561 for (String paramName : parameters.keySet()) 562 { 563 String value = parameters.get(paramName); 564 XMLUtils.createElement(handler, paramName, value != null ? value : ""); 565 } 566 XMLUtils.endElement(handler, "parameters"); 567 568 XMLUtils.endElement(handler, "datasource"); 569 } 570 571 /** 572 * Check that the used data sources are indeed available 573 */ 574 protected void checkDataSources() 575 { 576 Set<String> usedDataSourceIds = _dataSourceConsumerEP.getUsedDataSourceIds(); 577 for (String dataSourceId : usedDataSourceIds) 578 { 579 if (dataSourceId != null && dataSourceId.startsWith(getDataSourcePrefixId()) && getDataSourceDefinition(dataSourceId) == null && !PluginsManager.getInstance().isSafeMode()) 580 { 581 throw new UnknownDataSourceException("The data source '" + dataSourceId + "' was not found in the available data sources."); 582 } 583 } 584 } 585 586 /** 587 * This class represents the definition of a data source 588 */ 589 public static class DataSourceDefinition implements Cloneable 590 { 591 private String _id; 592 private I18nizableText _name; 593 private I18nizableText _description; 594 private Map<String, String> _parameters; 595 private boolean _isPrivate; 596 private boolean _isDefault; 597 598 /** 599 * Constructor 600 * @param id the id 601 * @param name the name 602 * @param description the description 603 * @param parameters the parameters 604 * @param isPrivate true if the data source is a private data source 605 * @param isDefault true if the data source is a default data source 606 */ 607 public DataSourceDefinition(String id, I18nizableText name, I18nizableText description, Map<String, String> parameters, boolean isPrivate, boolean isDefault) 608 { 609 _id = id; 610 _name = name; 611 _description = description; 612 _parameters = parameters; 613 _isPrivate = isPrivate; 614 _isDefault = isDefault; 615 } 616 617 /** 618 * The id of the data source 619 * @return the id of the data source 620 */ 621 public String getId() 622 { 623 return _id; 624 } 625 626 /** 627 * Get the name of the data source 628 * @return the name of the data source 629 */ 630 public I18nizableText getName() 631 { 632 return _name; 633 } 634 635 /** 636 * Get the description of the data source 637 * @return the description of the data source 638 */ 639 public I18nizableText getDescription() 640 { 641 return _description; 642 } 643 644 /** 645 * Returns true if this data source instance is private 646 * @return true if is private 647 */ 648 public boolean isPrivate() 649 { 650 return _isPrivate; 651 } 652 653 /** 654 * Returns true if this is a default data source 655 * @return true if this is a default data source 656 */ 657 public boolean isDefault() 658 { 659 return _isDefault; 660 } 661 662 /** 663 * Set default or not this data source 664 * @param isDefault true to set this data source as the default one, false otherwise 665 */ 666 public void setDefault(boolean isDefault) 667 { 668 _isDefault = isDefault; 669 } 670 671 /** 672 * Get the parameters of the data source definition 673 * @return the parameters 674 */ 675 public Map<String, String> getParameters() 676 { 677 return _parameters; 678 } 679 680 /** 681 * Duplicate the object 682 * @return The duplicated object 683 */ 684 public DataSourceDefinition duplicate() 685 { 686 return new DataSourceDefinition(_id, _name, _description, new HashMap<>(_parameters), _isPrivate, _isDefault); 687 } 688 689 @Override 690 protected Object clone() throws CloneNotSupportedException 691 { 692 // TODO Auto-generated method stub 693 return super.clone(); 694 } 695 } 696}