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