/** * AnalyzerBeans * Copyright (C) 2014 Neopost - Customer Information Management * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.eobjects.analyzer.util; import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.ObjectStreamField; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import org.apache.metamodel.util.EqualsBuilder; import org.apache.metamodel.util.HasName; import org.apache.metamodel.util.LegacyDeserializationObjectInputStream; import org.eobjects.analyzer.beans.api.ComponentCategory; import org.eobjects.analyzer.connection.Datastore; import org.eobjects.analyzer.data.InputColumn; import org.eobjects.analyzer.descriptors.MetricDescriptor; import org.eobjects.analyzer.job.ComponentJob; import org.eobjects.analyzer.reference.TextFileDictionary; import org.eobjects.analyzer.reference.TextFileSynonymCatalog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@link ObjectInputStream} implementation that is aware of changes such as * class or package renaming. This can be used to deserialize classes with * historic/legacy class names. * * Furthermore the deserialization mechanism is aware of multiple * {@link ClassLoader}s. This means that if the object being deserialized * pertains to a different {@link ClassLoader}, then this classloader can be * added using the {@link #addClassLoader(ClassLoader)} method. * * */ public class ChangeAwareObjectInputStream extends LegacyDeserializationObjectInputStream { private static final Logger logger = LoggerFactory.getLogger(ChangeAwareObjectInputStream.class); /** * Table mapping primitive type names to corresponding class objects. As * defined in {@link ObjectInputStream}. */ private static final Map<String, Class<?>> PRIMITIVE_CLASSES = new HashMap<String, Class<?>>(8, 1.0F); /** * Since the change from eobjects.org MetaModel to Apache MetaModel, a lot * of interfaces (especially those that extend {@link HasName}) have * transparently changed their serialization IDs. */ private static final Set<String> INTERFACES_WITH_SERIAL_ID_CHANGES = new HashSet<String>(); static { PRIMITIVE_CLASSES.put("boolean", boolean.class); PRIMITIVE_CLASSES.put("byte", byte.class); PRIMITIVE_CLASSES.put("char", char.class); PRIMITIVE_CLASSES.put("short", short.class); PRIMITIVE_CLASSES.put("int", int.class); PRIMITIVE_CLASSES.put("long", long.class); PRIMITIVE_CLASSES.put("float", float.class); PRIMITIVE_CLASSES.put("double", double.class); PRIMITIVE_CLASSES.put("void", void.class); INTERFACES_WITH_SERIAL_ID_CHANGES.add(InputColumn.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add(ComponentJob.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add(Datastore.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add(MetricDescriptor.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add(PropertyDescriptor.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add(ComponentCategory.class.getName()); INTERFACES_WITH_SERIAL_ID_CHANGES.add("org.eobjects.analyzer.beans.writers.WriteDataResult"); } private static final Comparator<String> comparator = new Comparator<String>() { @Override public int compare(String o1, String o2) { if (EqualsBuilder.equals(o1, o2)) { return 0; } // use length as the primary differentiator, to make sure long // packages are placed before short ones. int diff = o1.length() - o2.length(); if (diff == 0) { diff = o1.compareTo(o2); } return diff; } }; private final List<ClassLoader> additionalClassLoaders; private final Map<String, String> renamedPackages; private final Map<String, String> renamedClasses; public ChangeAwareObjectInputStream(InputStream in) throws IOException { super(in); renamedPackages = new TreeMap<String, String>(comparator); renamedClasses = new HashMap<String, String>(); additionalClassLoaders = new ArrayList<ClassLoader>(); // add analyzerbeans' own renamed classes addRenamedClass("org.eobjects.analyzer.reference.TextBasedDictionary", TextFileDictionary.class); addRenamedClass("org.eobjects.analyzer.reference.TextBasedSynonymCatalog", TextFileSynonymCatalog.class); // analyzer results moved as of ticket #843 addRenamedClass("org.eobjects.analyzer.result.PatternFinderResult", "org.eobjects.analyzer.beans.stringpattern.PatternFinderResult"); addRenamedClass("org.eobjects.analyzer.result.DateGapAnalyzerResult", "org.eobjects.analyzer.beans.dategap.DateGapAnalyzerResult"); addRenamedClass("org.eobjects.analyzer.util.TimeInterval", "org.eobjects.analyzer.beans.dategap.TimeInterval"); addRenamedClass("org.eobjects.analyzer.result.StringAnalyzerResult", "org.eobjects.analyzer.beans.StringAnalyzerResult"); addRenamedClass("org.eobjects.analyzer.result.NumberAnalyzerResult", "org.eobjects.analyzer.beans.NumberAnalyzerResult"); addRenamedClass("org.eobjects.analyzer.result.BooleanAnalyzerResult", "org.eobjects.analyzer.beans.BooleanAnalyzerResult"); addRenamedClass("org.eobjects.analyzer.result.DateAndTimeAnalyzerResult", "org.eobjects.analyzer.beans.DateAndTimeAnalyzerResult"); // analyzer results moved as of ticket #993 addRenamedClass("org.eobjects.analyzer.result.ValueDistributionGroupResult", "org.eobjects.analyzer.beans.valuedist.SingleValueDistributionResult"); addRenamedClass("org.eobjects.analyzer.result.ValueDistributionResult", "org.eobjects.analyzer.beans.valuedist.GroupedValueDistributionResult"); addRenamedClass("org.eobjects.analyzer.beans.valuedist.ValueDistributionGroupResult", "org.eobjects.analyzer.beans.valuedist.SingleValueDistributionResult"); addRenamedClass("org.eobjects.analyzer.beans.valuedist.ValueDistributionResult", "org.eobjects.analyzer.beans.valuedist.GroupedValueDistributionResult"); addRenamedClass("org.eobjects.analyzer.beans.valuedist.ValueCount", "org.eobjects.analyzer.result.SingleValueFrequency"); addRenamedClass("org.eobjects.analyzer.result.ValueCount", "org.eobjects.analyzer.result.SingleValueFrequency"); addRenamedClass("org.eobjects.analyzer.beans.valuedist.ValueCountList", "org.eobjects.analyzer.result.ValueCountList"); addRenamedClass("org.eobjects.analyzer.beans.valuedist.ValueCountListImpl", "org.eobjects.analyzer.result.ValueCountListImpl"); // duplicate detection analyzer changed addRenamedClass("com.hi.contacts.datacleaner.DuplicateDetectionAnalyzer", "com.hi.hiqmr.datacleaner.deduplication.Identify7DeduplicationAnalyzer"); // DataCleaner output writers package changed addRenamedPackage("org.eobjects.datacleaner.output.beans", "org.eobjects.datacleaner.extension.output"); } public void addClassLoader(ClassLoader classLoader) { additionalClassLoaders.add(classLoader); } public void addRenamedPackage(String originalPackageName, String newPackageName) { renamedPackages.put(originalPackageName, newPackageName); } public void addRenamedClass(String originalClassName, Class<?> newClass) { addRenamedClass(originalClassName, newClass.getName()); } public void addRenamedClass(String originalClassName, String newClassName) { renamedClasses.put(originalClassName, newClassName); } @Override protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException { final ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); final String originalClassName = resultClassDescriptor.getName(); if (renamedClasses.containsKey(originalClassName)) { final String className = renamedClasses.get(originalClassName); logger.info("Class '{}' was encountered. Returning class descriptor of new class name: '{}'", originalClassName, className); return getClassDescriptor(className, resultClassDescriptor); } else { final Set<Entry<String, String>> entrySet = renamedPackages.entrySet(); for (Entry<String, String> entry : entrySet) { final String legacyPackage = entry.getKey(); if (originalClassName.startsWith(legacyPackage)) { final String className = originalClassName.replaceFirst(legacyPackage, entry.getValue()); logger.info("Class '{}' was encountered. Returning class descriptor of new class name: '{}'", originalClassName, className); return getClassDescriptor(className, resultClassDescriptor); } } } if (INTERFACES_WITH_SERIAL_ID_CHANGES.contains(originalClassName)) { final ObjectStreamClass newClassDescriptor = ObjectStreamClass.lookup(resolveClass(originalClassName)); return newClassDescriptor; } return resultClassDescriptor; } private ObjectStreamClass getClassDescriptor(final String className, final ObjectStreamClass originalClassDescriptor) throws ClassNotFoundException { if (originalClassDescriptor == null) { logger.warn("Original ClassDescriptor resolved to null for '{}'", className); } final Class<?> newClass = resolveClass(className); final ObjectStreamClass newClassDescriptor = ObjectStreamClass.lookupAny(newClass); if (newClassDescriptor == null) { logger.warn("New ClassDescriptor resolved to null for {}", newClass); } final String[] newFieldNames = getFieldNames(newClassDescriptor); final String[] originalFieldNames = getFieldNames(originalClassDescriptor); if (!EqualsBuilder.equals(originalFieldNames, newFieldNames)) { logger.warn("Field names of original and new class ({}) does not correspond!", className); // try to hack our way out of it by changing the value of the "name" // field in the ORIGINAL descriptor try { Field field = ObjectStreamClass.class.getDeclaredField("name"); assert field != null; assert field.getType() == String.class; field.setAccessible(true); field.set(originalClassDescriptor, className); return originalClassDescriptor; } catch (Exception e) { logger.error("Unsuccesful attempt at changing the name of the original class descriptor"); if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new IllegalStateException(e); } } return newClassDescriptor; } @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { final String className = desc.getName(); if (className.startsWith("org.eobjects.metamodel") || className.startsWith("[Lorg.eobjects.metamodel")) { return super.resolveClass(desc); } return resolveClass(className); } private Class<?> resolveClass(String className) throws ClassNotFoundException { logger.debug("Resolving class '{}'", className); try { return Class.forName(className); } catch (ClassNotFoundException e) { final Class<?> primitiveClass = PRIMITIVE_CLASSES.get(className); if (primitiveClass != null) { return primitiveClass; } logger.info("Class '{}' was not resolved in main class loader.", className); final List<Exception> exceptions = new ArrayList<Exception>(additionalClassLoaders.size()); for (ClassLoader classLoader : additionalClassLoaders) { try { return Class.forName(className, true, classLoader); } catch (ClassNotFoundException minorException) { logger.info("Class '{}' was not resolved in additional class loader '{}'", className, classLoader); exceptions.add(minorException); } } logger.warn("Could not resolve class of name '{}'", className); // if we reach this stage, all classloaders have failed, log their // issues int i = 1; for (final Exception exception : exceptions) { int numExceptions = exceptions.size(); logger.error("Exception " + i + " of " + numExceptions, exception); i++; } throw e; } } private String[] getFieldNames(ObjectStreamClass classDescriptor) { if (classDescriptor == null) { return new String[0]; } final ObjectStreamField[] fields = classDescriptor.getFields(); final String[] fieldNames = new String[fields.length]; for (int i = 0; i < fieldNames.length; i++) { fieldNames[i] = fields[i].getName(); } return fieldNames; } }