5

I'm using Spring Boot, Spring Data REST, Spring HATEOAS, Hibernate, Spring Data Envers. I'm auditing my entities in order to tracking all changes. I am consuming REST service from an Angular 4 application. My goal is display a lix of boxes for each Revision (envers) showing only modified properties.

My entities are annotated with @Audited. This is my model:

@Audited
public class LicensePlate extends AbstractEntity {
    private static final long serialVersionUID = -6871697166535810224L;

    @NotEmpty
    @ColumnTransformer(read = "UPPER(licensePlate)", write = "UPPER(?)")
    @Column(nullable = false, unique = true)
    private String licensePlate;    

    @NotNull
    @Range(min = 1, max = 99)
    @Column(nullable = false, columnDefinition = "INTEGER default 50")
    private int seats = 50;


    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Country country;

As you can see, Country is LAZY and I can't to set it EAGER.

I found this class that recursively check lazy properties in the model and initialize them.

public class HibernateUtil {

    public static byte[] hibernateCollectionPackage = "org.hibernate.collection".getBytes();

    public static void initializeObject(Object o, String insidePackageName) {
        Set<Object> seenObjects = new HashSet<Object>();
        initializeObject(o, seenObjects, insidePackageName.getBytes());
        seenObjects = null;
    }

    private static void initializeObject(Object o, Set<Object> seenObjects, byte[] insidePackageName) {

        seenObjects.add(o);

        Method[] methods = o.getClass().getMethods();
        for (Method method : methods) {

            String methodName = method.getName();

            // check Getters exclusively
            if (methodName.length() < 3 || !"get".equals(methodName.substring(0, 3)))
                continue;

            // Getters without parameters
            if (method.getParameterTypes().length > 0)
                continue;

            int modifiers = method.getModifiers();

            // Getters that are public
            if (!Modifier.isPublic(modifiers))
                continue;

            // but not static
            if (Modifier.isStatic(modifiers))
                continue;

            try {

                // Check result of the Getter
                Object r = method.invoke(o);

                if (r == null)
                    continue;

                // prevent cycles
                if (seenObjects.contains(r))
                    continue;

                // ignore simple types, arrays und anonymous classes
                if (!isIgnoredType(r.getClass()) && !r.getClass().isPrimitive() && !r.getClass().isArray()
                        && !r.getClass().isAnonymousClass()) {

                    // ignore classes out of the given package and out of the
                    // hibernate collection
                    // package
                    if (!isClassInPackage(r.getClass(), insidePackageName) && !isClassInPackage(r.getClass(), hibernateCollectionPackage)) {
                        continue;
                    }

                    // initialize child object
                    Hibernate.initialize(r);

                    // traverse over the child object
                    initializeObject(r, seenObjects, insidePackageName);
                }

            } catch (InvocationTargetException e) {
                e.printStackTrace();
                return;
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
                return;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
                return;
            }
        }

    }

    private static final Set<Class<?>> IGNORED_TYPES = getIgnoredTypes();

    private static boolean isIgnoredType(Class<?> clazz) {
        return IGNORED_TYPES.contains(clazz);
    }

    private static Set<Class<?>> getIgnoredTypes() {
        Set<Class<?>> ret = new HashSet<Class<?>>();
        ret.add(Boolean.class);
        ret.add(Character.class);
        ret.add(Byte.class);
        ret.add(Short.class);
        ret.add(Integer.class);
        ret.add(Long.class);
        ret.add(Float.class);
        ret.add(Double.class);
        ret.add(Void.class);
        ret.add(String.class);
        ret.add(Class.class);
        ret.add(Package.class);
        return ret;
    }

    private static Boolean isClassInPackage(Class<?> clazz, byte[] insidePackageName) {

        Package p = clazz.getPackage();
        if (p == null)
            return null;

        byte[] packageName = p.getName().getBytes();

        int lenP = packageName.length;
        int lenI = insidePackageName.length;

        if (lenP < lenI)
            return false;

        for (int i = 0; i < lenI; i++) {
            if (packageName[i] != insidePackageName[i])
                return false;
        }

        return true;
    }
}

This is the REST controller:

