/** * 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.fieldfactory; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.logging.Logger; import javax.persistence.Embedded; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.Type; import com.vaadin.addon.jpacontainer.EntityContainer; import com.vaadin.addon.jpacontainer.EntityManagerProvider; import com.vaadin.addon.jpacontainer.EntityProvider; import com.vaadin.addon.jpacontainer.JPAContainer; import com.vaadin.addon.jpacontainer.JPAContainerFactory; import com.vaadin.addon.jpacontainer.JPAContainerItem; import com.vaadin.addon.jpacontainer.LazyLoadingDelegate; import com.vaadin.addon.jpacontainer.metadata.PropertyKind; import com.vaadin.addon.jpacontainer.provider.jndijta.CachingBatchableEntityProvider; import com.vaadin.addon.jpacontainer.provider.jndijta.CachingMutableEntityProvider; import com.vaadin.addon.jpacontainer.provider.jndijta.JndiJtaProvider; import com.vaadin.v7.data.Container; import com.vaadin.v7.data.Item; import com.vaadin.v7.ui.AbstractSelect; import com.vaadin.v7.ui.AbstractTextField; import com.vaadin.ui.Component; import com.vaadin.v7.ui.DefaultFieldFactory; import com.vaadin.v7.ui.Field; import com.vaadin.v7.ui.Form; import com.vaadin.v7.ui.FormFieldFactory; import com.vaadin.v7.ui.NativeSelect; import com.vaadin.v7.ui.Table; import com.vaadin.v7.ui.TableFieldFactory; /** * A {@link FormFieldFactory} and {@link TableFieldFactory} implementation * suitable for JPAContainer users. * <p> * As where the {@link DefaultFieldFactory} in Vaadin can only handle basic data * types, this field factory can also automatically create proper fields for * various referenced entities. This greatly speeds up construction of CRUD * views. Below are field types supported by this FieldFactory: * <p> * * <dl> * <dt><b>@ManyToOne</b></dt> * <dd> * Creates a select backed up by a JPAContainer listing all entities of * referenced type. An id->pojo converter is used to automatically convert * identifiers to actual referenced entity objects. * <p> * Example of a mapped property: @ManyToOne private Address address;<br> * Default type: {@link NativeSelect} * <p> * Created by * {@link #createManyToOneField(EntityContainer, Object, Object, Component)} * method. * <p> * The method * {@link #constructReferenceSelect(EntityContainer, Object, Object, Component, Class)} * can be used to override the select type. The type can also be set per * reference type with {@link #setMultiSelectType(Class, Class)}. * <p></dd> * <dt><b>@ManyToMany</b></dt> * <dd> * Creates a multiselect backed up by a JPAContainer listing all entities of the * type in the collection. Selected entities will be reflected to the collection * field in the entity using an id->pojo converter. * <p> * Example of a mapped property: @ManyToMany private Set<Address> * addresses; * <p> * Default type: {@link Table} (in multiselect mode) * <p> * Created by * {@link #createManyToManyField(EntityContainer, Object, Object, Component)} * method. * <p> * The method * {@link #constructCollectionSelect(EntityContainer, Object, Object, Component, Class)} * can be used to override the select type. Type can also be set per reference * type with {@link #setMultiSelectType(Class, Class)}. * <p></dd> * <dt><b>@OneToMany</b></dt> * <dd> * Creates a custom field based on Table to edit referenced entities "owned" by * the master entity. The table lists only entities which belong to entity being * currently edited. Referenced entities are editable straight in the table and * instances can be removed and added. * <p> * Example of a mapped property: @OneToMany(mappedBy="person", * cascade=CascadeType.ALL, orphanRemoval = true) private Set<Address> * addresses; * <p> * Default type: {@link MasterDetailEditor} (in multiselect mode) * <p> * Created by * {@link #createOneToManyField(EntityContainer, Object, Object, Component)} * method. * <p> * Some things to note: * <ul> * <li>Creation of new entities uses empty parameter constructor.</li> * <li>The master detail editor expects the referenced entity to have a * "back reference" to the owner. It needs to be specified in with mappedBy * parameter in the annotation or it to be "naturally named" (master type * starting in lower case).</li> * <li>Map type is not currently supported by the {@link MasterDetailEditor}.</li> * </ul> * <p></dd> * <dt><b>@OneToOne</b></dt> * <dd> * Creates a sub form for the referenced type. If the value is initially null, * the sub form tries to create one with empty parameter constructor. * <p> * Example of a mapped property: @OneToOne private Address address; * <p> * * Default type: {@link OneToOneForm} * <p> * Created by * {@link #createOneToOneField(EntityContainer, Object, Object, Component)} * method. * <p> * * </dd> * <dt><b>@Embedded</b></dt> * <dd> * Creates a sub form for the referenced embedded type. This is closely related * to @OneToOne reference. If the value is initially null, the sub form tries to * create one with empty parameter constructor. * <p> * Example of a mapped property: @Embedded private Address address; * <p> * * Default type: {@link EmbeddedForm} * <p> * Created by * {@link #createEmbeddedField(EntityContainer, Object, Object, Component)} * method.</dd> * * <dt><b>@ElementCollection</b></dt> * <dd> * Creates a custom field based on Table to edit referenced embeddables "owned" * by the master entity. Embeddables can be basic datatypes or types annotated * with @Embeddable annotation. This is closely related to the master detail * editor created for OneToMany relations. The table lists embeddables in * element collection. Referenced entities are editable straight in the table * and instances can be removed and added. * <p> * Example of a mapped property: @ElementCollection private Set<Address> * addresses; * <p> * Default type: {@link ElementCollectionEditor} * <p> * Created by * {@link #createElementCollectionField(EntityContainer, Object, Object, Component)} * method. * <p> * Note that creation of new elements uses empty paramater constructor. Also the * {@link ElementCollectionEditor} does not currently support Map type element * collection.</dd> * </dl> * * <p> * FieldFactory works recursively. E.g. sub forms or {@link MasterDetailEditor}s * it creates uses the same fieldfactory by default. When using the class in * such conditions one often wants to use * {@link #setVisibleProperties(Class, String...)} to configure created fields. * */ @SuppressWarnings("rawtypes") public class FieldFactory extends DefaultFieldFactory { private HashMap<Class<?>, String[]> propertyOrders; private HashMap<Class<?>, Class<? extends AbstractSelect>> multiselectTypes; private HashMap<Class<?>, Class<? extends AbstractSelect>> singleselectTypes; private EntityManagerProvider entityManagerProvider; /** * Creates a new instance of a {@link FieldFactory}. */ public FieldFactory() { } @Override public Field createField(Item item, Object propertyId, Component uiContext) { Field field; if (item instanceof JPAContainerItem) { JPAContainerItem jpaitem = (JPAContainerItem) item; EntityContainer container = jpaitem.getContainer(); field = createJPAContainerBackedField(jpaitem.getItemId(), propertyId, container, uiContext); if (field != null) { return field; } } else { field = createRelationFieldForEmbeddableEditor(item, propertyId, uiContext); if (field != null) { return field; } } if ("id".equals(propertyId)) { return createIdentifierField(); } field = createEnumSelect(item.getItemProperty(propertyId).getType(), propertyId); if (field == null) { field = super.createField(item, propertyId, uiContext); } return configureBasicFields(field); } /** * @param type * @param propertyId * @return */ protected Field createEnumSelect(Class<?> type, Object propertyId) { if (type.isEnum()) { AbstractSelect select = constructCollectionSelect(null, null, propertyId, null, type); populateEnums(type, select); select.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); return select; } return null; } private void populateEnums(Class<?> type, AbstractSelect select) { List<?> asList = Arrays.asList(type.getEnumConstants()); for (Object object : asList) { select.addItem(object); } } @SuppressWarnings("unchecked") private Field createRelationFieldForEmbeddableEditor(Item item, Object propertyId, Component uiContext) { EmbeddableEditor embeddableEditor = getEmbeddableEditor(uiContext); if (embeddableEditor == null) { return null; } Class<?> embeddedClassType = embeddableEditor.getEmbeddedClassType(); PropertyKind propertyKind = detectPropertyKind(embeddedClassType, propertyId); switch (propertyKind) { case MANY_TO_ONE: JPAContainer container = createJPAContainerFor( embeddableEditor.getMasterEntityContainer(), item .getItemProperty(propertyId).getType(), false); AbstractSelect select = constructReferenceSelect( embeddableEditor.getMasterEntityContainer(), null, propertyId, uiContext, embeddedClassType); select.setMultiSelect(false); select.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); select.setItemCaptionMode(NativeSelect.ITEM_CAPTION_MODE_ITEM); select.setContainerDataSource(container); select.setConverter(new SingleSelectConverter(select)); return select; default: break; } return null; } private PropertyKind detectPropertyKind(Class<?> embeddedClassType, Object propertyId) { java.lang.reflect.Field[] declaredFields = embeddedClassType .getDeclaredFields(); for (java.lang.reflect.Field field : declaredFields) { if (field.getName().equals(propertyId)) { if (field.getAnnotation(ManyToOne.class) != null) { return PropertyKind.MANY_TO_ONE; } else if (field.getAnnotation(Embedded.class) != null) { // TODO embedded in embedded ? } else if (field.getAnnotation(ManyToMany.class) != null) { // TODO unidirectional multiselect is a possible use case // also? } break; } } return PropertyKind.SIMPLE; } private EmbeddableEditor getEmbeddableEditor(Component uiContext) { if (uiContext == null) { return null; } if (uiContext instanceof EmbeddableEditor) { return (EmbeddableEditor) uiContext; } return getEmbeddableEditor(uiContext.getParent()); } /** * This method creates field for identifier property. The default * implementation does nothing. We expect identifiers to be assigned * automatically. This method can be overridden to change the behavior. * * @return the field for identifier property */ protected Field createIdentifierField() { return null; } /** * This method can be used to configure field generated by the * DefaultFieldFactory. By default it sets null representation of textfields * to empty string instead of 'null'. * * @param field * @return */ protected Field configureBasicFields(Field field) { if (field instanceof AbstractTextField) { ((AbstractTextField) field).setNullRepresentation(""); } return field; } @Override public Field createField(Container container, Object itemId, Object propertyId, Component uiContext) { Field field; if (container instanceof EntityContainer) { EntityContainer jpacontainer = (EntityContainer) container; field = createJPAContainerBackedField(itemId, propertyId, jpacontainer, uiContext); if (field != null) { return field; } } else { field = createRelationFieldForEmbeddableEditor( container.getItem(itemId), propertyId, uiContext); if (field != null) { return field; } } field = createEnumSelect(container.getType(propertyId), propertyId); if (field == null) { field = super.createField(container, itemId, propertyId, uiContext); } return configureBasicFields(field); } private Field createJPAContainerBackedField(Object itemId, Object propertyId, EntityContainer jpacontainer, Component uiContext) { Field field = null; PropertyKind propertyKind = jpacontainer.getPropertyKind(propertyId); switch (propertyKind) { case MANY_TO_ONE: field = createManyToOneField(jpacontainer, itemId, propertyId, uiContext); break; case ONE_TO_ONE: field = createOneToOneField(jpacontainer, itemId, propertyId, uiContext); break; case ONE_TO_MANY: field = createOneToManyField(jpacontainer, itemId, propertyId, uiContext); break; case MANY_TO_MANY: field = createManyToManyField(jpacontainer, itemId, propertyId, uiContext); break; case ELEMENT_COLLECTION: field = createElementCollectionField(jpacontainer, itemId, propertyId, uiContext); break; case EMBEDDED: field = createEmbeddedField(jpacontainer, itemId, propertyId, uiContext); break; default: break; } return field; } protected Field createEmbeddedField(EntityContainer jpacontainer, Object itemId, Object propertyId, Component uiContext) { // embedded fields are displayed in a sub form EmbeddedForm embeddedForm = new EmbeddedForm(this, jpacontainer); embeddedForm.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); return embeddedForm; } protected OneToOneForm createOneToOneField(EntityContainer<?> jpacontainer, Object itemId, Object propertyId, Component uiContext) { OneToOneForm oneToOneForm = new OneToOneForm(); oneToOneForm.setBackReferenceId(jpacontainer.getEntityClass() .getSimpleName().toLowerCase()); oneToOneForm.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); oneToOneForm.setFormFieldFactory(this); if (uiContext instanceof Form) { // write buffering is configure by Form after binding the data // source. Yes, you may read the previous sentence again or verify // this from the Vaadin code if you don't believe what you just // read. // As oneToOneForm creates the referenced type on demand if required // the buffering state needs to be available when proeprty is set // (otherwise the original master entity will be modified once the // form is opened). Form f = (Form) uiContext; oneToOneForm.setBuffered(f.isBuffered()); } return oneToOneForm; } @SuppressWarnings({ "serial" }) protected Field createManyToManyField(EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext) { /* * Detect what kind of reference type we have */ Class masterEntityClass = containerForProperty.getEntityClass(); Class referencedType = detectReferencedType( getEntityManagerFactory(containerForProperty), propertyId, masterEntityClass); final JPAContainer container = createJPAContainerFor( containerForProperty, referencedType, false); final AbstractSelect select = constructCollectionSelect( containerForProperty, itemId, propertyId, uiContext, referencedType); select.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); select.setContainerDataSource(container); // many to many, selectable from table listing all existing pojos select.setConverter(new MultiSelectConverter(select)); select.setMultiSelect(true); if (select instanceof Table) { Table t = (Table) select; t.setSelectable(true); Object[] visibleProperties = getVisibleProperties(referencedType); if (visibleProperties == null) { List<Object> asList = new ArrayList<Object>(Arrays.asList(t .getVisibleColumns())); asList.remove("id"); // TODO this should be the true "back reference" field from the // opposite direction, now we expect convention String simpleName = masterEntityClass.getSimpleName(); String backrefpropname = simpleName.substring(0, 1) .toLowerCase() + simpleName.substring(1); final String backReferencePropertyId = backrefpropname + "s"; asList.remove(backReferencePropertyId); visibleProperties = asList.toArray(); } t.setVisibleColumns(visibleProperties); } else { select.setItemCaptionMode(AbstractSelect.ITEM_CAPTION_MODE_ITEM); } return select; } protected Field createOneToManyField(EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext) { return new MasterDetailEditor(this, containerForProperty, itemId, propertyId, uiContext); } protected Field createElementCollectionField( EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext) { Class referencedType = detectReferencedType(containerForProperty .getEntityProvider().getEntityManager() .getEntityManagerFactory(), propertyId, containerForProperty.getEntityClass()); if (referencedType.isEnum()) { AbstractSelect collectionSelect = constructCollectionSelect(null, itemId, propertyId, uiContext, referencedType); collectionSelect.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); populateEnums(referencedType, collectionSelect); collectionSelect.setMultiSelect(true); if (List.class.isAssignableFrom(containerForProperty .getType(propertyId))) { collectionSelect.setPropertyDataSource(new ListTranslator( collectionSelect)); } if (collectionSelect instanceof Table) { Table t = (Table) collectionSelect; t.setColumnHeaderMode(Table.COLUMN_HEADER_MODE_HIDDEN); t.setSelectable(true); t.setRowHeaderMode(Table.ROW_HEADER_MODE_ID); } return collectionSelect; } else { return new ElementCollectionEditor(this, containerForProperty, itemId, propertyId, uiContext); } } /** * Detects the type entities in "collection types" (oneToMany, ManyToMany). * * @param propertyId * @param masterEntityClass * @return the type of entities in collection type */ protected Class detectReferencedType(EntityManagerFactory emf, Object propertyId, Class masterEntityClass) { Class referencedType = null; Metamodel metamodel = emf.getMetamodel(); Set<EntityType<?>> entities = metamodel.getEntities(); for (EntityType<?> entityType : entities) { Class<?> javaType = entityType.getJavaType(); if (javaType == masterEntityClass) { Attribute<?, ?> attribute = entityType.getAttribute(propertyId .toString()); PluralAttribute pAttribute = (PluralAttribute) attribute; Type elementType = pAttribute.getElementType(); referencedType = elementType.getJavaType(); break; } } return referencedType; } protected EntityManagerFactory getEntityManagerFactory( EntityContainer<?> containerForProperty) { return containerForProperty.getEntityProvider().getEntityManager() .getEntityManagerFactory(); } /** * Creates a field for simple reference (ManyToOne) * * @param containerForProperty * @param propertyId * @return */ protected Field createManyToOneField(EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext) { Class<?> type = containerForProperty.getType(propertyId); JPAContainer container = createJPAContainerFor(containerForProperty, type, false); AbstractSelect nativeSelect = constructReferenceSelect( containerForProperty, itemId, propertyId, uiContext, type); nativeSelect.setMultiSelect(false); nativeSelect.setCaption(DefaultFieldFactory .createCaptionByPropertyId(propertyId)); nativeSelect.setItemCaptionMode(NativeSelect.ITEM_CAPTION_MODE_ITEM); nativeSelect.setContainerDataSource(container); nativeSelect.setConverter(new SingleSelectConverter(nativeSelect)); return nativeSelect; } protected AbstractSelect constructReferenceSelect( EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext, Class<?> type) { if (singleselectTypes != null) { Class<? extends AbstractSelect> class1 = singleselectTypes .get(type); if (class1 != null) { try { return class1.newInstance(); } catch (Exception e) { Logger.getLogger(getClass().getName()).warning( "Could not create select of type " + class1.getName()); } } } return new NativeSelect(); } protected AbstractSelect constructCollectionSelect( EntityContainer containerForProperty, Object itemId, Object propertyId, Component uiContext, Class<?> type) { if (multiselectTypes != null) { Class<? extends AbstractSelect> class1 = multiselectTypes.get(type); try { return class1.newInstance(); } catch (Exception e) { Logger.getLogger(getClass().getName()).warning( "Could not create select of type " + class1.getName()); } } Table table = new Table(); table.setPageLength(5); return table; } /** * This method creates new JPAContainer instances to be used in fields * generated by this FieldFactory. * <p> * After setting up the container (with provider) the method configures it * with {@link #configureContainer(EntityContainer, JPAContainer)} method. * <p> * If you need to use JPAContaiener with some special settings (e.g. * customized EntityProvider) you should override this method. * * @param referenceContainer * most commonly this is the container for which property the * field is being created. The default implementation uses this * extensively to decide the new JPAContainer should be * configured. * @param type * the entity type to be listed in the container * @param buffered * true if the container should be "buffered" (e.g. for a * {@link MasterDetailEditor} that is used in a buffered Form). * @return new JPAContainer for given type that is to be used in a relation * field */ @SuppressWarnings("unchecked") protected JPAContainer<?> createJPAContainerFor( EntityContainer<?> referenceContainer, Class<?> type, boolean buffered) { JPAContainer<?> container = null; EntityProvider<?> referenceEntityProvider = referenceContainer .getEntityProvider(); if (referenceEntityProvider instanceof JndiJtaProvider) { JndiJtaProvider jndiProvider = (JndiJtaProvider) referenceEntityProvider; container = new JPAContainer(type); JndiJtaProvider entityProvider; if (buffered) { entityProvider = new CachingBatchableEntityProvider(type); } else { entityProvider = new CachingMutableEntityProvider(type); } // copy settings from parent provider entityProvider.setJndiAddresses(jndiProvider.getJndiAddresses()); container.setEntityProvider(entityProvider); } else { EntityManager em = referenceEntityProvider.getEntityManager(); if (buffered) { container = JPAContainerFactory.makeBatchable(type, em); } else { container = JPAContainerFactory.make(type, em); } } configureContainer(referenceContainer, container); return container; } /** * This method does additional configurations for the container instantiated * for a field. By default it copies the registered * {@link LazyLoadingDelegate} and {@link EntityManagerProvider} from the * reference container. * * @param referenceContainer * @param container */ protected void configureContainer(EntityContainer<?> referenceContainer, JPAContainer<?> container) { // Set the lazy loading delegate to the same as the parent. container.getEntityProvider() .setLazyLoadingDelegate( referenceContainer.getEntityProvider() .getLazyLoadingDelegate()); // Set the entity manager provider (if applicable) EntityManagerProvider registeredEMP = referenceContainer .getEntityProvider().getEntityManagerProvider(); if (registeredEMP != null) { container.getEntityProvider().setEntityManager(null); container.getEntityProvider().setEntityManagerProvider( registeredEMP); } } /** * Configures visible properties and their order for fields created for * reference/collection types referencing to given entity type. This order * is for example used by {@link Table}s created for OneToMany or ManyToMany * reference types. * * @param containerType * the entity type for which the visible properties will be set * @param propertyIdentifiers * the identifiers in wished order to be displayed */ public void setVisibleProperties(Class<?> containerType, String... propertyIdentifiers) { if (propertyOrders == null) { propertyOrders = new HashMap<Class<?>, String[]>(); } propertyOrders.put(containerType, propertyIdentifiers); } public void setMultiSelectType(Class<?> referenceType, Class<? extends AbstractSelect> selectType) { if (multiselectTypes == null) { multiselectTypes = new HashMap<Class<?>, Class<? extends AbstractSelect>>(); } multiselectTypes.put(referenceType, selectType); } public void setSingleSelectType(Class<?> referenceType, Class<? extends AbstractSelect> selectType) { if (singleselectTypes == null) { singleselectTypes = new HashMap<Class<?>, Class<? extends AbstractSelect>>(); } singleselectTypes.put(referenceType, selectType); } /** * Returns customized visible properties (and their order) for given entity * type. * * @param containerType * @return property identifiers that are configured to be displayed */ public String[] getVisibleProperties(Class<?> containerType) { if (propertyOrders != null) { return propertyOrders.get(containerType); } return null; } }