package au.com.vaadinutils.converter; /** * 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. */ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import javax.persistence.ManyToMany; import javax.persistence.OneToMany; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import au.com.vaadinutils.crud.CrudEntity; import com.google.common.base.Preconditions; import com.vaadin.addon.jpacontainer.EntityItem; import com.vaadin.addon.jpacontainer.EntityItemProperty; import com.vaadin.addon.jpacontainer.metadata.EntityClassMetadata; import com.vaadin.addon.jpacontainer.metadata.MetadataFactory; import com.vaadin.addon.jpacontainer.metadata.PropertyMetadata; import com.vaadin.data.Property; import com.vaadin.data.util.TransactionalPropertyWrapper; import com.vaadin.data.util.converter.Converter; import com.vaadin.ui.AbstractSelect; public class MultiSelectConverter<T extends CrudEntity> implements Converter<Collection<Object>, Collection<T>> { private static final long serialVersionUID = 1L; private final AbstractSelect select; private Boolean owningSide; private String mappedBy; @SuppressWarnings("rawtypes") private Class type; Logger logger = LogManager.getLogger(); public MultiSelectConverter(AbstractSelect select, @SuppressWarnings("rawtypes") Class type) { this.select = select; this.type = type; } private ContainerAdaptor<T> getContainer() { return ContainerAdaptorFactory.getAdaptor( select.getContainerDataSource()); } @SuppressWarnings("unchecked") @Override public Collection<Object> convertToPresentation(Collection<T> value, Class<? extends Collection<Object>> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException { // Value here is a collection of entities, should be transformed to a // collection (set) of identifier // TODO, consider creating a cached value if (value == null || value.isEmpty()) { try { // return // createNewCollectionForType(getPropertyDataSource().getType()); return createNewCollectionForType(type); } catch (Exception e) { throw new ConversionException(e); } } HashSet<Object> identifiers = new HashSet<Object>(); for (T entity : value) { identifiers.add(entity.getId()); } return identifiers; } @SuppressWarnings("unchecked") @Override public Collection<T> convertToModel(Collection<Object> value, Class<? extends Collection<T>> targetType, Locale locale) throws com.vaadin.data.util.converter.Converter.ConversionException { // NOTE, this currently works properly only if equals and hashcode // methods have been implemented correctly (both depending on identifier // of the entity) // TODO create a filter that has a workaround for invalid // equals/hashCode // formattedValue here is a set of identifiers. // We will modify the existing collection of entities to contain // corresponding entities Collection<Object> idset = value; Collection<T> modelValue = null; if (getPropertyDataSource() != null) { modelValue = (Collection<T>) getPropertyDataSource().getValue(); } if (modelValue == null) { try { modelValue = createNewCollectionForType(type); } catch (Exception e) { throw new ConversionException(e); } } if (idset == null || idset.isEmpty()) { modelValue.clear(); return modelValue; } HashSet<T> orphaned = new HashSet<T>(modelValue); // Add those that did not exist do not exist already + remove them from // orphaned collection for (Object id : idset) { EntityItem<T> item = (EntityItem<T>) getContainer().getItem(id); if (item != null) { T entity = item.getEntity(); if (!modelValue.contains(entity)) { modelValue.add(entity); addBackReference(entity); } orphaned.remove(entity); } else { logger.error("couldn't find id {} in database for type {} entityClass {}", id, type, getContainer() .getEntityClass()); } } // remove orphanded for (T entity : orphaned) { modelValue.remove(entity); removeBackReference(entity); } if (!isOwningSide()) { // refresh the item as modifying back references may also have // changed the collections, without this we'd get concurrent // modification exception. // FIXME: when verifying a field using this converter this following // line causes a value change event on that field, which causes all // kinds of shit which ultimately causes an exception causing the // validation to fail with a validation error message. // getPropertyDataSource().getItem().refresh(); } return modelValue; } @SuppressWarnings("rawtypes") private EntityItemProperty getPropertyDataSource() { if (select.getPropertyDataSource() != null) { return (EntityItemProperty) ((TransactionalPropertyWrapper) select.getPropertyDataSource()) .getWrappedProperty(); } return null; } private void removeBackReference(T entity) { if (!isOwningSide()) { Property<Object> itemProperty = getBackReferenceItemProperty(entity); Object property = itemProperty.getValue(); if (property instanceof Collection) { // many to many @SuppressWarnings("rawtypes") Collection c = (Collection) property; c.remove(getPropertyDataSource().getItem().getEntity()); itemProperty.setValue(c); } else { // one to many itemProperty.setValue(null); } } } private Property<Object> getBackReferenceItemProperty(T entity) { return getContainer().getProperty(entity,mappedBy); } @SuppressWarnings({ "rawtypes", "unchecked" }) private void addBackReference(T entity) { if (!isOwningSide()) { Property itemProperty = getBackReferenceItemProperty(entity); Object property = itemProperty.getValue(); if (property == null || !(property instanceof Collection)) { itemProperty.setValue(getPropertyDataSource().getItem().getEntity()); // one to many } else { // many to many Preconditions.checkArgument(property instanceof Collection, "Expected a Collection got " + itemProperty.getType() + " " + property.getClass().getCanonicalName()); Collection c = (Collection) property; c.add(getPropertyDataSource().getItem().getEntity()); itemProperty.setValue(c); } } } /** * Checks if the manytomany relation is owned by this side of the property. * As a side effect detects the name of the owner property if the relation * is owned by the other side. * * @return false if bidirectional connection and the mapping has a mappedBy * parameter. */ private boolean isOwningSide() { if (owningSide == null) { Class<?> entityClass = getPropertyDataSource().getItem().getContainer().getEntityClass(); EntityClassMetadata<?> entityClassMetadata = MetadataFactory.getInstance().getEntityClassMetadata( entityClass); PropertyMetadata property = entityClassMetadata.getProperty(getPropertyDataSource().getPropertyId()); ManyToMany annotation = property.getAnnotation(ManyToMany.class); if (annotation != null) { if (annotation.mappedBy() != null && !annotation.mappedBy().isEmpty()) { owningSide = Boolean.FALSE; mappedBy = annotation.mappedBy(); return owningSide; } } else { OneToMany annotation2 = property.getAnnotation(OneToMany.class); if (annotation2 != null) { if (annotation2.mappedBy() != null && !annotation2.mappedBy().isEmpty()) { owningSide = Boolean.FALSE; mappedBy = annotation2.mappedBy(); return owningSide; } } } owningSide = Boolean.TRUE; } return owningSide; } @SuppressWarnings("rawtypes") static Collection createNewCollectionForType(Class<?> type) throws InstantiationException, IllegalAccessException { if (type.isInterface()) { if (type == Set.class) { return new HashSet(); } else if (type == List.class) { return new ArrayList(); } else { throw new RuntimeException("Couldn't instantiate a collection for property."); } } return (Collection) type.newInstance(); } @SuppressWarnings("unchecked") @Override public Class<Collection<T>> getModelType() { if (getPropertyDataSource() != null) { return getPropertyDataSource().getType(); } return this.type; } @SuppressWarnings("unchecked") @Override public Class<Collection<Object>> getPresentationType() { if (getPropertyDataSource() != null) { return getPropertyDataSource().getType(); } return this.type; } }