/* * 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.test; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.persistence.EntityManager; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; 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.jmock.Expectations; import org.testng.annotations.Test; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.definition.ConfigurationDefinition; import org.rhq.core.domain.sync.ExporterMessages; import org.rhq.enterprise.server.sync.ExportReader; import org.rhq.enterprise.server.sync.ExportWriter; import org.rhq.enterprise.server.sync.ExportingInputStream; import org.rhq.enterprise.server.sync.ImportException; import org.rhq.enterprise.server.sync.NoSingleEntity; import org.rhq.enterprise.server.sync.SynchronizationConstants; import org.rhq.enterprise.server.sync.Synchronizer; import org.rhq.enterprise.server.sync.exporters.AbstractDelegatingExportingIterator; import org.rhq.enterprise.server.sync.exporters.Exporter; import org.rhq.enterprise.server.sync.exporters.ExportingIterator; import org.rhq.enterprise.server.sync.exporters.JAXBExportingIterator; 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.test.JMockTest; /** * * * @author Lukas Krejci */ @Test public class ExportingInputStreamTest extends JMockTest { private static final Log LOG = LogFactory.getLog(ExportingInputStreamTest.class); private static class ListToStringExporter<T> implements Exporter<NoSingleEntity, T> { List<T> valuesToExport; public static final String NOTE_PREFIX = "Wow, I just exported an item from a list: "; private class Iterator extends AbstractDelegatingExportingIterator<T, T> { public Iterator() { super(valuesToExport.iterator()); } public void export(ExportWriter output) throws XMLStreamException { output.writeStartElement("datum"); output.writeCharacters(getCurrent().toString()); output.writeEndElement(); } public String getNotes() { return NOTE_PREFIX + getCurrent(); } protected T convert(T object) { return object; } } public ListToStringExporter(List<T> valuesToExport) { this.valuesToExport = valuesToExport; } public ExportingIterator<T> getExportingIterator() { return new Iterator(); } public String getNotes() { return valuesToExport.toString(); } } private static class DummyImporter<T> implements Importer<NoSingleEntity, T> { @Override public ConfigurationDefinition getImportConfigurationDefinition() { return null; } @Override public void configure(Configuration importConfiguration) { } @Override public ExportedEntityMatcher<NoSingleEntity, T> getExportedEntityMatcher() { return null; } @Override public void update(NoSingleEntity entity, T exportedEntity) throws Exception { } @Override public T unmarshallExportedEntity(ExportReader reader) throws XMLStreamException { return null; } @Override public String finishImport() throws Exception { return null; } @Override public Set<EntityValidator<T>> getEntityValidators() { return Collections.emptySet(); } } private static class ListToStringSynchronizer<T> implements Synchronizer<NoSingleEntity, T> { private List<T> list; public ListToStringSynchronizer(List<T> list) { this.list = list; } @Override public Exporter<NoSingleEntity, T> getExporter() { return new ListToStringExporter<T>(list); } @Override public Importer<NoSingleEntity, T> getImporter() { return new DummyImporter<T>(); } @Override public Set<ConsistencyValidator> getRequiredValidators() { return Collections.emptySet(); } @Override public void initialize(Subject subject, EntityManager entityManager) { } } private static class StringListSynchronizer extends ListToStringSynchronizer<String> { public StringListSynchronizer(List<String> list) { super(list); } } private static class IntegerListSynchronizer extends ListToStringSynchronizer<Integer> { public IntegerListSynchronizer(List<Integer> list) { super(list); } } public static class Entity { public int value; public Entity(int value) { this.value = value; } } @XmlRootElement(name = "exported-entity") @XmlAccessorType(XmlAccessType.FIELD) public static class ExportedEntity { @XmlAttribute public int property; } public static class JAXBExporter implements Exporter<Entity, ExportedEntity> { private static class JAXBIterator extends JAXBExportingIterator<ExportedEntity, Entity> { public JAXBIterator(java.util.Iterator<Entity> sourceIterator) { super(sourceIterator, ExportedEntity.class); } @Override protected ExportedEntity convert(Entity object) { ExportedEntity ret = new ExportedEntity(); ret.property = object.value; return ret; } @Override public String getNotes() { return null; } } @Override public ExportingIterator<ExportedEntity> getExportingIterator() { List<Entity> data = new ArrayList<Entity>(); for(int i = 0; i < 4; ++i) { data.add(new Entity(i)); } return new JAXBIterator(data.iterator()); } @Override public String getNotes() { return null; } } public static class JAXBImporter implements Importer<Entity, ExportedEntity> { private Unmarshaller unmarshaller; { try { JAXBContext context = JAXBContext.newInstance(ExportedEntity.class); unmarshaller = context.createUnmarshaller(); } catch (JAXBException e) { throw new IllegalStateException(e); } } @Override public ConfigurationDefinition getImportConfigurationDefinition() { return null; } @Override public void configure(Configuration importConfiguration) { } @Override public ExportedEntityMatcher<Entity, ExportedEntity> getExportedEntityMatcher() { return new ExportedEntityMatcher<Entity, ExportedEntity>() { @Override public Entity findMatch(ExportedEntity object) { return new Entity(object.property); } }; } @Override public Set<EntityValidator<ExportedEntity>> getEntityValidators() { return Collections.emptySet(); } @Override public void update(Entity entity, ExportedEntity exportedEntity) throws Exception { entity.value = exportedEntity.property; } @Override public ExportedEntity unmarshallExportedEntity(ExportReader reader) throws XMLStreamException { try { return (ExportedEntity) unmarshaller.unmarshal(reader); } catch (JAXBException e) { throw new XMLStreamException(e); } } @Override public String finishImport() throws Exception { return null; } } public static class JAXBSynchronizer implements Synchronizer<Entity, ExportedEntity> { @Override public void initialize(Subject subject, EntityManager entityManager) { } @Override public Exporter<Entity, ExportedEntity> getExporter() { return new JAXBExporter(); } @Override public Importer<Entity, ExportedEntity> getImporter() { return new JAXBImporter(); } @Override public Set<ConsistencyValidator> getRequiredValidators() { return Collections.emptySet(); } } public void testSucessfulExport() throws Exception { List<String> list1 = Arrays.asList("a", "b", "c"); List<Integer> list2 = Arrays.asList(1, 2, 3); StringListSynchronizer ex1 = new StringListSynchronizer(list1); IntegerListSynchronizer ex2 = new IntegerListSynchronizer(list2); Set<Synchronizer<?, ?>> exporters = this.<Synchronizer<?, ?>>asSet(ex1, ex2); InputStream export = new ExportingInputStream(exporters, new HashMap<String, ExporterMessages>(), 1024, false); String exportContents = readAll(new InputStreamReader(export, "UTF-8")); LOG.info("Export contents:\n" + exportContents); export = new ByteArrayInputStream(exportContents.getBytes("UTF-8")); DocumentBuilder bld = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = bld.parse(export); Element root = doc.getDocumentElement(); assertEquals(SynchronizationConstants.CONFIGURATION_EXPORT_ELEMENT, root.getNodeName()); NodeList entities = root.getElementsByTagName(SynchronizationConstants.ENTITIES_EXPORT_ELEMENT); assertEquals(entities.getLength(), 2, "Unexpected number of entities elements"); Element export1 = (Element) entities.item(0); Element export2 = (Element) entities.item(1); assertEquals(export1.getAttribute("id"), StringListSynchronizer.class.getName()); assertEquals(export2.getAttribute("id"), IntegerListSynchronizer.class.getName()); String[] expectedNotes = new String[] {list1.toString(), list2.toString()}; for(int i = 0, elementIndex = 0; i < root.getChildNodes().getLength(); ++i) { Node node = root.getChildNodes().item(i); if (!(node instanceof Element)) { continue; } Element entitiesElement = (Element) node; assertEquals(entitiesElement.getNodeName(), SynchronizationConstants.ENTITIES_EXPORT_ELEMENT); NodeList errorMessages = entitiesElement.getElementsByTagName(SynchronizationConstants.ERROR_MESSAGE_ELEMENT); assertEquals(errorMessages.getLength(), 0, "Unexpected number of error message elements in an entities export."); Node note = getDirectChildByTagName(entitiesElement, SynchronizationConstants.NOTES_ELEMENT); assertNotNull(note, "Couldn't find exporter notes."); String notesText = ((Element)note).getTextContent(); assertEquals(notesText, expectedNotes[elementIndex], "Unexpected notes for entities."); NodeList entityElements = entitiesElement.getElementsByTagName(SynchronizationConstants.ENTITY_EXPORT_ELEMENT); assertEquals(entityElements.getLength(), 3, "Unexpected number of exported entities."); for(int j = 0; j < entityElements.getLength(); ++j) { Element entityElement = (Element) entityElements.item(j); errorMessages = entityElement.getElementsByTagName(SynchronizationConstants.ERROR_MESSAGE_ELEMENT); assertEquals(errorMessages.getLength(), 0, "Unexpected number of error message elements in an entity."); note = getDirectChildByTagName(entityElement, SynchronizationConstants.NOTES_ELEMENT); assertNotNull(note, "Could not find notes for an exported entity."); Node data = getDirectChildByTagName(entityElement, SynchronizationConstants.DATA_ELEMENT); assertNotNull(data, "Could not find data element in the entity."); Node datum = getDirectChildByTagName(data, "datum"); assertNotNull(datum, "Could not find the exported datum element containing the actual data."); String datumText = ((Element) datum).getTextContent(); notesText = ((Element)note).getTextContent(); assertEquals(notesText, ListToStringExporter.NOTE_PREFIX + datumText, "Unexpected discrepancy between data and notes in the export."); } ++elementIndex; } } @Test public void testJAXBHandled() throws Exception { JAXBSynchronizer sync = new JAXBSynchronizer(); Set<Synchronizer<?, ?>>syncs = this.<Synchronizer<?, ?>> asSet(sync); InputStream export = new ExportingInputStream(syncs, new HashMap<String, ExporterMessages>(), 1024, false); String exportContents = readAll(new InputStreamReader(export, "UTF-8")); XMLStreamReader rdr = XMLInputFactory.newInstance().createXMLStreamReader( new ByteArrayInputStream(exportContents.getBytes(Charset.forName("UTF-8")))); try { while (rdr.hasNext()) { switch (rdr.next()) { case XMLStreamReader.START_ELEMENT: String tagName = rdr.getName().getLocalPart(); if (SynchronizationConstants.ENTITIES_EXPORT_ELEMENT.equals(tagName)) { try { importSingle(rdr); } 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; } } } finally { rdr.close(); } } private <E, X> void importSingle(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."); Importer<E, X> importer = synchronizer.getImporter(); ExportedEntityMatcher<E, X> matcher = importer.getExportedEntityMatcher(); //the passed in configuration has precedence over the default one inlined in //the config file. while (rdr.hasNext()) { boolean bailout = false; switch (rdr.next()) { case XMLStreamConstants.START_ELEMENT: if (SynchronizationConstants.DATA_ELEMENT.equals(rdr.getName().getLocalPart())) { 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; } } } 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); } @Test(expectedExceptions = IOException.class) public void testExceptionHandling_Exporter_getExportingIterator() throws Exception { final Exporter<?, ?> failingExporter = context.mock(Exporter.class); final Synchronizer<?, ?> syncer = context.mock(Synchronizer.class); context.checking(new Expectations() { { RuntimeException failure = new RuntimeException("Injected failure"); allowing(failingExporter).getExportingIterator(); will(throwException(failure)); allowing(syncer).getRequiredValidators(); will(returnValue(Collections.emptySet())); allowing(syncer).getExporter(); will(returnValue(failingExporter)); } }); Set<Synchronizer<?, ?>> syncers = this.<Synchronizer<?, ?>>asSet(syncer); InputStream export = new ExportingInputStream(syncers, new HashMap<String, ExporterMessages>(), 1024, false); readAll(new InputStreamReader(export, "UTF-8")); //this should never be invoked, because reading the input stream should cause the exporter //to fail... fail("Successfully read the export even though one of the exporters threw an exception when asked for the exported entity iterator."); } @Test(expectedExceptions = IOException.class) public void testExceptionHandling_ExportingIterator_next() throws Exception { final ExportingIterator<?> iterator = context.mock(ExportingIterator.class); final Exporter<?, ?> exporter = context.mock(Exporter.class); final Importer<?, ?> importer = context.mock(Importer.class); final Synchronizer<?, ?> syncer = context.mock(Synchronizer.class); context.checking(new Expectations() { { RuntimeException failure = new RuntimeException("Injected failure"); allowing(iterator).hasNext(); will(returnValue(true)); allowing(iterator).next(); will(onConsecutiveCalls(returnValue("Success"), throwException(failure))); allowing(iterator).export(with(any(ExportWriter.class))); allowing(iterator).getNotes(); allowing(exporter).getExportingIterator(); will(returnValue(iterator)); allowing(exporter).getNotes(); allowing(syncer).getRequiredValidators(); will(returnValue(Collections.emptySet())); allowing(syncer).getExporter(); will(returnValue(exporter)); allowing(syncer).getImporter(); will(returnValue(importer)); allowing(importer).getImportConfigurationDefinition(); } }); Set<Synchronizer<?, ?>> syncers = this.<Synchronizer<?, ?>>asSet(syncer); InputStream export = new ExportingInputStream(syncers, new HashMap<String, ExporterMessages>(), 1024, false); readAll(new InputStreamReader(export, "UTF-8")); //this should never be invoked, because reading the input stream should cause the exporter //to fail... fail("Successfully read the export even though one of the exporters threw an exception when asked for the next exported entity."); } public void testExceptionHandling_ExportingIterator_export() throws Exception { final ExportingIterator<?> iterator = context.mock(ExportingIterator.class); final Exporter<?, ?> exporter = context.mock(Exporter.class); final Importer<?, ?> importer = context.mock(Importer.class); final Synchronizer<?, ?> syncer = context.mock(Synchronizer.class); context.checking(new Expectations() { { RuntimeException failure = new RuntimeException("Injected failure"); allowing(iterator).hasNext(); will(onConsecutiveCalls(returnValue(true), returnValue(true), returnValue(false))); allowing(iterator).next(); allowing(iterator).export(with(any(ExportWriter.class))); will(onConsecutiveCalls(returnValue(null), throwException(failure))); allowing(iterator).getNotes(); allowing(exporter).getExportingIterator(); will(returnValue(iterator)); allowing(exporter).getNotes(); allowing(syncer).getRequiredValidators(); will(returnValue(Collections.emptySet())); allowing(syncer).getExporter(); will(returnValue(exporter)); allowing(syncer).getImporter(); will(returnValue(importer)); allowing(importer).getImportConfigurationDefinition(); } }); Set<Synchronizer<?, ?>> syncers = this.<Synchronizer<?, ?>>asSet(syncer); InputStream export = new ExportingInputStream(syncers, new HashMap<String, ExporterMessages>(), 1024, false); String exportContents = readAll(new InputStreamReader(export, "UTF-8")); LOG.warn("Export contents:\n" + exportContents); export = new ByteArrayInputStream(exportContents.getBytes("UTF-8")); DocumentBuilder bld = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document doc = bld.parse(export); Element root = doc.getDocumentElement(); NodeList entities = root.getElementsByTagName(SynchronizationConstants.ENTITY_EXPORT_ELEMENT); assertEquals(entities.getLength(), 2, "Unexpected number of exported elements"); //get the entity with the error Element failedEntity = (Element) entities.item(1); Node errorMessage = getDirectChildByTagName(failedEntity, SynchronizationConstants.ERROR_MESSAGE_ELEMENT); assertNotNull(errorMessage, "Could not find the error-message element at the entity that failed to export."); } private <T> LinkedHashSet<T> asSet(T... ts) { LinkedHashSet<T> ret = new LinkedHashSet<T>(); for (T t : ts) { ret.add(t); } return ret; } private static String readAll(Reader rdr) throws IOException { try { StringBuilder bld = new StringBuilder(); int c; while((c = rdr.read()) != -1) { bld.append((char) c); } return bld.toString(); } finally { rdr.close(); } } private static Node getDirectChildByTagName(Node node, String tagName) { for(int i = 0; i < node.getChildNodes().getLength(); ++i) { Node n = node.getChildNodes().item(i); if (n.getNodeName().equals(tagName)) { return n; } } return null; } }