/*
* 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;
}
}