/** * Copyright 2009-2013 Oy Vaadin Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.vaadin.addon.jpacontainer.util; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.persistence.Id; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import com.vaadin.addon.jpacontainer.EntityProvider; import com.vaadin.addon.jpacontainer.LazyLoadingDelegate; /** * HibernateLazyLoadingDelegate is the default implementation of the * {@link LazyLoadingDelegate} interface for use with Hibernate. Note that * EclipseLink does not require any lazy loading delegates in order for lazy * loading to transparently work. * * HibernateLazyLoadingDelegate handles lazy loaded properties by explicitly * loading the property in question from the database and attaching it to the * entity passed in. This happens recursively if the property is nested. * * @author Jonatan Kronqvist / Vaadin Ltd * @since 2.0 */ public class HibernateLazyLoadingDelegate implements LazyLoadingDelegate { private EntityProvider<?> entityProvider; public void setEntityProvider(EntityProvider<?> ep) { entityProvider = ep; } public <E> E ensureLazyPropertyLoaded(E entity, String propertyName) { String prop = getRootPropertyName(propertyName); try { Object value = lazilyLoadPropertyValue(entity, prop); value = recurseIfNested(propertyName, value); trySetUsingSetter(entity, prop, value); } catch (IllegalArgumentException e) { e.printStackTrace(); } return entity; } /** * Builds a query and loads the property value for the entity * * @return the result list from the query. */ private <E> Object lazilyLoadPropertyValue(E entity, String prop) { CriteriaBuilder cb = entityProvider.getEntityManager() .getCriteriaBuilder(); CriteriaQuery<Object> q = cb.createQuery(); Root<? extends Object> root = q.from(entity.getClass()); q.select(root.get(prop)); q.where(cb.equal(root.get("id"), cb.literal(tryGetEntityId(entity)))); return entityProvider.getEntityManager().createQuery(q).getResultList(); } /** * Lazily load the properties recursively if this is a nested property. E.g. * loads the data for "bar" and "baz" if the property name is "foo.bar.baz" * and attaches these values to "foo" and returns the value of "foo". * * @param propertyName * the name ("path") of the, possibly nested, property. * @param value * the value of the "root" property in propertyName (e.g. the * value of "foo" if propertyName is "foo.bar.baz"). * @return the value of the "root" property */ private Object recurseIfNested(String propertyName, Object value) { if (isNestedProperty(propertyName)) { // If the property is nested, only the final node can be a // collection of items (the syntax doesn't support nesting through // collections), so we're safe to just grab the first element // of the list currently in 'value'. Object subEntity = ((List<?>) value).get(0); String subProperty = propertyName.substring(propertyName .indexOf('.') + 1); value = ensureLazyPropertyLoaded(subEntity, subProperty); } return value; } /** * @return the "root" property name, i.e. the string up to the first dot, * denoting a nested property. */ private String getRootPropertyName(String propertyName) { if (isNestedProperty(propertyName)) { return propertyName.substring(0, propertyName.indexOf('.')); } return propertyName; } /** * @return true if propertyName contains nested properties. */ private boolean isNestedProperty(String propertyName) { return propertyName.indexOf('.') != -1; } private <E> Object tryGetEntityId(E entity) { try { return getEntityId(entity); } catch (IllegalArgumentException e) { throw new RuntimeException("Could not get the entity id.", e); } catch (IllegalAccessException e) { throw new RuntimeException("Could not get the entity id.", e); } catch (InvocationTargetException e) { throw new RuntimeException("Could not get the entity id.", e); } } /** * Find the ID of an entity by finding either the field or the getter * annotated with the {@link Id} annotation and getting the value. * * @param entity * the entity to find the ID of. * @return the ID of the entity. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InvocationTargetException */ private <E> Object getEntityId(E entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { // Try fields for (Field f : entity.getClass().getDeclaredFields()) { if (f.isAnnotationPresent(Id.class)) { try { f.setAccessible(true); return f.get(entity); } finally { f.setAccessible(false); } } } // Try methods if no annotated field was found. for (Method m : entity.getClass().getMethods()) { if (m.isAnnotationPresent(Id.class)) { return m.invoke(entity); } } return null; } private <E> void trySetUsingSetter(E entity, String propertyName, Object value) { try { setUsingSetter(entity, propertyName, value); } catch (IllegalArgumentException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } catch (SecurityException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } catch (IllegalAccessException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } catch (InvocationTargetException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } catch (InstantiationException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } catch (NoSuchMethodException e) { throw new RuntimeException( "Could not set lazy loaded value for entity.", e); } } /** * Finds the setter for a property and sets the value of the property using * the found setter method. * * @param entity * the entity containing the property to set the value for. * @param propertyName * the name of the property. * @param value * the new value of the property. * @throws IllegalArgumentException * @throws IllegalAccessException * @throws InvocationTargetException * @throws InstantiationException * @throws SecurityException * @throws NoSuchMethodException */ @SuppressWarnings({ "rawtypes", "unchecked" }) private <E> void setUsingSetter(E entity, String propertyName, Object value) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException, SecurityException, NoSuchMethodException { Method setter = findSetterFor(entity, propertyName); if (setter != null) { Class<?> parameterType = setter.getParameterTypes()[0]; if (Collection.class.isAssignableFrom(parameterType)) { if (Set.class.isAssignableFrom(parameterType)) { value = new HashSet((Collection) value); } } else if (value instanceof Collection) { // "Unwrap" the value from the collection, since the setter // doesn't accept collections. value = ((Collection) value).iterator().next(); } setter.invoke(entity, value); } } private <E> Method findSetterFor(E entity, String propertyName) { for (Method m : entity.getClass().getMethods()) { if (m.getName().equalsIgnoreCase("set" + propertyName)) { return m; } } return null; } }