    @PreAuthorize("isAuthenticated()")
    @RequestMapping(value = "/licensePlates/{id}/revisions/{revid}", method = RequestMethod.GET)
    public ResponseEntity<?> findRevision(@PathVariable(value = "id") Long id, @PathVariable(value = "revid") Integer revId,
            Pageable pageable) {
        if (id != null) {
            List<RestChange> changes = new ArrayList<>();

            AuditReader reader = AuditReaderFactory.get(entityManager);
            AuditQuery q = reader.createQuery().forEntitiesAtRevision(LicensePlate.class, revId);
            LicensePlate current = (LicensePlate) q.getSingleResult();
            HibernateUtil.initializeObject(current, "it.model");

            //PREVIOUS
            if (revId > 1) {
                q = reader.createQuery().forEntitiesAtRevision(LicensePlate.class, revId - 1);
                LicensePlate previous = (LicensePlate) q.getSingleResult();             
                HibernateUtil.initializeObject(previous, "it.model");

                Diff diff = javers.compare(previous, current);
                for (Change change : diff.getChanges()) {
                    log.debug("Change: " + change);
                    if (change instanceof ValueChange) {
                        ValueChange c = (ValueChange) change;
                        RestChange restChange = new RestChange();
                        restChange.setPropertyName(c.getPropertyName());
                        restChange.setPreviousValue(c.getLeft());
                        restChange.setValue(c.getRight());
                        changes.add(restChange);
                    } else if (change instanceof ReferenceChange) {
                        ReferenceChange c = (ReferenceChange) change;
                        RestChange restChange = new RestChange();
                        restChange.setPropertyName(c.getPropertyName());
                        restChange.setPreviousValue(c.getLeftObject().get().toString());
                        restChange.setValue(c.getRightObject().get().toString());
                        changes.add(restChange);
                    }

                }
            }


            return new ResponseEntity<>(new Resources<>(changes), HttpStatus.OK);
        } else {
            throw new ResourceNotFoundException();
        }
    }

I debugged and the property country in the current and previous beans remains a proxy even after the call to Hibernate.initialize. The only way I found to fix the problem is call Hibernate.initialize(current.getCountry()) and then re-set the value in the licensePlate bean but I don't feel this is right.

I checked all others question with the similar problem and in all cases Hibernate.initialize was the solution of the question, but here seems it doesn't work. What am I doing wrong?

drenda
  • 5,846
  • 11
  • 68
  • 141
  • I assume `Country` is also audited? – Naros Nov 13 '17 at 14:28
  • Yes, sure. It's audited – drenda Nov 13 '17 at 14:29
  • 1
    This sounds like a bug, I would open a `hibernate-envers` issue at https://hibernate.atlassian.net – Naros Nov 13 '17 at 14:51
  • @Naros What is the bug in your opinion? The fact that Hibernate.initialize doesn't load the lazy object? Thanks – drenda Nov 13 '17 at 15:07
  • That's correct. The persister should be returning a proxy and therefore Hibernate#initialize should be able to hydrate it. Just be sure to attach a test case and I'll check it out. – Naros Nov 13 '17 at 15:10
  • @Naros I created a bug https://hibernate.atlassian.net/browse/HHH-12091 and I attached a test case. Thanks – drenda Nov 13 '17 at 22:17
  • 1
    You may want to check out https://stackoverflow.com/questions/24994440/no-serializer-found-for-class-org-hibernate-proxy-pojo-javassist-javassist as that post describes the fact you need to either specify ignoring the initializer property or as the error tells you; specify that you shouldn't fail on null beans. I'll update the JiRA as well but this is not a bug or problem with hibernate. – Naros Jan 17 '18 at 17:24

0 Answers0