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.Disposable; 040import org.apache.avalon.framework.activity.Initializable; 041import org.apache.avalon.framework.component.Component; 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.xml.serializer.OutputPropertiesFactory; 048import org.xml.sax.ContentHandler; 049import org.xml.sax.SAXException; 050 051import org.ametys.core.ObservationConstants; 052import org.ametys.core.datasource.DataSourceConsumer.TypeOfUse; 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.model.checker.ItemCheckerTestFailureException; 059import org.ametys.runtime.plugin.PluginsManager; 060import org.ametys.runtime.plugin.component.AbstractLogEnabled; 061 062/** 063 * Abstract component to handle data source 064 */ 065public abstract class AbstractDataSourceManager extends AbstractLogEnabled implements Component, Initializable, Serviceable, Disposable 066{ 067 /** The suffix of any default data source */ 068 public static final String DEFAULT_DATASOURCE_SUFFIX = "default-datasource"; 069 070 /** The observation manager */ 071 protected ObservationManager _observationManager; 072 /** The current user provider */ 073 protected CurrentUserProvider _currentUserProvider; 074 075 /** The data source definitions */ 076 protected Map<String, DataSourceDefinition> _dataSourcesDef; 077 078 private long _lastUpdate; 079 080 private DataSourceConsumerExtensionPoint _dataSourceConsumerEP; 081 082 @Override 083 public void service(ServiceManager serviceManager) throws ServiceException 084 { 085 _dataSourceConsumerEP = (DataSourceConsumerExtensionPoint) serviceManager.lookup(DataSourceConsumerExtensionPoint.ROLE); 086 try 087 { 088 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 089 } 090 catch (ServiceException e) 091 { 092 // Not a safe component... ignore it 093 } 094 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 095 } 096 097 @Override 098 public void initialize() throws Exception 099 { 100 _dataSourcesDef = new HashMap<>(); 101 102 readConfiguration(); 103 104 // Check parameters and create data source 105 for (DataSourceDefinition def : _dataSourcesDef.values()) 106 { 107 try 108 { 109 // Validate the data source 110 checkParameters(def.getParameters()); 111 } 112 catch (ItemCheckerTestFailureException e) 113 { 114 TypeOfUse typeOfUse = TypeOfUse.merge(_dataSourceConsumerEP.isInUse(def.getId()), def.isDefault() ? _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()) : TypeOfUse.NOT_USED); 115 if (!PluginsManager.getInstance().isSafeMode() && typeOfUse == TypeOfUse.BLOCKING) 116 { 117 // If not already in safe mode, go to safe mode for blocking use 118 throw e; 119 } 120 else 121 { 122 getLogger().warn("The data source '{}' is currently invalid", def.getId(), e); 123 } 124 } 125 126 // Create the data source 127 createDataSource (def); 128 } 129 130 if (getDefaultDataSourceDefinition() == null) 131 { 132 // Force a default data source at start-up if not present 133 internalSetDefaultDataSource(); 134 } 135 136 checkDataSources(); 137 } 138 139 /** 140 * Get the file configuration of data sources 141 * @return the file 142 */ 143 public abstract File getFileConfiguration(); 144 145 /** 146 * Get the prefix for data source identifier 147 * @return the id prefix 148 */ 149 protected abstract String getDataSourcePrefixId(); 150 151 /** 152 * Checks the parameters of a data source 153 * @param parameters the parameters of the data source 154 * @throws ItemCheckerTestFailureException if the test failed 155 */ 156 public abstract void checkParameters(Map<String, Object> parameters) throws ItemCheckerTestFailureException; 157 158 /** 159 * Creates a data source from its configuration 160 * @param dataSource the data source configuration 161 */ 162 protected abstract void createDataSource(DataSourceDefinition dataSource); 163 164 /** 165 * Edit a data source from its configuration 166 * @param dataSource the data source configuration 167 */ 168 protected abstract void editDataSource(DataSourceDefinition dataSource); 169 170 /** 171 * Deletes a data source 172 * @param dataSource the data source configuration 173 */ 174 protected abstract void deleteDataSource(DataSourceDefinition dataSource); 175 176 /** 177 * Set a default data source internally 178 */ 179 protected abstract void internalSetDefaultDataSource(); 180 181 /** 182 * Get the data source definitions 183 * @param includePrivate true to include private data sources 184 * @param includeInternal true to include internal data sources. Not used by default. 185 * @param includeDefault true to include an additional data source definition for each default data source 186 * @return the data source definitions 187 */ 188 public Map<String, DataSourceDefinition> getDataSourceDefinitions(boolean includePrivate, boolean includeInternal, boolean includeDefault) 189 { 190 readConfiguration(); 191 192 Map<String, DataSourceDefinition> dataSourceDefinitions = new HashMap<> (); 193 if (includeDefault) 194 { 195 DataSourceDefinition defaultDataSourceDefinition = getDefaultDataSourceDefinition(); 196 if (defaultDataSourceDefinition != null) 197 { 198 dataSourceDefinitions.put(getDefaultDataSourceId(), defaultDataSourceDefinition); 199 } 200 } 201 202 if (includePrivate) 203 { 204 dataSourceDefinitions.putAll(_dataSourcesDef); 205 return dataSourceDefinitions; 206 } 207 else 208 { 209 Map<String, DataSourceDefinition> publicDatasources = new HashMap<>(); 210 for (DataSourceDefinition definition : _dataSourcesDef.values()) 211 { 212 if (!definition.isPrivate()) 213 { 214 publicDatasources.put(definition.getId(), definition); 215 } 216 } 217 218 dataSourceDefinitions.putAll(publicDatasources); 219 return dataSourceDefinitions; 220 } 221 } 222 223 /** 224 * Get the data source definition or null if not found 225 * @param id the id of data source 226 * @return the data source definition or null if not found 227 */ 228 public DataSourceDefinition getDataSourceDefinition(String id) 229 { 230 readConfiguration(); 231 232 if (getDefaultDataSourceId().equals(id)) 233 { 234 return getDefaultDataSourceDefinition(); 235 } 236 237 return _dataSourcesDef.get(id); 238 } 239 240 /** 241 * Add a data source 242 * @param name the name 243 * @param description the description 244 * @param parameters the parameters 245 * @param isPrivate true if private 246 * @return the created data source definition 247 */ 248 public DataSourceDefinition add(I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate) 249 { 250 readConfiguration(); 251 252 String id = getDataSourcePrefixId() + StringUtils.generateKey(); 253 DataSourceDefinition ds = new DataSourceDefinition(id, name, description, parameters, isPrivate, false); 254 _dataSourcesDef.put(id, ds); 255 256 saveConfiguration(); 257 258 createDataSource(ds); 259 260 if (getDataSourceDefinitions(true, true, false).size() == 1) 261 { 262 internalSetDefaultDataSource(); 263 } 264 265 if (_observationManager != null) 266 { 267 Map<String, Object> eventParams = new HashMap<>(); 268 eventParams.put(ObservationConstants.ARGS_DATASOURCE_IDS, Collections.singletonList(ds.getId())); 269 _observationManager.notify(new Event(ObservationConstants.EVENT_DATASOURCE_ADDED, _currentUserProvider.getUser(), eventParams)); 270 } 271 272 return ds; 273 } 274 275 /** 276 * Edit a data source 277 * @param id the id 278 * @param name the name 279 * @param description the description 280 * @param parameters the parameters 281 * @param isPrivate true if private 282 * @return the edited data source definition 283 */ 284 public DataSourceDefinition edit(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate) 285 { 286 readConfiguration(); 287 288 if (_dataSourcesDef.containsKey(id)) 289 { 290 boolean isDefault = _dataSourcesDef.get(id).isDefault(); 291 DataSourceDefinition ds = new DataSourceDefinition(id, name, description, parameters, 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 && TypeOfUse.merge(_dataSourceConsumerEP.isInUse(id), dataSourceDef.isDefault() ? _dataSourceConsumerEP.isInUse(getDefaultDataSourceId()) : TypeOfUse.NOT_USED) != TypeOfUse.NOT_USED) 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 final void readConfiguration() 435 { 436 File file = getFileConfiguration(); 437 if (file.exists() && file.lastModified() > _lastUpdate) 438 { 439 _lastUpdate = new Date().getTime(); 440 _dataSourcesDef = doReadConfiguration(file); 441 } 442 } 443 444 /** 445 * Actually read configuration. 446 * @param file the definitions file. 447 * @return all definitions. 448 */ 449 protected abstract Map<String, DataSourceDefinition> doReadConfiguration(File file); 450 451 /** 452 * Save the configured data sources 453 */ 454 protected void saveConfiguration() 455 { 456 File file = getFileConfiguration(); 457 try 458 { 459 // Create file if it does not already exist 460 if (!file.exists()) 461 { 462 file.getParentFile().mkdirs(); 463 file.createNewFile(); 464 } 465 466 // create a transformer for saving sax into a file 467 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 468 469 // create the result where to write 470 try (OutputStream os = new FileOutputStream(file)) 471 { 472 StreamResult sResult = new StreamResult(os); 473 th.setResult(sResult); 474 475 // create the format of result 476 Properties format = new Properties(); 477 format.put(OutputKeys.METHOD, "xml"); 478 format.put(OutputKeys.INDENT, "yes"); 479 format.put(OutputKeys.ENCODING, "UTF-8"); 480 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4"); 481 th.getTransformer().setOutputProperties(format); 482 483 // Send SAX events 484 th.startDocument(); 485 XMLUtils.startElement(th, "datasources"); 486 487 for (DataSourceDefinition datasource : _dataSourcesDef.values()) 488 { 489 saxDataSource(th, datasource); 490 } 491 492 XMLUtils.endElement(th, "datasources"); 493 th.endDocument(); 494 } 495 } 496 catch (SAXException | IOException | TransformerConfigurationException e) 497 { 498 throw new RuntimeException("Unable to save the configuration of data sources", e); 499 } 500 } 501 502 /** 503 * SAX an instance of data source 504 * @param handler the content handler to sax into 505 * @param dataSource the data source 506 * @throws SAXException if an error occurred while SAXing 507 */ 508 protected void saxDataSource(ContentHandler handler, DataSourceDefinition dataSource) throws SAXException 509 { 510 AttributesImpl attrs = new AttributesImpl(); 511 512 attrs.addCDATAAttribute("id", dataSource.getId()); 513 attrs.addCDATAAttribute("private", String.valueOf(dataSource.isPrivate())); 514 attrs.addCDATAAttribute("default", String.valueOf(dataSource.isDefault())); 515 516 XMLUtils.startElement(handler, "datasource", attrs); 517 518 dataSource.getName().toSAX(handler, "name"); 519 dataSource.getDescription().toSAX(handler, "description"); 520 521 XMLUtils.startElement(handler, "parameters"); 522 Map<String, Object> parameters = dataSource.getParameters(); 523 for (String paramName : parameters.keySet()) 524 { 525 Object value = parameters.get(paramName); 526 XMLUtils.createElement(handler, paramName, value != null ? value.toString() : ""); 527 } 528 XMLUtils.endElement(handler, "parameters"); 529 530 XMLUtils.endElement(handler, "datasource"); 531 } 532 533 /** 534 * Check that the used data sources are indeed available 535 */ 536 protected void checkDataSources() 537 { 538 Set<String> usedDataSourceIds = _dataSourceConsumerEP.getUsedDataSourceIds().keySet(); 539 for (String dataSourceId : usedDataSourceIds) 540 { 541 if (dataSourceId != null && dataSourceId.startsWith(getDataSourcePrefixId()) && getDataSourceDefinition(dataSourceId) == null && !PluginsManager.getInstance().isSafeMode()) 542 { 543 throw new UnknownDataSourceException("The data source '" + dataSourceId + "' was not found in the available data sources."); 544 } 545 } 546 } 547 548 public void dispose() 549 { 550 for (DataSourceDefinition ds : _dataSourcesDef.values()) 551 { 552 deleteDataSource(ds); 553 } 554 555 _dataSourcesDef.clear(); 556 _lastUpdate = 0; 557 } 558 559 /** 560 * This class represents the definition of a data source 561 */ 562 public static class DataSourceDefinition implements Cloneable 563 { 564 private String _id; 565 private I18nizableText _name; 566 private I18nizableText _description; 567 private Map<String, Object> _parameters; 568 private boolean _isPrivate; 569 private boolean _isDefault; 570 571 /** 572 * Constructor 573 * @param id the id 574 * @param name the name 575 * @param description the description 576 * @param parameters the parameters 577 * @param isPrivate true if the data source is a private data source 578 * @param isDefault true if the data source is a default data source 579 */ 580 public DataSourceDefinition(String id, I18nizableText name, I18nizableText description, Map<String, Object> parameters, boolean isPrivate, boolean isDefault) 581 { 582 _id = id; 583 _name = name; 584 _description = description; 585 _parameters = parameters; 586 _isPrivate = isPrivate; 587 _isDefault = isDefault; 588 } 589 590 /** 591 * The id of the data source 592 * @return the id of the data source 593 */ 594 public String getId() 595 { 596 return _id; 597 } 598 599 /** 600 * Get the name of the data source 601 * @return the name of the data source 602 */ 603 public I18nizableText getName() 604 { 605 return _name; 606 } 607 608 /** 609 * Get the description of the data source 610 * @return the description of the data source 611 */ 612 public I18nizableText getDescription() 613 { 614 return _description; 615 } 616 617 /** 618 * Returns true if this data source instance is private 619 * @return true if is private 620 */ 621 public boolean isPrivate() 622 { 623 return _isPrivate; 624 } 625 626 /** 627 * Returns true if this is a default data source 628 * @return true if this is a default data source 629 */ 630 public boolean isDefault() 631 { 632 return _isDefault; 633 } 634 635 /** 636 * Set default or not this data source 637 * @param isDefault true to set this data source as the default one, false otherwise 638 */ 639 public void setDefault(boolean isDefault) 640 { 641 _isDefault = isDefault; 642 } 643 644 /** 645 * Get the parameters of the data source definition 646 * @return the parameters 647 */ 648 public Map<String, Object> getParameters() 649 { 650 return _parameters; 651 } 652 653 /** 654 * Duplicate the object 655 * @return The duplicated object 656 */ 657 public DataSourceDefinition duplicate() 658 { 659 return new DataSourceDefinition(_id, _name, _description, new HashMap<>(_parameters), _isPrivate, _isDefault); 660 } 661 } 662}