/* * RHQ Management Platform * Copyright (C) 2005-2011 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.sync; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.GZIPInputStream; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.sync.ConsistencyValidatorFailureReport; import org.rhq.core.domain.sync.ExportReport; import org.rhq.core.domain.sync.ExportWrapper; import org.rhq.core.domain.sync.ExporterMessages; import org.rhq.core.domain.sync.ImportConfiguration; import org.rhq.core.domain.sync.ImportConfigurationDefinition; import org.rhq.core.domain.sync.ImportReport; import org.rhq.core.util.stream.StreamUtil; import org.rhq.enterprise.server.RHQConstants; import org.rhq.enterprise.server.sync.importers.ExportedEntityMatcher; import org.rhq.enterprise.server.sync.importers.Importer; import org.rhq.enterprise.server.sync.validators.ConsistencyValidator; import org.rhq.enterprise.server.sync.validators.EntityValidator; import org.rhq.enterprise.server.sync.validators.InconsistentStateException; import org.rhq.enterprise.server.xmlschema.ConfigurationInstanceDescriptorUtil; /** * * * @author Lukas Krejci */ @Stateless public class SynchronizationManagerBean implements SynchronizationManagerLocal, SynchronizationManagerRemote { private static final Log LOG = LogFactory.getLog(SynchronizationManagerBean.class); @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME) private EntityManager entityManager; private JAXBContext defaultImportConfigurationJAXBContext; private SynchronizerFactory synchronizerFactory = new SynchronizerFactory(); public SynchronizationManagerBean() { try { defaultImportConfigurationJAXBContext = JAXBContext.newInstance(DefaultImportConfigurationDescriptor.class); } catch (JAXBException e) { throw new IllegalStateException("Failed to create DefaultImportConfigurationDescriptor unmarshaller. This should never happen."); } } //for test purposes @Override public void setSynchronizerFactory(SynchronizerFactory factory) { this.synchronizerFactory = factory; } @Override @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public ExportReport exportAllSubsystems(Subject subject) { ExportWrapper localExport = exportAllSubsystemsLocally(subject); byte[] buffer = new byte[65536]; ByteArrayOutputStream out = new ByteArrayOutputStream(10240); //10KB is a reasonable minimum size of an export try { int cnt = 0; while ((cnt = localExport.getExportFile().read(buffer)) != -1) { out.write(buffer, 0, cnt); } return new ExportReport(localExport.getMessagesPerExporter(), out.toByteArray()); } catch (Exception e) { return new ExportReport(e.getMessage()); } finally { try { out.close(); } catch (Exception e) { //this doesn't happen - out is backed by just an array LOG.error("Closing a byte array output stream failed. This should never happen."); } try { localExport.getExportFile().close(); } catch (Exception e) { LOG.warn("Failed to close the export file stream.", e); } } } @Override @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) public ExportWrapper exportAllSubsystemsLocally(Subject subject) { Set<Synchronizer<?, ?>> allSynchronizers = getInitializedSynchronizers(subject); Map<String, ExporterMessages> messages = new HashMap<String, ExporterMessages>(); try { return new ExportWrapper(messages, new ExportingInputStream(allSynchronizers, messages)); } catch (IOException e) { throw new IllegalStateException("Failed to initialize the export.", e); } } @Override public ImportReport importAllSubsystems(Subject subject, InputStream exportFile, List<ImportConfiguration> configurations) throws ValidationException, ImportException { File tmpFile = null; FileOutputStream tmpFileOut = null; try { tmpFile = File.createTempFile("rhq-synchronization", "tmp"); tmpFileOut = new FileOutputStream(tmpFile); StreamUtil.copy(exportFile, tmpFileOut); tmpFileOut.close(); } catch (IOException e) { throw new ImportException("Failed to copy the exportFile to a temporary location.", e); } finally { StreamUtil.safeClose(tmpFileOut); } InputStream in = null; try { Map<String, Configuration> configsPerImports = getConfigPerImporter(configurations); in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(tmpFile))); validateExport(subject, in, configsPerImports); in.close(); in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(tmpFile))); return importExportFile(subject, in, configsPerImports); } catch (XMLStreamException e) { throw new ImportException("Failed import due to XML parsing error.", e); } catch (IOException e) { throw new ImportException("The provided file is not a gzipped XML.", e); } finally { StreamUtil.safeClose(in); tmpFile.delete(); } } @Override public ImportReport importAllSubsystems(Subject subject, byte[] exportFile, List<ImportConfiguration> configurations) throws ValidationException, ImportException { return importAllSubsystems(subject, new ByteArrayInputStream(exportFile), configurations); } @Override public void validate(Subject subject, InputStream exportFile) throws ValidationException { try { validateExport(subject, exportFile, Collections.<String, Configuration>emptyMap()); } catch (XMLStreamException e) { throw new ValidationException("Failed to parse the export file.", e); } } @Override public void validate(Subject subject, byte[] exportFile) throws ValidationException { validate(subject, new ByteArrayInputStream(exportFile)); } @Override public ImportConfigurationDefinition getImportConfigurationDefinition(String synchronizerClass) { try { Class<?> cls = Class.forName(synchronizerClass); if (!Synchronizer.class.isAssignableFrom(cls)) { LOG.debug("Supplied synchronizer class does not implement the synchronizer interface: '" + synchronizerClass + "'."); return null; } Synchronizer<?, ?> syn = (Synchronizer<?, ?>) cls.newInstance(); return new ImportConfigurationDefinition(synchronizerClass, syn.getImporter().getImportConfigurationDefinition()); } catch (ClassNotFoundException e) { LOG.debug("Supplied synchronizer class is invalid: '" + synchronizerClass + "'.", e); return null; } catch (Exception e) { LOG.error("Failed to instantiate the synchronizer '" + synchronizerClass + "'. This should not happen."); throw new IllegalStateException("Failed to instantiate synchronizer '" + synchronizerClass + ".", e); } } @Override public List<ImportConfigurationDefinition> getImportConfigurationDefinitionOfAllSynchronizers() { List<ImportConfigurationDefinition> ret = new ArrayList<ImportConfigurationDefinition>(); for (Synchronizer<?, ?> syn : synchronizerFactory.getAllSynchronizers()) { ret.add(new ImportConfigurationDefinition(syn.getClass().getName(), syn.getImporter() .getImportConfigurationDefinition())); } return ret; } private void validateExport(Subject subject, InputStream exportFile, Map<String, Configuration> importConfigs) throws ValidationException, XMLStreamException { XMLStreamReader rdr = XMLInputFactory.newInstance().createXMLStreamReader(exportFile); try { Set<ConsistencyValidatorFailureReport> failures = new HashSet<ConsistencyValidatorFailureReport>(); Set<ConsistencyValidator> consistencyValidators = new HashSet<ConsistencyValidator>(); while (rdr.hasNext()) { switch (rdr.next()) { case XMLStreamConstants.START_ELEMENT: String tagName = rdr.getName().getLocalPart(); if (SynchronizationConstants.VALIDATOR_ELEMENT.equals(tagName)) { ConsistencyValidator validator = null; String validatorClass = rdr.getAttributeValue(null, SynchronizationConstants.CLASS_ATTRIBUTE); if (!isConsistencyValidatorClass(validatorClass)) { LOG.info("The export file contains an unknown consistency validator: " + validatorClass + ". Ignoring."); continue; } try { validator = validateSingle(rdr, subject); } catch (Exception e) { failures.add(new ConsistencyValidatorFailureReport(validatorClass, printExceptionToString(e))); } if (validator != null) { consistencyValidators.add(validator); } } else if (SynchronizationConstants.ENTITIES_EXPORT_ELEMENT.equals(tagName)) { String synchronizerClass = rdr.getAttributeValue(null, SynchronizationConstants.ID_ATTRIBUTE); try { failures.addAll(validateEntities(rdr, subject, consistencyValidators, importConfigs)); } catch (Exception e) { throw new ValidationException( "Validation failed unexpectedly while processing the entities exported by the synchronizer '" + synchronizerClass + "'.", e); } } } } if (!failures.isEmpty()) { throw new ValidationException(failures); } } finally { rdr.close(); } } /** * @param validatorClass * @return */ private boolean isConsistencyValidatorClass(String validatorClass) { try { Class<?> cls = Class.forName(validatorClass); return ConsistencyValidator.class.isAssignableFrom(cls); } catch (Exception e) { return false; } } private <E, X> Set<ConsistencyValidatorFailureReport> validateEntities(XMLStreamReader rdr, Subject subject, Set<ConsistencyValidator> consistencyValidators, Map<String, Configuration> importConfigurations) throws Exception { String synchronizerClass = rdr.getAttributeValue(null, SynchronizationConstants.ID_ATTRIBUTE); HashSet<ConsistencyValidatorFailureReport> ret = new HashSet<ConsistencyValidatorFailureReport>(); @SuppressWarnings("unchecked") Synchronizer<E, X> synchronizer = instantiate(synchronizerClass, Synchronizer.class, "The id attribute of entities doesn't correspond to a class implementing the Synchronizer interface."); synchronizer.initialize(subject, entityManager); Importer<E, X> importer = synchronizer.getImporter(); Set<ConsistencyValidator> requriedConsistencyValidators = synchronizer.getRequiredValidators(); //check that all the required consistency validators were run for(ConsistencyValidator v : requriedConsistencyValidators) { if (!consistencyValidators.contains(v)) { ret.add(new ConsistencyValidatorFailureReport(v.getClass().getName(), "The validator '" + v.getClass().getName() + "' is required by the synchronizer '" + synchronizerClass + "' but was not found in the export file.")); } } //don't bother checking if there are inconsistencies in the export file if (!ret.isEmpty()) { return ret; } boolean configured = false; Configuration importConfiguration = importConfigurations.get(synchronizerClass); Set<EntityValidator<X>> validators = null; //the passed in configuration has precedence over the default one inlined in //the config file. if (importConfiguration != null) { importer.configure(importConfiguration); validators = importer.getEntityValidators(); for(EntityValidator<X> v : validators) { v.initialize(subject, entityManager); } configured = true; } while (rdr.hasNext()) { boolean bailout = false; switch (rdr.next()) { case XMLStreamConstants.START_ELEMENT: if (SynchronizationConstants.DEFAULT_CONFIGURATION_ELEMENT.equals(rdr.getName().getLocalPart())) { if (!configured) { importConfiguration = getDefaultConfiguration(rdr); } } else if (SynchronizationConstants.DATA_ELEMENT.equals(rdr.getName().getLocalPart())) { //first check if the configure method has been called if (!configured) { importer.configure(importConfiguration); validators = importer.getEntityValidators(); for(EntityValidator<X> v : validators) { v.initialize(subject, entityManager); } configured = true; } //now do the validation rdr.nextTag(); X exportedEntity = importer.unmarshallExportedEntity(new ExportReader(rdr)); for (EntityValidator<X> validator : validators) { try { validator.validateExportedEntity(exportedEntity); } catch (Exception e) { ValidationException v = new ValidationException("Failed to validate entity [" + exportedEntity + "]", e); ret.add(new ConsistencyValidatorFailureReport(validator.getClass().getName(), printExceptionToString(v))); } } } break; case XMLStreamConstants.END_ELEMENT: if (SynchronizationConstants.ENTITIES_EXPORT_ELEMENT.equals(rdr.getName().getLocalPart())) { bailout = true; } } if (bailout) { break; } } return ret; } private ImportReport importExportFile(Subject subject, InputStream exportFile, Map<String, Configuration> importerConfigs) throws ImportException, XMLStreamException { XMLStreamReader rdr = XMLInputFactory.newInstance().createXMLStreamReader(exportFile); ImportReport report = new ImportReport(); while (rdr.hasNext()) { switch (rdr.next()) { case XMLStreamReader.START_ELEMENT: String tagName = rdr.getName().getLocalPart(); if (SynchronizationConstants.ENTITIES_EXPORT_ELEMENT.equals(tagName)) { try { String synchronizer = rdr.getAttributeValue(null, SynchronizationConstants.ID_ATTRIBUTE); String notes = importSingle(subject, importerConfigs, rdr); if (notes != null) { report.getImporterNotes().put(synchronizer, notes); } } catch (Exception e) { //fail fast on the import errors... This runs in a single transaction //so all imports done so far will get rolled-back. //(Even if we change our minds later and run a transaction per importer //we should fail fast to prevent further damage due to possible //constraint violations in the db, etc.) throw new ImportException("Import failed.", e); } } break; } } return report; } private ConsistencyValidator validateSingle(XMLStreamReader rdr, Subject subject) throws InstantiationException, IllegalAccessException, ClassNotFoundException, XMLStreamException, InconsistentStateException { String validatorClassName = rdr.getAttributeValue(null, SynchronizationConstants.CLASS_ATTRIBUTE); ConsistencyValidator validator = instantiate( validatorClassName, ConsistencyValidator.class, "The validator class denoted in the export file ('%s') does not implement the ConsistencyValidator interface. This should not happen."); //init the validator validator.initialize(subject, entityManager); //perform the validation validator.initializeExportedStateValidation(new ExportReader(rdr)); validator.validateExportedState(); return validator; } private <E, X> String importSingle(Subject subject, Map<String, Configuration> importConfigs, XMLStreamReader rdr) throws Exception { String synchronizerClassName = rdr.getAttributeValue(null, SynchronizationConstants.ID_ATTRIBUTE); @SuppressWarnings("unchecked") Synchronizer<E, X> synchronizer = instantiate(synchronizerClassName, Synchronizer.class, "The synchronizer denoted in the export file ('%s') does not implement the importer interface. This should not happen."); synchronizer.initialize(subject, entityManager); Importer<E, X> importer = synchronizer.getImporter(); ExportedEntityMatcher<E, X> matcher = null; //this will be initialized once the importer is configured boolean configured = false; Configuration importConfiguration = importConfigs.get(synchronizerClassName); //the passed in configuration has precedence over the default one inlined in //the config file. if (importConfiguration != null) { importer.configure(importConfiguration); matcher = importer.getExportedEntityMatcher(); configured = true; } while (rdr.hasNext()) { boolean bailout = false; switch (rdr.next()) { case XMLStreamConstants.START_ELEMENT: if (SynchronizationConstants.DEFAULT_CONFIGURATION_ELEMENT.equals(rdr.getName().getLocalPart())) { if (!configured) { importConfiguration = getDefaultConfiguration(rdr); } } else if (SynchronizationConstants.DATA_ELEMENT.equals(rdr.getName().getLocalPart())) { //first check if the configure method has been called if (!configured) { importer.configure(importConfiguration); matcher = importer.getExportedEntityMatcher(); configured = true; } //now do the import rdr.nextTag(); X exportedEntity = importer.unmarshallExportedEntity(new ExportReader(rdr)); E entity = matcher == null ? null : matcher.findMatch(exportedEntity); importer.update(entity, exportedEntity); } break; case XMLStreamConstants.END_ELEMENT: if (SynchronizationConstants.ENTITIES_EXPORT_ELEMENT.equals(rdr.getName().getLocalPart())) { bailout = true; } } if (bailout) { break; } } //we might have had no data and because we configure the importer lazily, it might //be left unconfigured by the above loop. if (!configured) { importer.configure(importConfiguration); } return importer.finishImport(); } private static String printExceptionToString(Throwable t) { StringWriter str = new StringWriter(); PrintWriter wrt = new PrintWriter(str); t.printStackTrace(wrt); return str.toString(); } private <T> T instantiate(String className, Class<T> desiredClass, String notAssignableErrorMessage) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Class<?> cls = Class.forName(className); if (!desiredClass.isAssignableFrom(cls)) { throw new IllegalStateException(String.format(notAssignableErrorMessage, className, desiredClass.getName())); } Object instance = cls.newInstance(); return desiredClass.cast(instance); } private Set<Synchronizer<?, ?>> getInitializedSynchronizers(Subject subject) { Set<Synchronizer<?, ?>> ret = synchronizerFactory.getAllSynchronizers(); for(Synchronizer<?, ?> s : ret) { s.initialize(subject, entityManager); } return ret; } private Map<String, Configuration> getConfigPerImporter(List<ImportConfiguration> list) { Map<String, Configuration> ret = new HashMap<String, Configuration>(); if (list != null) { for (ImportConfiguration ic : list) { ret.put(ic.getSynchronizerClassName(), ic.getConfiguration()); } } return ret; } private Configuration getDefaultConfiguration(XMLStreamReader rdr) throws JAXBException { Unmarshaller unmarshaller = defaultImportConfigurationJAXBContext.createUnmarshaller(); DefaultImportConfigurationDescriptor descriptor = (DefaultImportConfigurationDescriptor) unmarshaller.unmarshal(rdr); ConfigurationInstanceDescriptorUtil.ConfigurationAndDefinition ccd = ConfigurationInstanceDescriptorUtil.createConfigurationAndDefinition(descriptor); return ccd.configuration; } }