package ru.hflabs.rcd.history; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import ru.hflabs.rcd.Directories; import ru.hflabs.rcd.model.Historical; import ru.hflabs.rcd.model.annotation.Hashed; import ru.hflabs.rcd.model.change.Diff; import ru.hflabs.rcd.service.IDifferenceService; import ru.hflabs.rcd.service.IServiceFactory; import ru.hflabs.util.core.EqualsUtil; import ru.hflabs.util.core.Holder; import ru.hflabs.util.javac.InMemoryJavaFileObject; import ru.hflabs.util.javac.RuntimeCompiler; import ru.hflabs.util.javac.RuntimeCompilerUtil; import ru.hflabs.util.spring.util.ReflectionUtil; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.UndeclaredThrowableException; import java.util.*; /** * Класс <class>DifferenceServiceFactory</class> реализует фабрику создания <i>runtime</i> сервисов построения разницы между однотипными объектами * * @author Nazin Alexander * @see IServiceFactory * @see IDifferenceService * @see Hashed */ public class DifferenceServiceFactory<E> implements IDifferenceService<E>, IServiceFactory<IDifferenceService<E>, Class<?>>, InitializingBean { /** Сервис сравнения полей */ private static final Comparator<Field> FIELDS_COMPARATOR = new FieldsComparator(); /** Кеш сервисов */ private final Holder<Class<?>, IDifferenceService<E>> servicesHolder; /** Кеш полей */ private final Holder<Class<?>, Collection<Field>> hashedFieldsHolder; public DifferenceServiceFactory() { this.servicesHolder = new ServicesHolder(); this.hashedFieldsHolder = new HashedFieldsHolder(); } @Override public Collection<Diff> createDiff(E from, E to) { if (from != null && to != null) { Class<?> fromClass = from.getClass(); Class<?> toClass = to.getClass(); Assert.isTrue( EqualsUtil.equals(fromClass, toClass), String.format("Create a difference for different entities (%s != %s) is not supported", fromClass.getName(), toClass.getName()) ); return retrieveService(fromClass).createDiff(from, to); } else if (from != null) { return retrieveService(from.getClass()).createDiff(from, to); } else if (to != null) { return retrieveService(to.getClass()).createDiff(from, to); } return Collections.emptyList(); } @Override public String createHashCode(E target) { Assert.notNull(target, "Target essence must not be NULL"); return retrieveService(target.getClass()).createHashCode(target); } @Override public IDifferenceService<E> retrieveService(Class<?> targetClass) { Assert.notNull(targetClass, "Target class must not be NULL"); return servicesHolder.getValue(targetClass); } @Override public void destroyService(Class<?> key, IDifferenceService<E> service) { servicesHolder.removeValue(key); } /** * Выполняет построение сервиса * * @param targetClass целевой класс * @return Возвращает объект готовый к компиляции */ private InMemoryJavaFileObject<IDifferenceService<E>> buildInstance(Class<?> targetClass) { final StringWriter writer = new StringWriter(); final PrintWriter out = new PrintWriter(writer); // Определяем пакет класса final String packageName = getClass().getPackage().getName(); out.format("package %s;", packageName).println(); out.println(); // Добавляем необходимые import-ы buildImports(out, targetClass); // Формируем заголовок класса RuntimeCompilerUtil.printPublicClassHeaderTemplate( out, getClass().getName(), String.format("extends %s<%s>", DifferenceService.class.getSimpleName(), targetClass.getSimpleName()) ); // Формирование методов buildMethod_createDiff(out, targetClass); buildMethod_createHashCode(out, targetClass); out.println("}"); out.close(); return RuntimeCompilerUtil.createJavaFileObject( packageName, targetClass.getSimpleName() + IDifferenceService.class.getSimpleName(), writer.toString() ); } /** * Формирование импортов * * @param out поток вывода * @param targetClass целевой класс */ protected void buildImports(PrintWriter out, Class<?> targetClass) { out.format("import %s;", targetClass.getName()).println(); out.format("import %s;", ru.hflabs.rcd.model.change.Diff.class.getName()).println(); out.format("import %s;", ru.hflabs.rcd.service.IDifferenceService.class.getName()).println(); out.format("import %s;", ru.hflabs.util.core.EqualsUtil.class.getName()).println(); out.format("import %s;", ru.hflabs.util.core.FormatUtil.class.getName()).println(); out.format("import %s;", ru.hflabs.util.core.MD5.class.getName()).println(); out.println(); out.format("import %s;", java.util.ArrayList.class.getName()).println(); out.format("import %s;", java.util.Collection.class.getName()).println(); out.println(); } /** * Выполняет построение метода {@link IDifferenceService#createDiff(Object, Object)} * * @param out поток вывода * @param targetClass целевой класс */ private void buildMethod_createDiff(PrintWriter out, Class<?> targetClass) { out.println(" @Override"); out.format(" public Collection<Diff> createDiff(%s from, %s to) {", targetClass.getSimpleName(), targetClass.getSimpleName()).println(); out.println(" final Collection<Diff> result = new ArrayList<Diff>();"); for (Field field : hashedFieldsHolder.getValue(targetClass)) { final String fieldName = field.getName(); final String fieldClassName = ClassUtils.resolvePrimitiveIfNecessary(field.getType()).getName(); out.format(" // %s", fieldName).println(); out.println(" {"); if (Enum.class.isAssignableFrom(field.getType())) { out.format(" final String fromValue = (from != null) ? FormatUtil.format(from.%s(), false) : null;", RuntimeCompilerUtil.get(field)).println(); out.format(" final String toValue = (to != null) ? FormatUtil.format(to.%s(), false) : null;", RuntimeCompilerUtil.get(field)).println(); } else if (Collection.class.isAssignableFrom(field.getType()) || Map.class.isAssignableFrom(field.getType())) { out.format(" final String fromValue = (from != null) ? formatObject(from.%s()) : null;", RuntimeCompilerUtil.get(field)).println(); out.format(" final String toValue = (to != null) ? formatObject(to.%s()) : null;", RuntimeCompilerUtil.get(field)).println(); } else { out.format(" final String fromValue = (from != null) ? FormatUtil.format(from.%s()) : null;", RuntimeCompilerUtil.get(field)).println(); out.format(" final String toValue = (to != null) ? FormatUtil.format(to.%s()) : null;", RuntimeCompilerUtil.get(field)).println(); } out.println(" if (!EqualsUtil.equals(fromValue, toValue)) {"); out.format(" result.add(new Diff(\"%s\", \"%s\", fromValue, toValue));", fieldClassName, fieldName).println(); out.println(" }"); out.println(" }"); } out.println(" return !result.isEmpty() ? result : null;"); out.println(" }"); out.println(); } /** * Выполняет построение метода {@link IDifferenceService#createHashCode(Object)} * * @param out поток вывода * @param targetClass целевой класс */ private void buildMethod_createHashCode(PrintWriter out, Class<?> targetClass) { out.println(" @Override"); out.format(" public String createHashCode(%s target) {", targetClass.getSimpleName()).println(); out.println(" return MD5.asHex("); for (Iterator<Field> iterator = hashedFieldsHolder.getValue(targetClass).iterator(); iterator.hasNext(); ) { final Field field = iterator.next(); final String separator = iterator.hasNext() ? "," : ""; if (CharSequence.class.isAssignableFrom(field.getType())) { out.format(" target.%s()%s", RuntimeCompilerUtil.get(field), separator).println(); } else if (Enum.class.isAssignableFrom(field.getType())) { out.format(" FormatUtil.format(target.%s(), false)%s", RuntimeCompilerUtil.get(field), separator).println(); } else if (Collection.class.isAssignableFrom(field.getType())) { out.format(" formatCollection(target.%s())%s", RuntimeCompilerUtil.get(field), separator).println(); } else if (Map.class.isAssignableFrom(field.getType())) { out.format(" formatMap(target.%s())%s", RuntimeCompilerUtil.get(field), separator).println(); } else { out.format(" FormatUtil.format(target.%s())%s", RuntimeCompilerUtil.get(field), separator).println(); } } out.println(" );"); out.println(" }"); out.println(); } /** * Создает новый экземпляр класса * * @param memoryClass исходный класс * @return Возвращает созданный экземпляр класса */ private IDifferenceService<E> createInstance(Class<IDifferenceService<E>> memoryClass) throws Exception { return memoryClass.newInstance(); } @Override public void afterPropertiesSet() throws Exception { for (Class<?> clazz : ReflectionUtil.getAnnotatedClasses(Historical.class.getPackage().getName(), Hashed.class)) { retrieveService(clazz); } } /** * Класс <class>ServicesHolder</class> реализует кеш сервисов * * @author Nazin Alexander */ private class ServicesHolder extends Holder<Class<?>, IDifferenceService<E>> { @Override protected IDifferenceService<E> createValue(Class<?> key) { try { return createInstance( RuntimeCompiler.compileIfNotFound( buildInstance(key), System.out, Directories.RUNTIME_FOLDER_SOURCE.location, Directories.RUNTIME_FOLDER_COMPILED.location ) ); } catch (Exception ex) { throw new UndeclaredThrowableException(ex); } } } /** * Класс <class>HashedFieldsHolder</class> реализует кеш полей, которые присутствуют в аннотации {@link Hashed} * * @author Nazin Alexander */ private class HashedFieldsHolder extends Holder<Class<?>, Collection<Field>> { @Override protected Collection<Field> createValue(Class<?> key) { final Hashed hashed = key.getAnnotation(Hashed.class); Assert.notNull(hashed, String.format("Class '%s' not contains @%s annotation", key.getName(), Hashed.class.getSimpleName())); final Collection<String> ignoreFields = Sets.newHashSet(key.getAnnotation(Hashed.class).ignore()); final List<Field> result = Lists.newArrayList(); ReflectionUtil.doWithFields( key, new ReflectionUtils.FieldCallback() { @Override public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { result.add(field); } }, new ReflectionUtils.FieldFilter() { @Override public boolean matches(Field field) { return !ignoreFields.contains(field.getName()) && !Modifier.isTransient(field.getModifiers()) && !Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers()) && !Collection.class.isAssignableFrom(field.getType()); } } ); Assert.isTrue(!CollectionUtils.isEmpty(result), String.format("Can't find any hashed fields for class '%s'", key.getName())); Collections.sort(result, FIELDS_COMPARATOR); return result; } } /** * Класс <class>FieldsComparator</class> реализует сервис сравнения полей на основании задекларированного класса и имени поля * * @author Nazin Alexander */ private static class FieldsComparator implements Comparator<Field> { @Override public int compare(Field o1, Field o2) { Class<?> o1Class = o1.getDeclaringClass(); Class<?> o2Class = o1.getDeclaringClass(); int classResult = o1Class.getName().compareTo(o2Class.getName()); return classResult == 0 ? o1.getName().compareTo(o2.getName()) : classResult; } } }