diff options
author | Artur Signell <artur@vaadin.com> | 2016-08-18 22:10:47 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-08-20 00:12:18 +0300 |
commit | fe3dca081a64af892a7f4c0416ecc643aec3ec5a (patch) | |
tree | 1901fb377336d3c5a772335322d9c434a4a75e24 /compatibility-server | |
parent | 65370e12a0605926cb80e205c2b0e74fefe83e5b (diff) | |
download | vaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.tar.gz vaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.zip |
Move remaining selects and container implementations to compatibility package
Because of dependencies also moves
Calendar, ColorPicker, SQLContainer, container filters
Change-Id: I0594cb24f20486ebbca4be578827fea7cdf92108
Diffstat (limited to 'compatibility-server')
111 files changed, 30340 insertions, 0 deletions
diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/AbstractBeanContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractBeanContainer.java new file mode 100644 index 0000000000..995e2f8675 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractBeanContainer.java @@ -0,0 +1,929 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Filterable; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Container.SimpleFilterable; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.MethodProperty.MethodException; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * An abstract base class for in-memory containers for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class and explicitly adding or removing properties is not + * supported. Only beans of the same type can be added to the container. + * </p> + * + * <p> + * Subclasses should implement any public methods adding items to the container, + * typically calling the protected methods {@link #addItem(Object, Object)}, + * {@link #addItemAfter(Object, Object, Object)} and + * {@link #addItemAt(int, Object, Object)}. + * </p> + * + * @param <IDTYPE> + * The type of the item identifier + * @param <BEANTYPE> + * The type of the Bean + * + * @since 6.5 + */ +public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> + extends AbstractInMemoryContainer<IDTYPE, String, BeanItem<BEANTYPE>> + implements Filterable, SimpleFilterable, Sortable, ValueChangeListener, + PropertySetChangeNotifier { + + /** + * Resolver that maps beans to their (item) identifiers, removing the need + * to explicitly specify item identifiers when there is no need to customize + * this. + * + * Note that beans can also be added with an explicit id even if a resolver + * has been set. + * + * @param <IDTYPE> + * @param <BEANTYPE> + * + * @since 6.5 + */ + public static interface BeanIdResolver<IDTYPE, BEANTYPE> + extends Serializable { + /** + * Return the item identifier for a bean. + * + * @param bean + * @return + */ + public IDTYPE getIdForBean(BEANTYPE bean); + } + + /** + * A item identifier resolver that returns the value of a bean property. + * + * The bean must have a getter for the property, and the getter must return + * an object of type IDTYPE. + */ + protected class PropertyBasedBeanIdResolver + implements BeanIdResolver<IDTYPE, BEANTYPE> { + + private final Object propertyId; + + public PropertyBasedBeanIdResolver(Object propertyId) { + if (propertyId == null) { + throw new IllegalArgumentException( + "Property identifier must not be null"); + } + this.propertyId = propertyId; + } + + @Override + @SuppressWarnings("unchecked") + public IDTYPE getIdForBean(BEANTYPE bean) + throws IllegalArgumentException { + VaadinPropertyDescriptor<BEANTYPE> pd = model.get(propertyId); + if (null == pd) { + throw new IllegalStateException( + "Property " + propertyId + " not found"); + } + try { + Property<IDTYPE> property = (Property<IDTYPE>) pd + .createProperty(bean); + return property.getValue(); + } catch (MethodException e) { + throw new IllegalArgumentException(e); + } + } + + } + + /** + * The resolver that finds the item ID for a bean, or null not to use + * automatic resolving. + * + * Methods that add a bean without specifying an ID must not be called if no + * resolver has been set. + */ + private BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver = null; + + /** + * Maps all item ids in the container (including filtered) to their + * corresponding BeanItem. + */ + private final Map<IDTYPE, BeanItem<BEANTYPE>> itemIdToItem = new HashMap<IDTYPE, BeanItem<BEANTYPE>>(); + + /** + * The type of the beans in the container. + */ + private final Class<? super BEANTYPE> type; + + /** + * A description of the properties found in beans of type {@link #type}. + * Determines the property ids that are present in the container. + */ + private final LinkedHashMap<String, VaadinPropertyDescriptor<BEANTYPE>> model; + + /** + * Constructs a {@code AbstractBeanContainer} for beans of the given type. + * + * @param type + * the type of the beans that will be added to the container. + * @throws IllegalArgumentException + * If {@code type} is null + */ + protected AbstractBeanContainer(Class<? super BEANTYPE> type) { + if (type == null) { + throw new IllegalArgumentException( + "The bean type passed to AbstractBeanContainer must not be null"); + } + this.type = type; + model = BeanItem.getPropertyDescriptors((Class<BEANTYPE>) type); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + @Override + public Class<?> getType(Object propertyId) { + VaadinPropertyDescriptor<BEANTYPE> descriptor = model.get(propertyId); + if (descriptor == null) { + return null; + } + return descriptor.getPropertyType(); + } + + /** + * Create a BeanItem for a bean using pre-parsed bean metadata (based on + * {@link #getBeanType()}). + * + * @param bean + * @return created {@link BeanItem} or null if bean is null + */ + protected BeanItem<BEANTYPE> createBeanItem(BEANTYPE bean) { + return bean == null ? null : new BeanItem<BEANTYPE>(bean, model); + } + + /** + * Returns the type of beans this Container can contain. + * + * This comes from the bean type constructor parameter, and bean metadata + * (including container properties) is based on this. + * + * @return + */ + public Class<? super BEANTYPE> getBeanType() { + return type; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + @Override + public Collection<String> getContainerPropertyIds() { + return model.keySet(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() { + int origSize = size(); + IDTYPE firstItem = getFirstVisibleItem(); + + internalRemoveAllItems(); + + // detach listeners from all Items + for (Item item : itemIdToItem.values()) { + removeAllValueChangeListeners(item); + } + itemIdToItem.clear(); + + // fire event only if the visible view changed, regardless of whether + // filtered out items were removed or not + if (origSize != 0) { + fireItemsRemoved(0, firstItem, origSize); + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + @Override + public BeanItem<BEANTYPE> getItem(Object itemId) { + // TODO return only if visible? + return getUnfilteredItem(itemId); + } + + @Override + protected BeanItem<BEANTYPE> getUnfilteredItem(Object itemId) { + return itemIdToItem.get(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItemIds() + */ + @Override + @SuppressWarnings("unchecked") + public List<IDTYPE> getItemIds() { + return (List<IDTYPE>) super.getItemIds(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + Item item = getItem(itemId); + if (item == null) { + return null; + } + return item.getItemProperty(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) { + // TODO should also remove items that are filtered out + int origSize = size(); + Item item = getItem(itemId); + int position = indexOfId(itemId); + + if (internalRemoveItem(itemId)) { + // detach listeners from Item + removeAllValueChangeListeners(item); + + // remove item + itemIdToItem.remove(itemId); + + // fire event only if the visible view changed, regardless of + // whether filtered out items were removed or not + if (size() != origSize) { + fireItemRemoved(position, itemId); + } + + return true; + } else { + return false; + } + } + + /** + * Re-filter the container when one of the monitored properties changes. + */ + @Override + public void valueChange(ValueChangeEvent event) { + // if a property that is used in a filter is changed, refresh filtering + filterAll(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#addContainerFilter(java.lang.Object, + * java.lang.String, boolean, boolean) + */ + @Override + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#removeAllContainerFilters() + */ + @Override + public void removeAllContainerFilters() { + if (!getFilters().isEmpty()) { + for (Item item : itemIdToItem.values()) { + removeAllValueChangeListeners(item); + } + removeAllFilters(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Filterable#removeContainerFilters(java.lang + * .Object) + */ + @Override + public void removeContainerFilters(Object propertyId) { + Collection<Filter> removedFilters = super.removeFilters(propertyId); + if (!removedFilters.isEmpty()) { + // stop listening to change events for the property + for (Item item : itemIdToItem.values()) { + removeValueChangeListener(item, propertyId); + } + } + } + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + @Override + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#hasContainerFilters() + */ + @Override + public boolean hasContainerFilters() { + return super.hasContainerFilters(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return super.getContainerFilters(); + } + + /** + * Make this container listen to the given property provided it notifies + * when its value changes. + * + * @param item + * The {@link Item} that contains the property + * @param propertyId + * The id of the property + */ + private void addValueChangeListener(Item item, Object propertyId) { + Property<?> property = item.getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + // avoid multiple notifications for the same property if + // multiple filters are in use + ValueChangeNotifier notifier = (ValueChangeNotifier) property; + notifier.removeListener(this); + notifier.addListener(this); + } + } + + /** + * Remove this container as a listener for the given property. + * + * @param item + * The {@link Item} that contains the property + * @param propertyId + * The id of the property + */ + private void removeValueChangeListener(Item item, Object propertyId) { + Property<?> property = item.getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property).removeListener(this); + } + } + + /** + * Remove this contains as a listener for all the properties in the given + * {@link Item}. + * + * @param item + * The {@link Item} that contains the properties + */ + private void removeAllValueChangeListeners(Item item) { + for (Object propertyId : item.getItemPropertyIds()) { + removeValueChangeListener(item, propertyId); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getSortablePropertyIds(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sortContainer(propertyId, ascending); + } + + @Override + public ItemSorter getItemSorter() { + return super.getItemSorter(); + } + + @Override + public void setItemSorter(ItemSorter itemSorter) { + super.setItemSorter(itemSorter); + } + + @Override + protected void registerNewItem(int position, IDTYPE itemId, + BeanItem<BEANTYPE> item) { + itemIdToItem.put(itemId, item); + + // add listeners to be able to update filtering on property + // changes + for (Filter filter : getFilters()) { + for (String propertyId : getContainerPropertyIds()) { + if (filter.appliesToProperty(propertyId)) { + // addValueChangeListener avoids adding duplicates + addValueChangeListener(item, propertyId); + } + } + } + } + + /** + * Check that a bean can be added to the container (is of the correct type + * for the container). + * + * @param bean + * @return + */ + private boolean validateBean(BEANTYPE bean) { + return bean != null && getBeanType().isAssignableFrom(bean.getClass()); + } + + /** + * Adds the bean to the Container. + * + * Note: the behavior of this method changed in Vaadin 6.6 - now items are + * added at the very end of the unfiltered container and not after the last + * visible item if filtering is used. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + protected BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAtEnd(itemId, createBeanItem(bean), true); + } + + /** + * Adds the bean after the given bean. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + protected BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId, + IDTYPE newItemId, BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAfter(previousItemId, newItemId, + createBeanItem(bean), true); + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The item id for the bean to add to the container. + * @param bean + * The bean to add to the container. + * + * @return Returns the new BeanItem or null if the operation fails. + */ + protected BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId, + BEANTYPE bean) { + if (!validateBean(bean)) { + return null; + } + return internalAddItemAt(index, newItemId, createBeanItem(bean), true); + } + + /** + * Adds a bean to the container using the bean item id resolver to find its + * identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItem(Object, Object) + * + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBean(BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItem(itemId, bean); + } + + /** + * Adds a bean to the container after a specified item identifier, using the + * bean item id resolver to find its identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItemAfter(Object, Object, Object) + * + * @param previousItemId + * the identifier of the bean after which this bean should be + * added, null to add to the beginning + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, + BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItemAfter(previousItemId, itemId, bean); + } + + /** + * Adds a bean at a specified (filtered view) position in the container + * using the bean item id resolver to find its identifier. + * + * A bean id resolver must be set before calling this method. + * + * @see #addItemAfter(Object, Object, Object) + * + * @param index + * the index (in the filtered view) at which to add the item + * @param bean + * the bean to add + * @return BeanItem<BEANTYPE> item added or null + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if an identifier cannot be resolved for the bean + */ + protected BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + if (bean == null) { + return null; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + return addItemAt(index, itemId, bean); + } + + /** + * Adds all the beans from a {@link Collection} in one operation using the + * bean item identifier resolver. More efficient than adding them one by + * one. + * + * A bean id resolver must be set before calling this method. + * + * Note: the behavior of this method changed in Vaadin 6.6 - now items are + * added at the very end of the unfiltered container and not after the last + * visible item if filtering is used. + * + * @param collection + * The collection of beans to add. Must not be null. + * @throws IllegalStateException + * if no bean identifier resolver has been set + * @throws IllegalArgumentException + * if the resolver returns a null itemId for one of the beans in + * the collection + */ + protected void addAll(Collection<? extends BEANTYPE> collection) + throws IllegalStateException, IllegalArgumentException { + boolean modified = false; + int origSize = size(); + + for (BEANTYPE bean : collection) { + // TODO skipping invalid beans - should not allow them in javadoc? + if (bean == null + || !getBeanType().isAssignableFrom(bean.getClass())) { + continue; + } + IDTYPE itemId = resolveBeanId(bean); + if (itemId == null) { + throw new IllegalArgumentException( + "Resolved identifier for a bean must not be null"); + } + + if (internalAddItemAtEnd(itemId, createBeanItem(bean), + false) != null) { + modified = true; + } + } + + if (modified) { + // Filter the contents when all items have been added + if (isFiltered()) { + doFilterContainer(!getFilters().isEmpty()); + } + if (visibleNewItemsWasAdded(origSize)) { + // fire event about added items + int firstPosition = origSize; + IDTYPE firstItemId = getVisibleItemIds().get(firstPosition); + int affectedItems = size() - origSize; + fireItemsAdded(firstPosition, firstItemId, affectedItems); + } + } + } + + private boolean visibleNewItemsWasAdded(int origSize) { + return size() > origSize; + } + + /** + * Use the bean resolver to get the identifier for a bean. + * + * @param bean + * @return resolved bean identifier, null if could not be resolved + * @throws IllegalStateException + * if no bean resolver is set + */ + protected IDTYPE resolveBeanId(BEANTYPE bean) { + if (beanIdResolver == null) { + throw new IllegalStateException( + "Bean item identifier resolver is required."); + } + return beanIdResolver.getIdForBean(bean); + } + + /** + * Sets the resolver that finds the item id for a bean, or null not to use + * automatic resolving. + * + * Methods that add a bean without specifying an id must not be called if no + * resolver has been set. + * + * Note that methods taking an explicit id can be used whether a resolver + * has been defined or not. + * + * @param beanIdResolver + * to use or null to disable automatic id resolution + */ + protected void setBeanIdResolver( + BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) { + this.beanIdResolver = beanIdResolver; + } + + /** + * Returns the resolver that finds the item ID for a bean. + * + * @return resolver used or null if automatic item id resolving is disabled + */ + public BeanIdResolver<IDTYPE, BEANTYPE> getBeanIdResolver() { + return beanIdResolver; + } + + /** + * Create an item identifier resolver using a named bean property. + * + * @param propertyId + * property identifier, which must map to a getter in BEANTYPE + * @return created resolver + */ + protected BeanIdResolver<IDTYPE, BEANTYPE> createBeanPropertyResolver( + Object propertyId) { + return new PropertyBasedBeanIdResolver(propertyId); + } + + /** + * @deprecated As of 7.0, replaced by {@link #addPropertySetChangeListener} + **/ + @Deprecated + @Override + public void addListener(Container.PropertySetChangeListener listener) { + addPropertySetChangeListener(listener); + } + + @Override + public void addPropertySetChangeListener( + Container.PropertySetChangeListener listener) { + super.addPropertySetChangeListener(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Deprecated + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + removePropertySetChangeListener(listener); + } + + @Override + public void removePropertySetChangeListener( + Container.PropertySetChangeListener listener) { + super.removePropertySetChangeListener(listener); + } + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Use addNestedContainerProperty(String) to add container properties to a " + + getClass().getSimpleName()); + } + + /** + * Adds a property for the container and all its items. + * + * Primarily for internal use, may change in future versions. + * + * @param propertyId + * @param propertyDescriptor + * @return true if the property was added + */ + protected final boolean addContainerProperty(String propertyId, + VaadinPropertyDescriptor<BEANTYPE> propertyDescriptor) { + if (null == propertyId || null == propertyDescriptor) { + return false; + } + + // Fails if the Property is already present + if (model.containsKey(propertyId)) { + return false; + } + + model.put(propertyId, propertyDescriptor); + for (BeanItem<BEANTYPE> item : itemIdToItem.values()) { + item.addItemProperty(propertyId, + propertyDescriptor.createProperty(item.getBean())); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /** + * Adds a nested container property for the container, e.g. + * "manager.address.street". + * + * All intermediate getters must exist and should return non-null values + * when the property value is accessed. If an intermediate getter returns + * null, a null value will be returned. + * + * @see NestedMethodProperty + * + * @param propertyId + * @return true if the property was added + */ + public boolean addNestedContainerProperty(String propertyId) { + return addContainerProperty(propertyId, + new NestedPropertyDescriptor(propertyId, type)); + } + + /** + * Adds a nested container properties for all sub-properties of a named + * property to the container. The named property itself is removed from the + * model as its subproperties are added. + * + * All intermediate getters must exist and should return non-null values + * when the property value is accessed. If an intermediate getter returns + * null, a null value will be returned. + * + * @see NestedMethodProperty + * @see #addNestedContainerProperty(String) + * + * @param propertyId + */ + @SuppressWarnings("unchecked") + public void addNestedContainerBean(String propertyId) { + Class<?> propertyType = getType(propertyId); + LinkedHashMap<String, VaadinPropertyDescriptor<Object>> pds = BeanItem + .getPropertyDescriptors((Class<Object>) propertyType); + for (String subPropertyId : pds.keySet()) { + String qualifiedPropertyId = propertyId + "." + subPropertyId; + NestedPropertyDescriptor<BEANTYPE> pd = new NestedPropertyDescriptor<BEANTYPE>( + qualifiedPropertyId, (Class<BEANTYPE>) type); + model.put(qualifiedPropertyId, pd); + model.remove(propertyId); + for (BeanItem<BEANTYPE> item : itemIdToItem.values()) { + item.addItemProperty(qualifiedPropertyId, + pd.createProperty(item.getBean())); + item.removeItemProperty(propertyId); + } + } + + // Sends a change event + fireContainerPropertySetChange(); + } + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + // Fails if the Property is not present + if (!model.containsKey(propertyId)) { + return false; + } + + // Removes the Property to Property list and types + model.remove(propertyId); + + // If remove the Property from all Items + for (final Iterator<IDTYPE> i = getAllItemIds().iterator(); i + .hasNext();) { + getUnfilteredItem(i.next()).removeItemProperty(propertyId); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/AbstractContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractContainer.java new file mode 100644 index 0000000000..1995102345 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractContainer.java @@ -0,0 +1,307 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.LinkedList; + +import com.vaadin.data.Container; + +/** + * Abstract container class that manages event listeners and sending events to + * them ({@link PropertySetChangeNotifier}, {@link ItemSetChangeNotifier}). + * + * Note that this class provides the internal implementations for both types of + * events and notifiers as protected methods, but does not implement the + * {@link PropertySetChangeNotifier} and {@link ItemSetChangeNotifier} + * interfaces directly. This way, subclasses can choose not to implement them. + * Subclasses implementing those interfaces should also override the + * corresponding {@link #addListener()} and {@link #removeListener()} methods to + * make them public. + * + * @since 6.6 + */ +public abstract class AbstractContainer implements Container { + + /** + * List of all Property set change event listeners. + */ + private Collection<Container.PropertySetChangeListener> propertySetChangeListeners = null; + + /** + * List of all container Item set change event listeners. + */ + private Collection<Container.ItemSetChangeListener> itemSetChangeListeners = null; + + /** + * An <code>event</code> object specifying the container whose Property set + * has changed. + * + * This class does not provide information about which properties were + * concerned by the change, but subclasses can provide additional + * information about the changes. + */ + protected static class BasePropertySetChangeEvent extends EventObject + implements Container.PropertySetChangeEvent, Serializable { + + protected BasePropertySetChangeEvent(Container source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + /** + * An <code>event</code> object specifying the container whose Item set has + * changed. + * + * This class does not provide information about the exact changes + * performed, but subclasses can add provide additional information about + * the changes. + */ + protected static class BaseItemSetChangeEvent extends EventObject + implements Container.ItemSetChangeEvent, Serializable { + + protected BaseItemSetChangeEvent(Container source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + // PropertySetChangeNotifier + + /** + * Implementation of the corresponding method in + * {@link PropertySetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + protected void addPropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (getPropertySetChangeListeners() == null) { + setPropertySetChangeListeners( + new LinkedList<Container.PropertySetChangeListener>()); + } + getPropertySetChangeListeners().add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addPropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Deprecated + protected void addListener(Container.PropertySetChangeListener listener) { + addPropertySetChangeListener(listener); + } + + /** + * Implementation of the corresponding method in + * {@link PropertySetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see PropertySetChangeNotifier#removeListener(com.vaadin.data.Container. + * PropertySetChangeListener) + */ + protected void removePropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (getPropertySetChangeListeners() != null) { + getPropertySetChangeListeners().remove(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Deprecated + protected void removeListener( + Container.PropertySetChangeListener listener) { + removePropertySetChangeListener(listener); + } + + // ItemSetChangeNotifier + + /** + * Implementation of the corresponding method in + * {@link ItemSetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + protected void addItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (getItemSetChangeListeners() == null) { + setItemSetChangeListeners( + new LinkedList<Container.ItemSetChangeListener>()); + } + getItemSetChangeListeners().add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Deprecated + protected void addListener(Container.ItemSetChangeListener listener) { + addItemSetChangeListener(listener); + } + + /** + * Implementation of the corresponding method in + * {@link ItemSetChangeNotifier}, override with the corresponding public + * method and implement the interface to use this. + * + * @see ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + protected void removeItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (getItemSetChangeListeners() != null) { + getItemSetChangeListeners().remove(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Deprecated + protected void removeListener(Container.ItemSetChangeListener listener) { + removeItemSetChangeListener(listener); + } + + /** + * Sends a simple Property set change event to all interested listeners. + */ + protected void fireContainerPropertySetChange() { + fireContainerPropertySetChange(new BasePropertySetChangeEvent(this)); + } + + /** + * Sends a Property set change event to all interested listeners. + * + * Use {@link #fireContainerPropertySetChange()} instead of this method + * unless additional information about the exact changes is available and + * should be included in the event. + * + * @param event + * the property change event to send, optionally with additional + * information + */ + protected void fireContainerPropertySetChange( + Container.PropertySetChangeEvent event) { + if (getPropertySetChangeListeners() != null) { + final Object[] l = getPropertySetChangeListeners().toArray(); + for (int i = 0; i < l.length; i++) { + ((Container.PropertySetChangeListener) l[i]) + .containerPropertySetChange(event); + } + } + } + + /** + * Sends a simple Item set change event to all interested listeners, + * indicating that anything in the contents may have changed (items added, + * removed etc.). + */ + protected void fireItemSetChange() { + fireItemSetChange(new BaseItemSetChangeEvent(this)); + } + + /** + * Sends an Item set change event to all registered interested listeners. + * + * @param event + * the item set change event to send, optionally with additional + * information + */ + protected void fireItemSetChange(ItemSetChangeEvent event) { + if (getItemSetChangeListeners() != null) { + final Object[] l = getItemSetChangeListeners().toArray(); + for (int i = 0; i < l.length; i++) { + ((Container.ItemSetChangeListener) l[i]) + .containerItemSetChange(event); + } + } + } + + /** + * Sets the property set change listener collection. For internal use only. + * + * @param propertySetChangeListeners + */ + protected void setPropertySetChangeListeners( + Collection<Container.PropertySetChangeListener> propertySetChangeListeners) { + this.propertySetChangeListeners = propertySetChangeListeners; + } + + /** + * Returns the property set change listener collection. For internal use + * only. + */ + protected Collection<Container.PropertySetChangeListener> getPropertySetChangeListeners() { + return propertySetChangeListeners; + } + + /** + * Sets the item set change listener collection. For internal use only. + * + * @param itemSetChangeListeners + */ + protected void setItemSetChangeListeners( + Collection<Container.ItemSetChangeListener> itemSetChangeListeners) { + this.itemSetChangeListeners = itemSetChangeListeners; + } + + /** + * Returns the item set change listener collection. For internal use only. + */ + protected Collection<Container.ItemSetChangeListener> getItemSetChangeListeners() { + return itemSetChangeListeners; + } + + public Collection<?> getListeners(Class<?> eventType) { + if (Container.PropertySetChangeEvent.class + .isAssignableFrom(eventType)) { + if (propertySetChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertySetChangeListeners); + } + } else if (Container.ItemSetChangeEvent.class + .isAssignableFrom(eventType)) { + if (itemSetChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(itemSetChangeListeners); + } + } + + return Collections.EMPTY_LIST; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/AbstractInMemoryContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractInMemoryContainer.java new file mode 100644 index 0000000000..43b65ab75e --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -0,0 +1,1165 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * Abstract {@link Container} class that handles common functionality for + * in-memory containers. Concrete in-memory container classes can either inherit + * this class, inherit {@link AbstractContainer}, or implement the + * {@link Container} interface directly. + * + * Adding and removing items (if desired) must be implemented in subclasses by + * overriding the appropriate add*Item() and remove*Item() and removeAllItems() + * methods, calling the corresponding + * {@link #internalAddItemAfter(Object, Object, Item)}, + * {@link #internalAddItemAt(int, Object, Item)}, + * {@link #internalAddItemAtEnd(Object, Item, boolean)}, + * {@link #internalRemoveItem(Object)} and {@link #internalRemoveAllItems()} + * methods. + * + * By default, adding and removing container properties is not supported, and + * subclasses need to implement {@link #getContainerPropertyIds()}. Optionally, + * subclasses can override {@link #addContainerProperty(Object, Class, Object)} + * and {@link #removeContainerProperty(Object)} to implement them. + * + * Features: + * <ul> + * <li>{@link Container.Ordered} + * <li>{@link Container.Indexed} + * <li>{@link Filterable} and {@link SimpleFilterable} (internal implementation, + * does not implement the interface directly) + * <li>{@link Sortable} (internal implementation, does not implement the + * interface directly) + * </ul> + * + * To implement {@link Sortable}, subclasses need to implement + * {@link #getSortablePropertyIds()} and call the superclass method + * {@link #sortContainer(Object[], boolean[])} in the method + * <code>sort(Object[], boolean[])</code>. + * + * To implement {@link Filterable}, subclasses need to implement the methods + * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)} + * (calling {@link #addFilter(Filter)}), + * {@link Filterable#removeAllContainerFilters()} (calling + * {@link #removeAllFilters()}) and + * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)} + * (calling {@link #removeFilter(com.vaadin.data.Container.Filter)}). + * + * To implement {@link SimpleFilterable}, subclasses also need to implement the + * methods + * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)} + * and {@link SimpleFilterable#removeContainerFilters(Object)} calling + * {@link #addFilter(com.vaadin.data.Container.Filter)} and + * {@link #removeFilters(Object)} respectively. + * + * @param <ITEMIDTYPE> + * the class of item identifiers in the container, use Object if can + * be any class + * @param <PROPERTYIDCLASS> + * the class of property identifiers for the items in the container, + * use Object if can be any class + * @param <ITEMCLASS> + * the (base) class of the Item instances in the container, use + * {@link Item} if unknown + * + * @since 6.6 + */ +public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITEMCLASS extends Item> + extends AbstractContainer + implements ItemSetChangeNotifier, Container.Indexed { + + /** + * An ordered {@link List} of all item identifiers in the container, + * including those that have been filtered out. + * + * Must not be null. + */ + private List<ITEMIDTYPE> allItemIds; + + /** + * An ordered {@link List} of item identifiers in the container after + * filtering, excluding those that have been filtered out. + * + * This is what the external API of the {@link Container} interface and its + * subinterfaces shows (e.g. {@link #size()}, {@link #nextItemId(Object)}). + * + * If null, the full item id list is used instead. + */ + private List<ITEMIDTYPE> filteredItemIds; + + /** + * Filters that are applied to the container to limit the items visible in + * it + */ + private Set<Filter> filters = new HashSet<Filter>(); + + /** + * The item sorter which is used for sorting the container. + */ + private ItemSorter itemSorter = new DefaultItemSorter(); + + // Constructors + + /** + * Constructor for an abstract in-memory container. + */ + protected AbstractInMemoryContainer() { + setAllItemIds(new ListSet<ITEMIDTYPE>()); + } + + // Container interface methods with more specific return class + + // default implementation, can be overridden + @Override + public ITEMCLASS getItem(Object itemId) { + if (containsId(itemId)) { + return getUnfilteredItem(itemId); + } else { + return null; + } + } + + private static abstract class BaseItemAddOrRemoveEvent extends EventObject + implements Serializable { + protected Object itemId; + protected int index; + protected int count; + + public BaseItemAddOrRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source); + this.itemId = itemId; + this.index = index; + this.count = count; + } + + public Container getContainer() { + return (Container) getSource(); + } + + public Object getFirstItemId() { + return itemId; + } + + public int getFirstIndex() { + return index; + } + + public int getAffectedItemsCount() { + return count; + } + } + + /** + * An <code>Event</code> object specifying information about the added + * items. + * + * <p> + * This class provides information about the first added item and the number + * of added items. + * </p> + * + * @since 7.4 + */ + protected static class BaseItemAddEvent extends BaseItemAddOrRemoveEvent + implements Container.Indexed.ItemAddEvent { + + public BaseItemAddEvent(Container source, Object itemId, int index, + int count) { + super(source, itemId, index, count); + } + + @Override + public int getAddedItemsCount() { + return getAffectedItemsCount(); + } + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + * + * <p> + * This class provides information about the first removed item and the + * number of removed items. + * </p> + * + * @since 7.4 + */ + protected static class BaseItemRemoveEvent extends BaseItemAddOrRemoveEvent + implements Container.Indexed.ItemRemoveEvent { + + public BaseItemRemoveEvent(Container source, Object itemId, int index, + int count) { + super(source, itemId, index, count); + } + + @Override + public int getRemovedItemsCount() { + return getAffectedItemsCount(); + } + } + + /** + * Get an item even if filtered out. + * + * For internal use only. + * + * @param itemId + * @return + */ + protected abstract ITEMCLASS getUnfilteredItem(Object itemId); + + // cannot override getContainerPropertyIds() and getItemIds(): if subclass + // uses Object as ITEMIDCLASS or PROPERTYIDCLASS, Collection<Object> cannot + // be cast to Collection<MyInterface> + + // public abstract Collection<PROPERTYIDCLASS> getContainerPropertyIds(); + // public abstract Collection<ITEMIDCLASS> getItemIds(); + + // Container interface method implementations + + @Override + public int size() { + return getVisibleItemIds().size(); + } + + @Override + public boolean containsId(Object itemId) { + // only look at visible items after filtering + if (itemId == null) { + return false; + } else { + return getVisibleItemIds().contains(itemId); + } + } + + @Override + public List<?> getItemIds() { + return Collections.unmodifiableList(getVisibleItemIds()); + } + + // Container.Ordered + + @Override + public ITEMIDTYPE nextItemId(Object itemId) { + int index = indexOfId(itemId); + if (index >= 0 && index < size() - 1) { + return getIdByIndex(index + 1); + } else { + // out of bounds + return null; + } + } + + @Override + public ITEMIDTYPE prevItemId(Object itemId) { + int index = indexOfId(itemId); + if (index > 0) { + return getIdByIndex(index - 1); + } else { + // out of bounds + return null; + } + } + + @Override + public ITEMIDTYPE firstItemId() { + if (size() > 0) { + return getIdByIndex(0); + } else { + return null; + } + } + + @Override + public ITEMIDTYPE lastItemId() { + if (size() > 0) { + return getIdByIndex(size() - 1); + } else { + return null; + } + } + + @Override + public boolean isFirstId(Object itemId) { + if (itemId == null) { + return false; + } + return itemId.equals(firstItemId()); + } + + @Override + public boolean isLastId(Object itemId) { + if (itemId == null) { + return false; + } + return itemId.equals(lastItemId()); + } + + // Container.Indexed + + @Override + public ITEMIDTYPE getIdByIndex(int index) { + return getVisibleItemIds().get(index); + } + + @Override + public List<ITEMIDTYPE> getItemIds(int startIndex, int numberOfIds) { + if (startIndex < 0) { + throw new IndexOutOfBoundsException( + "Start index cannot be negative! startIndex=" + startIndex); + } + + if (startIndex > getVisibleItemIds().size()) { + throw new IndexOutOfBoundsException( + "Start index exceeds container size! startIndex=" + + startIndex + " containerLastItemIndex=" + + (getVisibleItemIds().size() - 1)); + } + + if (numberOfIds < 1) { + if (numberOfIds == 0) { + return Collections.emptyList(); + } + + throw new IllegalArgumentException( + "Cannot get negative amount of items! numberOfItems=" + + numberOfIds); + } + + int endIndex = startIndex + numberOfIds; + + if (endIndex > getVisibleItemIds().size()) { + endIndex = getVisibleItemIds().size(); + } + + return Collections.unmodifiableList( + getVisibleItemIds().subList(startIndex, endIndex)); + + } + + @Override + public int indexOfId(Object itemId) { + return getVisibleItemIds().indexOf(itemId); + } + + // methods that are unsupported by default, override to support + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public Object addItem() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing items not supported. Override the removeItem() method if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing items not supported. Override the removeAllItems() method if required as specified in AbstractInMemoryContainer javadoc."); + } + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Adding container properties not supported. Override the addContainerProperty() method if required."); + } + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Removing container properties not supported. Override the addContainerProperty() method if required."); + } + + // ItemSetChangeNotifier + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Deprecated + @Override + public void addListener(Container.ItemSetChangeListener listener) { + addItemSetChangeListener(listener); + } + + @Override + public void addItemSetChangeListener( + Container.ItemSetChangeListener listener) { + super.addItemSetChangeListener(listener); + } + + @Override + public void removeItemSetChangeListener( + Container.ItemSetChangeListener listener) { + super.removeItemSetChangeListener(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Deprecated + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + removeItemSetChangeListener(listener); + } + + // internal methods + + // Filtering support + + /** + * Filter the view to recreate the visible item list from the unfiltered + * items, and send a notification if the set of visible items changed in any + * way. + */ + protected void filterAll() { + if (doFilterContainer(!getFilters().isEmpty())) { + fireItemSetChange(); + } + } + + /** + * Filters the data in the container and updates internal data structures. + * This method should reset any internal data structures and then repopulate + * them so {@link #getItemIds()} and other methods only return the filtered + * items. + * + * @param hasFilters + * true if filters has been set for the container, false + * otherwise + * @return true if the item set has changed as a result of the filtering + */ + protected boolean doFilterContainer(boolean hasFilters) { + if (!hasFilters) { + boolean changed = getAllItemIds().size() != getVisibleItemIds() + .size(); + setFilteredItemIds(null); + return changed; + } + + // Reset filtered list + List<ITEMIDTYPE> originalFilteredItemIds = getFilteredItemIds(); + boolean wasUnfiltered = false; + if (originalFilteredItemIds == null) { + originalFilteredItemIds = Collections.emptyList(); + wasUnfiltered = true; + } + setFilteredItemIds(new ListSet<ITEMIDTYPE>()); + + // Filter + boolean equal = true; + Iterator<ITEMIDTYPE> origIt = originalFilteredItemIds.iterator(); + for (final Iterator<ITEMIDTYPE> i = getAllItemIds().iterator(); i + .hasNext();) { + final ITEMIDTYPE id = i.next(); + if (passesFilters(id)) { + // filtered list comes from the full list, can use == + equal = equal && origIt.hasNext() && origIt.next() == id; + getFilteredItemIds().add(id); + } + } + + return (wasUnfiltered && !getAllItemIds().isEmpty()) || !equal + || origIt.hasNext(); + } + + /** + * Checks if the given itemId passes the filters set for the container. The + * caller should make sure the itemId exists in the container. For + * non-existing itemIds the behavior is undefined. + * + * @param itemId + * An itemId that exists in the container. + * @return true if the itemId passes all filters or no filters are set, + * false otherwise. + */ + protected boolean passesFilters(Object itemId) { + ITEMCLASS item = getUnfilteredItem(itemId); + if (getFilters().isEmpty()) { + return true; + } + final Iterator<Filter> i = getFilters().iterator(); + while (i.hasNext()) { + final Filter f = i.next(); + if (!f.passesFilter(itemId, item)) { + return false; + } + } + return true; + } + + /** + * Adds a container filter and re-filter the view. + * + * The filter must implement Filter and its sub-filters (if any) must also + * be in-memory filterable. + * + * This can be used to implement + * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)} + * and optionally also + * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)} + * (with {@link SimpleStringFilter}). + * + * Note that in some cases, incompatible filters cannot be detected when + * added and an {@link UnsupportedFilterException} may occur when performing + * filtering. + * + * @throws UnsupportedFilterException + * if the filter is detected as not supported by the container + */ + protected void addFilter(Filter filter) throws UnsupportedFilterException { + getFilters().add(filter); + filterAll(); + } + + /** + * Returns true if any filters have been applied to the container. + * + * @return true if the container has filters applied, false otherwise + * @since 7.1 + */ + protected boolean hasContainerFilters() { + return !getContainerFilters().isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#getContainerFilters() + */ + protected Collection<Filter> getContainerFilters() { + return Collections.unmodifiableCollection(filters); + } + + /** + * Remove a specific container filter and re-filter the view (if necessary). + * + * This can be used to implement + * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)} + * . + */ + protected void removeFilter(Filter filter) { + for (Iterator<Filter> iterator = getFilters().iterator(); iterator + .hasNext();) { + Filter f = iterator.next(); + if (f.equals(filter)) { + iterator.remove(); + filterAll(); + return; + } + } + } + + /** + * Remove all container filters for all properties and re-filter the view. + * + * This can be used to implement + * {@link Filterable#removeAllContainerFilters()}. + */ + protected void removeAllFilters() { + if (getFilters().isEmpty()) { + return; + } + getFilters().clear(); + filterAll(); + } + + /** + * Checks if there is a filter that applies to a given property. + * + * @param propertyId + * @return true if there is an active filter for the property + */ + protected boolean isPropertyFiltered(Object propertyId) { + if (getFilters().isEmpty() || propertyId == null) { + return false; + } + final Iterator<Filter> i = getFilters().iterator(); + while (i.hasNext()) { + final Filter f = i.next(); + if (f.appliesToProperty(propertyId)) { + return true; + } + } + return false; + } + + /** + * Remove all container filters for a given property identifier and + * re-filter the view. This also removes filters applying to multiple + * properties including the one identified by propertyId. + * + * This can be used to implement + * {@link Filterable#removeContainerFilters(Object)}. + * + * @param propertyId + * @return Collection<Filter> removed filters + */ + protected Collection<Filter> removeFilters(Object propertyId) { + if (getFilters().isEmpty() || propertyId == null) { + return Collections.emptyList(); + } + List<Filter> removedFilters = new LinkedList<Filter>(); + for (Iterator<Filter> iterator = getFilters().iterator(); iterator + .hasNext();) { + Filter f = iterator.next(); + if (f.appliesToProperty(propertyId)) { + removedFilters.add(f); + iterator.remove(); + } + } + if (!removedFilters.isEmpty()) { + filterAll(); + return removedFilters; + } + return Collections.emptyList(); + } + + // sorting + + /** + * Returns the ItemSorter used for comparing items in a sort. See + * {@link #setItemSorter(ItemSorter)} for more information. + * + * @return The ItemSorter used for comparing two items in a sort. + */ + protected ItemSorter getItemSorter() { + return itemSorter; + } + + /** + * Sets the ItemSorter used for comparing items in a sort. The + * {@link ItemSorter#compare(Object, Object)} method is called with item ids + * to perform the sorting. A default ItemSorter is used if this is not + * explicitly set. + * + * @param itemSorter + * The ItemSorter used for comparing two items in a sort (not + * null). + */ + protected void setItemSorter(ItemSorter itemSorter) { + this.itemSorter = itemSorter; + } + + /** + * Sort base implementation to be used to implement {@link Sortable}. + * + * Subclasses should call this from a public + * {@link #sort(Object[], boolean[])} method when implementing Sortable. + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + protected void sortContainer(Object[] propertyId, boolean[] ascending) { + if (!(this instanceof Sortable)) { + throw new UnsupportedOperationException( + "Cannot sort a Container that does not implement Sortable"); + } + + // Set up the item sorter for the sort operation + getItemSorter().setSortProperties((Sortable) this, propertyId, + ascending); + + // Perform the actual sort + doSort(); + + // Post sort updates + if (isFiltered()) { + filterAll(); + } else { + fireItemSetChange(); + } + + } + + /** + * Perform the sorting of the data structures in the container. This is + * invoked when the <code>itemSorter</code> has been prepared for the sort + * operation. Typically this method calls + * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays + * (containing item ids) that need to be sorted. + * + */ + protected void doSort() { + Collections.sort(getAllItemIds(), getItemSorter()); + } + + /** + * Returns the sortable property identifiers for the container. Can be used + * to implement {@link Sortable#getSortableContainerPropertyIds()}. + */ + protected Collection<?> getSortablePropertyIds() { + LinkedList<Object> sortables = new LinkedList<Object>(); + for (Object propertyId : getContainerPropertyIds()) { + Class<?> propertyType = getType(propertyId); + if (Comparable.class.isAssignableFrom(propertyType) + || propertyType.isPrimitive()) { + sortables.add(propertyId); + } + } + return sortables; + } + + // removing items + + /** + * Removes all items from the internal data structures of this class. This + * can be used to implement {@link #removeAllItems()} in subclasses. + * + * No notification is sent, the caller has to fire a suitable item set + * change notification. + */ + protected void internalRemoveAllItems() { + // Removes all Items + getAllItemIds().clear(); + if (isFiltered()) { + getFilteredItemIds().clear(); + } + } + + /** + * Removes a single item from the internal data structures of this class. + * This can be used to implement {@link #removeItem(Object)} in subclasses. + * + * No notification is sent, the caller has to fire a suitable item set + * change notification. + * + * @param itemId + * the identifier of the item to remove + * @return true if an item was successfully removed, false if failed to + * remove or no such item + */ + protected boolean internalRemoveItem(Object itemId) { + if (itemId == null) { + return false; + } + + boolean result = getAllItemIds().remove(itemId); + if (result && isFiltered()) { + getFilteredItemIds().remove(itemId); + } + + return result; + } + + // adding items + + /** + * Adds the bean to all internal data structures at the given position. + * Fails if an item with itemId is already in the container. Returns a the + * item if it was added successfully, null otherwise. + * + * <p> + * Caller should initiate filtering after calling this method. + * </p> + * + * For internal use only - subclasses should use + * {@link #internalAddItemAtEnd(Object, Item, boolean)}, + * {@link #internalAddItemAt(int, Object, Item, boolean)} and + * {@link #internalAddItemAfter(Object, Object, Item, boolean)} instead. + * + * @param position + * The position at which the item should be inserted in the + * unfiltered collection of items + * @param itemId + * The item identifier for the item to insert + * @param item + * The item to insert + * + * @return ITEMCLASS if the item was added successfully, null otherwise + */ + private ITEMCLASS internalAddAt(int position, ITEMIDTYPE itemId, + ITEMCLASS item) { + if (position < 0 || position > getAllItemIds().size() || itemId == null + || item == null) { + return null; + } + // Make sure that the item has not been added previously + if (getAllItemIds().contains(itemId)) { + return null; + } + + // "filteredList" will be updated in filterAll() which should be invoked + // by the caller after calling this method. + getAllItemIds().add(position, itemId); + registerNewItem(position, itemId, item); + + return item; + } + + /** + * Add an item at the end of the container, and perform filtering if + * necessary. An event is fired if the filtered view changes. + * + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAtEnd(ITEMIDTYPE newItemId, + ITEMCLASS item, boolean filter) { + ITEMCLASS newItem = internalAddAt(getAllItemIds().size(), newItemId, + item); + if (newItem != null && filter) { + // TODO filter only this item, use fireItemAdded() + filterAll(); + if (!isFiltered()) { + // TODO hack: does not detect change in filterAll() in this case + fireItemAdded(indexOfId(newItemId), newItemId, item); + } + } + return newItem; + } + + /** + * Add an item after a given (visible) item, and perform filtering. An event + * is fired if the filtered view changes. + * + * The new item is added at the beginning if previousItemId is null. + * + * @param previousItemId + * item id of a visible item after which to add the new item, or + * null to add at the beginning + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAfter(ITEMIDTYPE previousItemId, + ITEMIDTYPE newItemId, ITEMCLASS item, boolean filter) { + // only add if the previous item is visible + ITEMCLASS newItem = null; + if (previousItemId == null) { + newItem = internalAddAt(0, newItemId, item); + } else if (containsId(previousItemId)) { + newItem = internalAddAt(getAllItemIds().indexOf(previousItemId) + 1, + newItemId, item); + } + if (newItem != null && filter) { + // TODO filter only this item, use fireItemAdded() + filterAll(); + if (!isFiltered()) { + // TODO hack: does not detect change in filterAll() in this case + fireItemAdded(indexOfId(newItemId), newItemId, item); + } + } + return newItem; + } + + /** + * Add an item at a given (visible after filtering) item index, and perform + * filtering. An event is fired if the filtered view changes. + * + * @param index + * position where to add the item (visible/view index) + * @param newItemId + * @param item + * new item to add + * @param filter + * true to perform filtering and send event after adding the + * item, false to skip these operations for batch inserts - if + * false, caller needs to make sure these operations are + * performed at the end of the batch + * @return item added or null if no item was added + */ + protected ITEMCLASS internalAddItemAt(int index, ITEMIDTYPE newItemId, + ITEMCLASS item, boolean filter) { + if (index < 0 || index > size()) { + return null; + } else if (index == 0) { + // add before any item, visible or not + return internalAddItemAfter(null, newItemId, item, filter); + } else { + // if index==size(), adds immediately after last visible item + return internalAddItemAfter(getIdByIndex(index - 1), newItemId, + item, filter); + } + } + + /** + * Registers a new item as having been added to the container. This can + * involve storing the item or any relevant information about it in internal + * container-specific collections if necessary, as well as registering + * listeners etc. + * + * The full identifier list in {@link AbstractInMemoryContainer} has already + * been updated to reflect the new item when this method is called. + * + * @param position + * @param itemId + * @param item + */ + protected void registerNewItem(int position, ITEMIDTYPE itemId, + ITEMCLASS item) { + } + + // item set change notifications + + /** + * Notify item set change listeners that an item has been added to the + * container. + * + * @since 7.4 + * + * @param position + * position of the added item in the view + * @param itemId + * id of the added item + * @param item + * the added item + */ + protected void fireItemAdded(int position, ITEMIDTYPE itemId, + ITEMCLASS item) { + fireItemsAdded(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been added to the + * container. + * + * @param firstPosition + * position of the first visible added item in the view + * @param firstItemId + * id of the first visible added item + * @param numberOfItems + * the number of visible added items + */ + protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId, + int numberOfItems) { + BaseItemAddEvent addEvent = new BaseItemAddEvent(this, firstItemId, + firstPosition, numberOfItems); + fireItemSetChange(addEvent); + } + + /** + * Notify item set change listeners that an item has been removed from the + * container. + * + * @since 7.4 + * + * @param position + * position of the removed item in the view prior to removal (if + * was visible) + * @param itemId + * id of the removed item, of type {@link Object} to satisfy + * {@link Container#removeItem(Object)} API + */ + protected void fireItemRemoved(int position, Object itemId) { + fireItemsRemoved(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been removed from the + * container. + * + * @param firstPosition + * position of the first visible removed item in the view prior + * to removal + * @param firstItemId + * id of the first visible removed item, of type {@link Object} + * to satisfy {@link Container#removeItem(Object)} API + * @param numberOfItems + * the number of removed visible items + * + */ + protected void fireItemsRemoved(int firstPosition, Object firstItemId, + int numberOfItems) { + BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(removeEvent); + } + + // visible and filtered item identifier lists + + /** + * Returns the internal list of visible item identifiers after filtering. + * + * For internal use only. + */ + protected List<ITEMIDTYPE> getVisibleItemIds() { + if (isFiltered()) { + return getFilteredItemIds(); + } else { + return getAllItemIds(); + } + } + + /** + * Returns the item id of the first visible item after filtering. 'Null' is + * returned if there is no visible items. + * <p> + * For internal use only. + * + * @since 7.4 + * + * @return item id of the first visible item + */ + protected ITEMIDTYPE getFirstVisibleItem() { + if (!getVisibleItemIds().isEmpty()) { + return getVisibleItemIds().get(0); + } + return null; + } + + /** + * Returns true is the container has active filters. + * + * @return true if the container is currently filtered + */ + protected boolean isFiltered() { + return filteredItemIds != null; + } + + /** + * Internal helper method to set the internal list of filtered item + * identifiers. Should not be used outside this class except for + * implementing clone(), may disappear from future versions. + * + * @param filteredItemIds + */ + @Deprecated + protected void setFilteredItemIds(List<ITEMIDTYPE> filteredItemIds) { + this.filteredItemIds = filteredItemIds; + } + + /** + * Internal helper method to get the internal list of filtered item + * identifiers. Should not be used outside this class except for + * implementing clone(), may disappear from future versions - use + * {@link #getVisibleItemIds()} in other contexts. + * + * @return List<ITEMIDTYPE> + */ + protected List<ITEMIDTYPE> getFilteredItemIds() { + return filteredItemIds; + } + + /** + * Internal helper method to set the internal list of all item identifiers. + * Should not be used outside this class except for implementing clone(), + * may disappear from future versions. + * + * @param allItemIds + */ + @Deprecated + protected void setAllItemIds(List<ITEMIDTYPE> allItemIds) { + this.allItemIds = allItemIds; + } + + /** + * Internal helper method to get the internal list of all item identifiers. + * Avoid using this method outside this class, may disappear in future + * versions. + * + * @return List<ITEMIDTYPE> + */ + protected List<ITEMIDTYPE> getAllItemIds() { + return allItemIds; + } + + /** + * Set the internal collection of filters without performing filtering. + * + * This method is mostly for internal use, use + * {@link #addFilter(com.vaadin.data.Container.Filter)} and + * <code>remove*Filter*</code> (which also re-filter the container) instead + * when possible. + * + * @param filters + */ + protected void setFilters(Set<Filter> filters) { + this.filters = filters; + } + + /** + * Returns the internal collection of filters. The returned collection + * should not be modified by callers outside this class. + * + * @return Set<Filter> + */ + protected Set<Filter> getFilters() { + return filters; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/BeanContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/BeanContainer.java new file mode 100644 index 0000000000..01a5256199 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/BeanContainer.java @@ -0,0 +1,179 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.util.Collection; + +/** + * An in-memory container for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class. Only beans of the same type can be added to the + * container. + * </p> + * + * <p> + * In BeanContainer (unlike {@link BeanItemContainer}), the item IDs do not have + * to be the beans themselves. The container can be used either with explicit + * item IDs or the item IDs can be generated when adding beans. + * </p> + * + * <p> + * To use explicit item IDs, use the methods {@link #addItem(Object, Object)}, + * {@link #addItemAfter(Object, Object, Object)} and + * {@link #addItemAt(int, Object, Object)}. + * </p> + * + * <p> + * If a bean id resolver is set using + * {@link #setBeanIdResolver(com.vaadin.data.util.AbstractBeanContainer.BeanIdResolver)} + * or {@link #setBeanIdProperty(Object)}, the methods {@link #addBean(Object)}, + * {@link #addBeanAfter(Object, Object)}, {@link #addBeanAt(int, Object)} and + * {@link #addAll(java.util.Collection)} can be used to add items to the + * container. If one of these methods is called, the resolver is used to + * generate an identifier for the item (must not return null). + * </p> + * + * <p> + * Note that explicit item identifiers can also be used when a resolver has been + * set by calling the addItem*() methods - the resolver is only used when adding + * beans using the addBean*() or {@link #addAll(Collection)} methods. + * </p> + * + * <p> + * It is not possible to add additional properties to the container. + * </p> + * + * @param <IDTYPE> + * The type of the item identifier + * @param <BEANTYPE> + * The type of the Bean + * + * @see AbstractBeanContainer + * @see BeanItemContainer + * + * @since 6.5 + */ +public class BeanContainer<IDTYPE, BEANTYPE> + extends AbstractBeanContainer<IDTYPE, BEANTYPE> { + + public BeanContainer(Class<? super BEANTYPE> type) { + super(type); + } + + /** + * Adds the bean to the Container. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + public BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) { + if (itemId != null && bean != null) { + return super.addItem(itemId, bean); + } else { + return null; + } + } + + /** + * Adds the bean after the given item id. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + @Override + public BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId, + IDTYPE newItemId, BEANTYPE bean) { + if (newItemId != null && bean != null) { + return super.addItemAfter(previousItemId, newItemId, bean); + } else { + return null; + } + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The item id for the bean to add to the container. + * @param bean + * The bean to add to the container. + * + * @return Returns the new BeanItem or null if the operation fails. + */ + @Override + public BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId, + BEANTYPE bean) { + if (newItemId != null && bean != null) { + return super.addItemAt(index, newItemId, bean); + } else { + return null; + } + } + + // automatic item id resolution + + /** + * Sets the bean id resolver to use a property of the beans as the + * identifier. + * + * @param propertyId + * the identifier of the property to use to find item identifiers + */ + public void setBeanIdProperty(Object propertyId) { + setBeanIdResolver(createBeanPropertyResolver(propertyId)); + } + + @Override + // overridden to make public + public void setBeanIdResolver( + BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) { + super.setBeanIdResolver(beanIdResolver); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBean(BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBean(bean); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAfter(previousItemId, bean); + } + + @Override + // overridden to make public + public BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAt(index, bean); + } + + @Override + // overridden to make public + public void addAll(Collection<? extends BEANTYPE> collection) + throws IllegalStateException { + super.addAll(collection); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java new file mode 100644 index 0000000000..c0c4fdcaa6 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java @@ -0,0 +1,264 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A wrapper class for adding the Item interface to any Java Bean. + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class BeanItem<BT> extends PropertysetItem { + + /** + * The bean which this Item is based on. + */ + private final BT bean; + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all properties + * of a Java Bean to it. The properties are identified by their respective + * bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * + */ + public BeanItem(BT bean) { + this(bean, (Class<BT>) bean.getClass()); + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all properties + * of a Java Bean to it. The properties are identified by their respective + * bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @since 7.4 + * + * @param bean + * the Java Bean to copy properties from. + * @param beanClass + * class of the {@code bean} + * + */ + public BeanItem(BT bean, Class<BT> beanClass) { + this(bean, getPropertyDescriptors(beanClass)); + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> using a pre-computed set + * of properties. The properties are identified by their respective bean + * names. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyDescriptors + * pre-computed property descriptors + */ + BeanItem(BT bean, + Map<String, VaadinPropertyDescriptor<BT>> propertyDescriptors) { + + this.bean = bean; + + for (VaadinPropertyDescriptor<BT> pd : propertyDescriptors.values()) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyIds + * id of the property. + */ + public BeanItem(BT bean, Collection<?> propertyIds) { + + this.bean = bean; + + // Create bean information + LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pds = getPropertyDescriptors( + (Class<BT>) bean.getClass()); + + // Add all the bean properties as MethodProperties to this Item + for (Object id : propertyIds) { + VaadinPropertyDescriptor<BT> pd = pds.get(id); + if (pd != null) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + } + + /** + * <p> + * Creates a new instance of <code>BeanItem</code> and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyIds + * ids of the properties. + */ + public BeanItem(BT bean, String... propertyIds) { + this(bean, Arrays.asList(propertyIds)); + } + + /** + * <p> + * Perform introspection on a Java Bean class to find its properties. + * </p> + * + * <p> + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone <code>is</code> and + * <code>are</code> methods are not supported. + * </p> + * + * @param beanClass + * the Java Bean class to get properties for. + * @return an ordered map from property names to property descriptors + */ + static <BT> LinkedHashMap<String, VaadinPropertyDescriptor<BT>> getPropertyDescriptors( + final Class<BT> beanClass) { + final LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pdMap = new LinkedHashMap<String, VaadinPropertyDescriptor<BT>>(); + + // Try to introspect, if it fails, we just have an empty Item + try { + List<PropertyDescriptor> propertyDescriptors = BeanUtil + .getBeanPropertyDescriptor(beanClass); + + // Add all the bean properties as MethodProperties to this Item + // later entries on the list overwrite earlier ones + for (PropertyDescriptor pd : propertyDescriptors) { + final Method getMethod = pd.getReadMethod(); + if ((getMethod != null) + && getMethod.getDeclaringClass() != Object.class) { + VaadinPropertyDescriptor<BT> vaadinPropertyDescriptor = new MethodPropertyDescriptor<BT>( + pd.getName(), pd.getPropertyType(), + pd.getReadMethod(), pd.getWriteMethod()); + pdMap.put(pd.getName(), vaadinPropertyDescriptor); + } + } + } catch (final java.beans.IntrospectionException ignored) { + } + + return pdMap; + } + + /** + * Expands nested bean properties by replacing a top-level property with + * some or all of its sub-properties. The expansion is not recursive. + * + * @param propertyId + * property id for the property whose sub-properties are to be + * expanded, + * @param subPropertyIds + * sub-properties to expand, all sub-properties are expanded if + * not specified + */ + public void expandProperty(String propertyId, String... subPropertyIds) { + Set<String> subPropertySet = new HashSet<String>( + Arrays.asList(subPropertyIds)); + + if (0 == subPropertyIds.length) { + // Enumerate all sub-properties + Class<?> propertyType = getItemProperty(propertyId).getType(); + Map<String, ?> pds = getPropertyDescriptors(propertyType); + subPropertySet.addAll(pds.keySet()); + } + + for (String subproperty : subPropertySet) { + String qualifiedPropertyId = propertyId + "." + subproperty; + addNestedProperty(qualifiedPropertyId); + } + + removeItemProperty(propertyId); + } + + /** + * Adds a nested property to the item. The property must not exist in the + * item already and must of form "field1.field2" where field2 is a field in + * the object referenced to by field1. If an intermediate property returns + * null, the property will return a null value + * + * @param nestedPropertyId + * property id to add. + */ + public void addNestedProperty(String nestedPropertyId) { + addItemProperty(nestedPropertyId, + new NestedMethodProperty<Object>(getBean(), nestedPropertyId)); + } + + /** + * Gets the underlying JavaBean object. + * + * @return the bean object. + */ + public BT getBean() { + return bean; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItemContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItemContainer.java new file mode 100644 index 0000000000..66e553164d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItemContainer.java @@ -0,0 +1,253 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.util.Collection; + +/** + * An in-memory container for JavaBeans. + * + * <p> + * The properties of the container are determined automatically by introspecting + * the used JavaBean class. Only beans of the same type can be added to the + * container. + * </p> + * + * <p> + * BeanItemContainer uses the beans themselves as identifiers. The + * {@link Object#hashCode()} of a bean is used when storing and looking up beans + * so it must not change during the lifetime of the bean (it should not depend + * on any part of the bean that can be modified). Typically this restricts the + * implementation of {@link Object#equals(Object)} as well in order for it to + * fulfill the contract between {@code equals()} and {@code hashCode()}. + * </p> + * + * <p> + * To add items to the container, use the methods {@link #addBean(Object)}, + * {@link #addBeanAfter(Object, Object)} and {@link #addBeanAt(int, Object)}. + * Also {@link #addItem(Object)}, {@link #addItemAfter(Object, Object)} and + * {@link #addItemAt(int, Object)} can be used as synonyms for them. + * </p> + * + * <p> + * It is not possible to add additional properties to the container. + * </p> + * + * @param <BEANTYPE> + * The type of the Bean + * + * @since 5.4 + */ +@SuppressWarnings("serial") +public class BeanItemContainer<BEANTYPE> + extends AbstractBeanContainer<BEANTYPE, BEANTYPE> { + + /** + * Bean identity resolver that returns the bean itself as its item + * identifier. + * + * This corresponds to the old behavior of {@link BeanItemContainer}, and + * requires suitable (identity-based) equals() and hashCode() methods on the + * beans. + * + * @param <BT> + * + * @since 6.5 + */ + private static class IdentityBeanIdResolver<BT> + implements BeanIdResolver<BT, BT> { + + @Override + public BT getIdForBean(BT bean) { + return bean; + } + + } + + /** + * Constructs a {@code BeanItemContainer} for beans of the given type. + * + * @param type + * the type of the beans that will be added to the container. + * @throws IllegalArgumentException + * If {@code type} is null + */ + public BeanItemContainer(Class<? super BEANTYPE> type) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>()); + } + + /** + * Constructs a {@code BeanItemContainer} and adds the given beans to it. + * The collection must not be empty. + * {@link BeanItemContainer#BeanItemContainer(Class)} can be used for + * creating an initially empty {@code BeanItemContainer}. + * + * Note that when using this constructor, the actual class of the first item + * in the collection is used to determine the bean properties supported by + * the container instance, and only beans of that class or its subclasses + * can be added to the collection. If this is problematic or empty + * collections need to be supported, use {@link #BeanItemContainer(Class)} + * and {@link #addAll(Collection)} instead. + * + * @param collection + * a non empty {@link Collection} of beans. + * @throws IllegalArgumentException + * If the collection is null or empty. + * + * @deprecated As of 6.5, use {@link #BeanItemContainer(Class, Collection)} + * instead + */ + @SuppressWarnings("unchecked") + @Deprecated + public BeanItemContainer(Collection<? extends BEANTYPE> collection) + throws IllegalArgumentException { + // must assume the class is BT + // the class information is erased by the compiler + this((Class<BEANTYPE>) getBeanClassForCollection(collection), + collection); + } + + /** + * Internal helper method to support the deprecated {@link Collection} + * container. + * + * @param <BT> + * @param collection + * @return + * @throws IllegalArgumentException + */ + @SuppressWarnings("unchecked") + @Deprecated + private static <BT> Class<? extends BT> getBeanClassForCollection( + Collection<? extends BT> collection) + throws IllegalArgumentException { + if (collection == null || collection.isEmpty()) { + throw new IllegalArgumentException( + "The collection passed to BeanItemContainer constructor must not be null or empty. Use the other BeanItemContainer constructor."); + } + return (Class<? extends BT>) collection.iterator().next().getClass(); + } + + /** + * Constructs a {@code BeanItemContainer} and adds the given beans to it. + * + * @param type + * the type of the beans that will be added to the container. + * @param collection + * a {@link Collection} of beans (can be empty or null). + * @throws IllegalArgumentException + * If {@code type} is null + */ + public BeanItemContainer(Class<? super BEANTYPE> type, + Collection<? extends BEANTYPE> collection) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>()); + + if (collection != null) { + addAll(collection); + } + } + + /** + * Adds all the beans from a {@link Collection} in one go. More efficient + * than adding them one by one. + * + * @param collection + * The collection of beans to add. Must not be null. + */ + @Override + public void addAll(Collection<? extends BEANTYPE> collection) { + super.addAll(collection); + } + + /** + * Adds the bean after the given bean. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param previousItemId + * the bean (of type BT) after which to add newItemId + * @param newItemId + * the bean (of type BT) to add (not null) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object) + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItemAfter(Object previousItemId, + Object newItemId) throws IllegalArgumentException { + return super.addBeanAfter((BEANTYPE) previousItemId, + (BEANTYPE) newItemId); + } + + /** + * Adds a new bean at the given index. + * + * The bean is used both as the item contents and as the item identifier. + * + * @param index + * Index at which the bean should be added. + * @param newItemId + * The bean to add to the container. + * @return Returns the new BeanItem or null if the operation fails. + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItemAt(int index, Object newItemId) + throws IllegalArgumentException { + return super.addBeanAt(index, (BEANTYPE) newItemId); + } + + /** + * Adds the bean to the Container. + * + * The bean is used both as the item contents and as the item identifier. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + @SuppressWarnings("unchecked") + public BeanItem<BEANTYPE> addItem(Object itemId) { + return super.addBean((BEANTYPE) itemId); + } + + /** + * Adds the bean to the Container. + * + * The bean is used both as the item contents and as the item identifier. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + public BeanItem<BEANTYPE> addBean(BEANTYPE bean) { + return addItem(bean); + } + + /** + * Unsupported in BeanItemContainer. + */ + @Override + protected void setBeanIdResolver( + AbstractBeanContainer.BeanIdResolver<BEANTYPE, BEANTYPE> beanIdResolver) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "BeanItemContainer always uses an IdentityBeanIdResolver"); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/ContainerHierarchicalWrapper.java b/compatibility-server/src/main/java/com/vaadin/data/util/ContainerHierarchicalWrapper.java new file mode 100644 index 0000000000..9e108cd615 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/ContainerHierarchicalWrapper.java @@ -0,0 +1,865 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * <p> + * A wrapper class for adding external hierarchy to containers not implementing + * the {@link com.vaadin.data.Container.Hierarchical} interface. + * </p> + * + * <p> + * If the wrapped container is changed directly (that is, not through the + * wrapper), and does not implement Container.ItemSetChangeNotifier and/or + * Container.PropertySetChangeNotifier the hierarchy information must be updated + * with the {@link #updateHierarchicalWrapper()} method. + * </p> + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class ContainerHierarchicalWrapper implements Container.Hierarchical, + Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier { + + /** The wrapped container */ + private final Container container; + + /** Set of IDs of those contained Items that can't have children. */ + private HashSet<Object> noChildrenAllowed = null; + + /** Mapping from Item ID to parent Item ID */ + private Hashtable<Object, Object> parent = null; + + /** Mapping from Item ID to a list of child IDs */ + private Hashtable<Object, LinkedList<Object>> children = null; + + /** List that contains all root elements of the container. */ + private LinkedHashSet<Object> roots = null; + + /** Is the wrapped container hierarchical by itself ? */ + private boolean hierarchical; + + /** + * A comparator that sorts the listed items before other items. Otherwise, + * the order is undefined. + */ + private static class ListedItemsFirstComparator + implements Comparator<Object>, Serializable { + private final Collection<?> itemIds; + + private ListedItemsFirstComparator(Collection<?> itemIds) { + this.itemIds = itemIds; + } + + @Override + public int compare(Object o1, Object o2) { + if (o1.equals(o2)) { + return 0; + } + for (Object id : itemIds) { + if (id == o1) { + return -1; + } else if (id == o2) { + return 1; + } + } + return 0; + } + } + + /** + * Constructs a new hierarchical wrapper for an existing Container. Works + * even if the to-be-wrapped container already implements the + * <code>Container.Hierarchical</code> interface. + * + * @param toBeWrapped + * the container that needs to be accessed hierarchically + * @see #updateHierarchicalWrapper() + */ + public ContainerHierarchicalWrapper(Container toBeWrapped) { + + container = toBeWrapped; + hierarchical = container instanceof Container.Hierarchical; + + // Check arguments + if (container == null) { + throw new NullPointerException("Null can not be wrapped"); + } + + // Create initial order if needed + if (!hierarchical) { + noChildrenAllowed = new HashSet<Object>(); + parent = new Hashtable<Object, Object>(); + children = new Hashtable<Object, LinkedList<Object>>(); + roots = new LinkedHashSet<Object>(container.getItemIds()); + } + + updateHierarchicalWrapper(); + + } + + /** + * Updates the wrapper's internal hierarchy data to include all Items in the + * underlying container. If the contents of the wrapped container change + * without the wrapper's knowledge, this method needs to be called to update + * the hierarchy information of the Items. + */ + public void updateHierarchicalWrapper() { + + if (!hierarchical) { + + // Recreate hierarchy and data structures if missing + if (noChildrenAllowed == null || parent == null || children == null + || roots == null) { + noChildrenAllowed = new HashSet<Object>(); + parent = new Hashtable<Object, Object>(); + children = new Hashtable<Object, LinkedList<Object>>(); + roots = new LinkedHashSet<Object>(container.getItemIds()); + } + + // Check that the hierarchy is up-to-date + else { + + // ensure order of root and child lists is same as in wrapped + // container + Collection<?> itemIds = container.getItemIds(); + Comparator<Object> basedOnOrderFromWrappedContainer = new ListedItemsFirstComparator( + itemIds); + + // Calculate the set of all items in the hierarchy + final HashSet<Object> s = new HashSet<Object>(); + s.addAll(parent.keySet()); + s.addAll(children.keySet()); + s.addAll(roots); + + // Remove unnecessary items + for (final Iterator<Object> i = s.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!container.containsId(id)) { + removeFromHierarchyWrapper(id); + } + } + + // Add all the missing items + final Collection<?> ids = container.getItemIds(); + for (final Iterator<?> i = ids.iterator(); i.hasNext();) { + final Object id = i.next(); + if (!s.contains(id)) { + addToHierarchyWrapper(id); + s.add(id); + } + } + + Object[] array = roots.toArray(); + Arrays.sort(array, basedOnOrderFromWrappedContainer); + roots = new LinkedHashSet<Object>(); + for (int i = 0; i < array.length; i++) { + roots.add(array[i]); + } + for (Object object : children.keySet()) { + LinkedList<Object> object2 = children.get(object); + Collections.sort(object2, basedOnOrderFromWrappedContainer); + } + + } + } + } + + /** + * Removes the specified Item from the wrapper's internal hierarchy + * structure. + * <p> + * Note : The Item is not removed from the underlying Container. + * </p> + * + * @param itemId + * the ID of the item to remove from the hierarchy. + */ + private void removeFromHierarchyWrapper(Object itemId) { + + LinkedList<Object> oprhanedChildren = children.remove(itemId); + if (oprhanedChildren != null) { + for (Object object : oprhanedChildren) { + // make orphaned children root nodes + setParent(object, null); + } + } + + roots.remove(itemId); + final Object p = parent.get(itemId); + if (p != null) { + final LinkedList<Object> c = children.get(p); + if (c != null) { + c.remove(itemId); + } + } + parent.remove(itemId); + noChildrenAllowed.remove(itemId); + } + + /** + * Adds the specified Item specified to the internal hierarchy structure. + * The new item is added as a root Item. The underlying container is not + * modified. + * + * @param itemId + * the ID of the item to add to the hierarchy. + */ + private void addToHierarchyWrapper(Object itemId) { + roots.add(itemId); + + } + + /* + * Can the specified Item have any children? Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container) + .areChildrenAllowed(itemId); + } + + if (noChildrenAllowed.contains(itemId)) { + return false; + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the children of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getChildren(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).getChildren(itemId); + } + + final Collection<?> c = children.get(itemId); + if (c == null) { + return null; + } + return Collections.unmodifiableCollection(c); + } + + /* + * Gets the ID of the parent of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getParent(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).getParent(itemId); + } + + return parent.get(itemId); + } + + /* + * Is the Item corresponding to the given ID a leaf node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean hasChildren(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).hasChildren(itemId); + } + + LinkedList<Object> list = children.get(itemId); + return (list != null && !list.isEmpty()); + } + + /* + * Is the Item corresponding to the given ID a root node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).isRoot(itemId); + } + + if (parent.containsKey(itemId)) { + return false; + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the root elements in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> rootItemIds() { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).rootItemIds(); + } + + return Collections.unmodifiableCollection(roots); + } + + /** + * <p> + * Sets the given Item's capability to have children. If the Item identified + * with the itemId already has children and the areChildrenAllowed is false + * this method fails and <code>false</code> is returned; the children must + * be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)} or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * @param itemId + * the ID of the Item in the container whose child capability is + * to be set. + * @param childrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container) + .setChildrenAllowed(itemId, childrenAllowed); + } + + // Check that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Update status + if (childrenAllowed) { + noChildrenAllowed.remove(itemId); + } else { + noChildrenAllowed.add(itemId); + } + + return true; + } + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (<code>canHaveChildren(newParentId) == true</code>). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent <code>null</code>. + * </p> + * + * @param itemId + * the ID of the item to be set as the child of the Item + * identified with newParentId. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + + // If the wrapped container implements the method directly, use it + if (hierarchical) { + return ((Container.Hierarchical) container).setParent(itemId, + newParentId); + } + + // Check that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Get the old parent + final Object oldParentId = parent.get(itemId); + + // Check if no change is necessary + if ((newParentId == null && oldParentId == null) + || (newParentId != null && newParentId.equals(oldParentId))) { + return true; + } + + // Making root + if (newParentId == null) { + + // Remove from old parents children list + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(itemId); + } + } + + // Add to be a root + roots.add(itemId); + + // Update parent + parent.remove(itemId); + + fireItemSetChangeIfAbstractContainer(); + + return true; + } + + // Check that the new parent exists in container and can have + // children + if (!containsId(newParentId) + || noChildrenAllowed.contains(newParentId)) { + return false; + } + + // Check that setting parent doesn't result to a loop + Object o = newParentId; + while (o != null && !o.equals(itemId)) { + o = parent.get(o); + } + if (o != null) { + return false; + } + + // Update parent + parent.put(itemId, newParentId); + LinkedList<Object> pcl = children.get(newParentId); + if (pcl == null) { + pcl = new LinkedList<Object>(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Remove from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + } + } + + fireItemSetChangeIfAbstractContainer(); + + return true; + } + + /** + * inform container (if it is instance of AbstractContainer) about the + * change in hierarchy (#15421) + */ + private void fireItemSetChangeIfAbstractContainer() { + if (container instanceof AbstractContainer) { + ((AbstractContainer) container).fireItemSetChange(); + } + } + + /** + * Creates a new Item into the Container, assigns it an automatic ID, and + * adds it to the hierarchy. + * + * @return the autogenerated ID of the new Item or <code>null</code> if the + * operation failed + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Object addItem() throws UnsupportedOperationException { + + final Object id = container.addItem(); + if (!hierarchical && id != null) { + addToHierarchyWrapper(id); + } + return id; + } + + /** + * Adds a new Item by its ID to the underlying container and to the + * hierarchy. + * + * @param itemId + * the ID of the Item to be created. + * @return the added Item or <code>null</code> if the operation failed. + * @throws UnsupportedOperationException + * if the addItem is not supported. + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + // Null ids are not accepted + if (itemId == null) { + return null; + } + + final Item item = container.addItem(itemId); + if (!hierarchical && item != null) { + addToHierarchyWrapper(itemId); + } + return item; + } + + /** + * Removes all items from the underlying container and from the hierarcy. + * + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeAllItems is not supported. + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + + final boolean success = container.removeAllItems(); + + if (!hierarchical && success) { + roots.clear(); + parent.clear(); + children.clear(); + noChildrenAllowed.clear(); + } + return success; + } + + /** + * Removes an Item specified by the itemId from the underlying container and + * from the hierarchy. + * + * @param itemId + * the ID of the Item to be removed. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeItem is not supported. + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + final boolean success = container.removeItem(itemId); + + if (!hierarchical && success) { + removeFromHierarchyWrapper(itemId); + } + + return success; + } + + /** + * Removes the Item identified by given itemId and all its children. + * + * @see #removeItem(Object) + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public boolean removeItemRecursively(Object itemId) { + return HierarchicalContainer.removeItemRecursively(this, itemId); + } + + /** + * Adds a new Property to all Items in the Container. + * + * @param propertyId + * the ID of the new Property. + * @param type + * the Data type of the new Property. + * @param defaultValue + * the value all created Properties are initialized to. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the addContainerProperty is not supported. + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + return container.addContainerProperty(propertyId, type, defaultValue); + } + + /** + * Removes the specified Property from the underlying container and from the + * hierarchy. + * <p> + * Note : The Property will be removed from all Items in the Container. + * </p> + * + * @param propertyId + * the ID of the Property to remove. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + * @throws UnsupportedOperationException + * if the removeContainerProperty is not supported. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + return container.removeContainerProperty(propertyId); + } + + /* + * Does the container contain the specified Item? Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean containsId(Object itemId) { + return container.containsId(itemId); + } + + /* + * Gets the specified Item from the container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Item getItem(Object itemId) { + return container.getItem(itemId); + } + + /* + * Gets the ID's of all Items stored in the Container Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getItemIds() { + return container.getItemIds(); + } + + /* + * Gets the Property identified by the given itemId and propertyId from the + * Container Don't add a JavaDoc comment here, we use the default + * documentation from implemented interface. + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + return container.getContainerProperty(itemId, propertyId); + } + + /* + * Gets the ID's of all Properties stored in the Container Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getContainerPropertyIds() { + return container.getContainerPropertyIds(); + } + + /* + * Gets the data type of all Properties identified by the given Property ID. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Class<?> getType(Object propertyId) { + return container.getType(propertyId); + } + + /* + * Gets the number of Items in the Container. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public int size() { + int size = container.size(); + assert size >= 0; + return size; + } + + /* + * Registers a new Item set change listener for this Container. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void addItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .addItemSetChangeListener(new PiggybackListener(listener)); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Container.ItemSetChangeListener listener) { + addItemSetChangeListener(listener); + } + + /* + * Removes a Item set change listener from the object. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (container instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) container) + .removeItemSetChangeListener( + new PiggybackListener(listener)); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Container.ItemSetChangeListener listener) { + removeItemSetChangeListener(listener); + } + + /* + * Registers a new Property set change listener for this Container. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void addPropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .addPropertySetChangeListener( + new PiggybackListener(listener)); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addPropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Container.PropertySetChangeListener listener) { + addPropertySetChangeListener(listener); + } + + /* + * Removes a Property set change listener from the object. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removePropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (container instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) container) + .removePropertySetChangeListener( + new PiggybackListener(listener)); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Container.PropertySetChangeListener listener) { + removePropertySetChangeListener(listener); + } + + /** + * This listener 'piggybacks' on the real listener in order to update the + * wrapper when needed. It proxies equals() and hashCode() to the real + * listener so that the correct listener gets removed. + * + */ + private class PiggybackListener + implements Container.PropertySetChangeListener, + Container.ItemSetChangeListener { + + Object listener; + + public PiggybackListener(Object realListener) { + listener = realListener; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + updateHierarchicalWrapper(); + ((Container.ItemSetChangeListener) listener) + .containerItemSetChange(event); + + } + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + updateHierarchicalWrapper(); + ((Container.PropertySetChangeListener) listener) + .containerPropertySetChange(event); + + } + + @Override + public boolean equals(Object obj) { + return obj == listener || (obj != null && obj.equals(listener)); + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/FilesystemContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/FilesystemContainer.java new file mode 100644 index 0000000000..46b4f4bcd4 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/FilesystemContainer.java @@ -0,0 +1,924 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.server.Resource; +import com.vaadin.util.FileTypeResolver; + +/** + * A hierarchical container wrapper for a filesystem. + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class FilesystemContainer implements Container.Hierarchical { + + /** + * String identifier of a file's "name" property. + */ + public static String PROPERTY_NAME = "Name"; + + /** + * String identifier of a file's "size" property. + */ + public static String PROPERTY_SIZE = "Size"; + + /** + * String identifier of a file's "icon" property. + */ + public static String PROPERTY_ICON = "Icon"; + + /** + * String identifier of a file's "last modified" property. + */ + public static String PROPERTY_LASTMODIFIED = "Last Modified"; + + /** + * List of the string identifiers for the available properties. + */ + public static Collection<String> FILE_PROPERTIES; + + private final static Method FILEITEM_LASTMODIFIED; + + private final static Method FILEITEM_NAME; + + private final static Method FILEITEM_ICON; + + private final static Method FILEITEM_SIZE; + + static { + + FILE_PROPERTIES = new ArrayList<String>(); + FILE_PROPERTIES.add(PROPERTY_NAME); + FILE_PROPERTIES.add(PROPERTY_ICON); + FILE_PROPERTIES.add(PROPERTY_SIZE); + FILE_PROPERTIES.add(PROPERTY_LASTMODIFIED); + FILE_PROPERTIES = Collections.unmodifiableCollection(FILE_PROPERTIES); + try { + FILEITEM_LASTMODIFIED = FileItem.class.getMethod("lastModified", + new Class[] {}); + FILEITEM_NAME = FileItem.class.getMethod("getName", new Class[] {}); + FILEITEM_ICON = FileItem.class.getMethod("getIcon", new Class[] {}); + FILEITEM_SIZE = FileItem.class.getMethod("getSize", new Class[] {}); + } catch (final NoSuchMethodException e) { + throw new RuntimeException( + "Internal error finding methods in FilesystemContainer"); + } + } + + private File[] roots = new File[] {}; + + private FilenameFilter filter = null; + + private boolean recursive = true; + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. The files are included recursively. + * + * @param root + * the root file for the new file-system container. Null values + * are ignored. + */ + public FilesystemContainer(File root) { + if (root != null) { + roots = new File[] { root }; + } + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. The files are included recursively. + * + * @param root + * the root file for the new file-system container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, boolean recursive) { + this(root); + setRecursive(recursive); + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified file + * as the root of the filesystem. + * + * @param root + * the root file for the new file-system container. + * @param extension + * the Filename extension (w/o separator) to limit the files in + * container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, String extension, boolean recursive) { + this(root); + this.setFilter(extension); + setRecursive(recursive); + } + + /** + * Constructs a new <code>FileSystemContainer</code> with the specified root + * and recursivity status. + * + * @param root + * the root file for the new file-system container. + * @param filter + * the Filename filter to limit the files in container. + * @param recursive + * should the container recursively contain subdirectories. + */ + public FilesystemContainer(File root, FilenameFilter filter, + boolean recursive) { + this(root); + this.setFilter(filter); + setRecursive(recursive); + } + + /** + * Adds new root file directory. Adds a file to be included as root file + * directory in the <code>FilesystemContainer</code>. + * + * @param root + * the File to be added as root directory. Null values are + * ignored. + */ + public void addRoot(File root) { + if (root != null) { + final File[] newRoots = new File[roots.length + 1]; + for (int i = 0; i < roots.length; i++) { + newRoots[i] = roots[i]; + } + newRoots[roots.length] = root; + roots = newRoots; + } + } + + /** + * Tests if the specified Item in the container may have children. Since a + * <code>FileSystemContainer</code> contains files and directories, this + * method returns <code>true</code> for directory Items only. + * + * @param itemId + * the id of the item. + * @return <code>true</code> if the specified Item is a directory, + * <code>false</code> otherwise. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + return itemId instanceof File && ((File) itemId).canRead() + && ((File) itemId).isDirectory(); + } + + /* + * Gets the ID's of all Items who are children of the specified Item. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Collection<File> getChildren(Object itemId) { + + if (!(itemId instanceof File)) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + File[] f; + if (filter != null) { + f = ((File) itemId).listFiles(filter); + } else { + f = ((File) itemId).listFiles(); + } + + if (f == null) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + + return Collections.unmodifiableCollection(l); + } + + /* + * Gets the parent item of the specified Item. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Object getParent(Object itemId) { + + if (!(itemId instanceof File)) { + return null; + } + return ((File) itemId).getParentFile(); + } + + /* + * Tests if the specified Item has any children. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean hasChildren(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + String[] l; + if (filter != null) { + l = ((File) itemId).list(filter); + } else { + l = ((File) itemId).list(); + } + return (l != null) && (l.length > 0); + } + + /* + * Tests if the specified Item is the root of the filesystem. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + for (int i = 0; i < roots.length; i++) { + if (roots[i].equals(itemId)) { + return true; + } + } + return false; + } + + /* + * Gets the ID's of all root Items in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<File> rootItemIds() { + + File[] f; + + // in single root case we use children + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return Collections.unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + + return Collections.unmodifiableCollection(l); + } + + /** + * Returns <code>false</code> when conversion from files to directories is + * not supported. + * + * @param itemId + * the ID of the item. + * @param areChildrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operaton is successful otherwise + * <code>false</code>. + * @throws UnsupportedOperationException + * if the setChildrenAllowed is not supported. + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException( + "Conversion file to/from directory is not supported"); + } + + /** + * Returns <code>false</code> when moving files around in the filesystem is + * not supported. + * + * @param itemId + * the ID of the item. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation is successful otherwise + * <code>false</code>. + * @throws UnsupportedOperationException + * if the setParent is not supported. + */ + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException("File moving is not supported"); + } + + /* + * Tests if the filesystem contains the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean containsId(Object itemId) { + + if (!(itemId instanceof File)) { + return false; + } + boolean val = false; + + // Try to match all roots + for (int i = 0; i < roots.length; i++) { + try { + val |= ((File) itemId).getCanonicalPath() + .startsWith(roots[i].getCanonicalPath()); + } catch (final IOException e) { + // Exception ignored + } + + } + if (val && filter != null) { + val &= filter.accept(((File) itemId).getParentFile(), + ((File) itemId).getName()); + } + return val; + } + + /* + * Gets the specified Item from the filesystem. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Item getItem(Object itemId) { + + if (!(itemId instanceof File)) { + return null; + } + return new FileItem((File) itemId); + } + + /** + * Internal recursive method to add the files under the specified directory + * to the collection. + * + * @param col + * the collection where the found items are added + * @param f + * the root file where to start adding files + */ + private void addItemIds(Collection<File> col, File f) { + File[] l; + if (filter != null) { + l = f.listFiles(filter); + } else { + l = f.listFiles(); + } + if (l == null) { + // File.listFiles returns null if File does not exist or if there + // was an IO error (permission denied) + return; + } + final List<File> ll = Arrays.asList(l); + Collections.sort(ll); + + for (final Iterator<File> i = ll.iterator(); i.hasNext();) { + final File lf = i.next(); + col.add(lf); + if (lf.isDirectory()) { + addItemIds(col, lf); + } + } + } + + /* + * Gets the IDs of Items in the filesystem. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Collection<File> getItemIds() { + + if (recursive) { + final Collection<File> col = new ArrayList<File>(); + for (int i = 0; i < roots.length; i++) { + addItemIds(col, roots[i]); + } + return Collections.unmodifiableCollection(col); + } else { + File[] f; + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return Collections + .unmodifiableCollection(new LinkedList<File>()); + } + + final List<File> l = Arrays.asList(f); + Collections.sort(l); + return Collections.unmodifiableCollection(l); + } + + } + + /** + * Gets the specified property of the specified file Item. The available + * file properties are "Name", "Size" and "Last Modified". If propertyId is + * not one of those, <code>null</code> is returned. + * + * @param itemId + * the ID of the file whose property is requested. + * @param propertyId + * the property's ID. + * @return the requested property's value, or <code>null</code> + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + + if (!(itemId instanceof File)) { + return null; + } + + if (propertyId.equals(PROPERTY_NAME)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_NAME, null); + } + + if (propertyId.equals(PROPERTY_ICON)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_ICON, null); + } + + if (propertyId.equals(PROPERTY_SIZE)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_SIZE, null); + } + + if (propertyId.equals(PROPERTY_LASTMODIFIED)) { + return new MethodProperty<Object>(getType(propertyId), + new FileItem((File) itemId), FILEITEM_LASTMODIFIED, null); + } + + return null; + } + + /** + * Gets the collection of available file properties. + * + * @return Unmodifiable collection containing all available file properties. + */ + @Override + public Collection<String> getContainerPropertyIds() { + return FILE_PROPERTIES; + } + + /** + * Gets the specified property's data type. "Name" is a <code>String</code>, + * "Size" is a <code>Long</code>, "Last Modified" is a <code>Date</code>. If + * propertyId is not one of those, <code>null</code> is returned. + * + * @param propertyId + * the ID of the property whose type is requested. + * @return data type of the requested property, or <code>null</code> + */ + @Override + public Class<?> getType(Object propertyId) { + + if (propertyId.equals(PROPERTY_NAME)) { + return String.class; + } + if (propertyId.equals(PROPERTY_ICON)) { + return Resource.class; + } + if (propertyId.equals(PROPERTY_SIZE)) { + return Long.class; + } + if (propertyId.equals(PROPERTY_LASTMODIFIED)) { + return Date.class; + } + return null; + } + + /** + * Internal method to recursively calculate the number of files under a root + * directory. + * + * @param f + * the root to start counting from. + */ + private int getFileCounts(File f) { + File[] l; + if (filter != null) { + l = f.listFiles(filter); + } else { + l = f.listFiles(); + } + + if (l == null) { + return 0; + } + int ret = l.length; + for (int i = 0; i < l.length; i++) { + if (l[i].isDirectory()) { + ret += getFileCounts(l[i]); + } + } + return ret; + } + + /** + * Gets the number of Items in the container. In effect, this is the + * combined amount of files and directories. + * + * @return Number of Items in the container. + */ + @Override + public int size() { + + if (recursive) { + int counts = 0; + for (int i = 0; i < roots.length; i++) { + counts += getFileCounts(roots[i]); + } + return counts; + } else { + File[] f; + if (roots.length == 1) { + if (filter != null) { + f = roots[0].listFiles(filter); + } else { + f = roots[0].listFiles(); + } + } else { + f = roots; + } + + if (f == null) { + return 0; + } + return f.length; + } + } + + /** + * A Item wrapper for files in a filesystem. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public class FileItem implements Item { + + /** + * The wrapped file. + */ + private final File file; + + /** + * Constructs a FileItem from a existing file. + */ + private FileItem(File file) { + this.file = file; + } + + /* + * Gets the specified property of this file. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Property getItemProperty(Object id) { + return getContainerProperty(file, id); + } + + /* + * Gets the IDs of all properties available for this item Don't add a + * JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Collection<String> getItemPropertyIds() { + return getContainerPropertyIds(); + } + + /** + * Calculates a integer hash-code for the Property that's unique inside + * the Item containing the Property. Two different Properties inside the + * same Item contained in the same list always have different + * hash-codes, though Properties in different Items may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return file.hashCode() ^ FilesystemContainer.this.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two + * Properties got from an Item with the same ID are equal. + * + * @param obj + * an object to compare with this object. + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof FileItem)) { + return false; + } + final FileItem fi = (FileItem) obj; + return fi.getHost() == getHost() && fi.file.equals(file); + } + + /** + * Gets the host of this file. + */ + private FilesystemContainer getHost() { + return FilesystemContainer.this; + } + + /** + * Gets the last modified date of this file. + * + * @return Date + */ + public Date lastModified() { + return new Date(file.lastModified()); + } + + /** + * Gets the name of this file. + * + * @return file name of this file. + */ + public String getName() { + return file.getName(); + } + + /** + * Gets the icon of this file. + * + * @return the icon of this file. + */ + public Resource getIcon() { + return FileTypeResolver.getIcon(file); + } + + /** + * Gets the size of this file. + * + * @return size + */ + public long getSize() { + if (file.isDirectory()) { + return 0; + } + return file.length(); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + if ("".equals(file.getName())) { + return file.getAbsolutePath(); + } + return file.getName(); + } + + /** + * Filesystem container does not support adding new properties. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("Filesystem container " + + "does not support adding new properties"); + } + + /** + * Filesystem container does not support removing properties. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Filesystem container does not support property removal"); + } + + } + + /** + * Generic file extension filter for displaying only files having certain + * extension. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public class FileExtensionFilter implements FilenameFilter, Serializable { + + private final String filter; + + /** + * Constructs a new FileExtensionFilter using given extension. + * + * @param fileExtension + * the File extension without the separator (dot). + */ + public FileExtensionFilter(String fileExtension) { + filter = "." + fileExtension; + } + + /** + * Allows only files with the extension and directories. + * + * @see java.io.FilenameFilter#accept(File, String) + */ + @Override + public boolean accept(File dir, String name) { + if (name.endsWith(filter)) { + return true; + } + return new File(dir, name).isDirectory(); + } + + } + + /** + * Returns the file filter used to limit the files in this container. + * + * @return Used filter instance or null if no filter is assigned. + */ + public FilenameFilter getFilter() { + return filter; + } + + /** + * Sets the file filter used to limit the files in this container. + * + * @param filter + * The filter to set. <code>null</code> disables filtering. + */ + public void setFilter(FilenameFilter filter) { + this.filter = filter; + } + + /** + * Sets the file filter used to limit the files in this container. + * + * @param extension + * the Filename extension (w/o separator) to limit the files in + * container. + */ + public void setFilter(String extension) { + filter = new FileExtensionFilter(extension); + } + + /** + * Is this container recursive filesystem. + * + * @return <code>true</code> if container is recursive, <code>false</code> + * otherwise. + */ + public boolean isRecursive() { + return recursive; + } + + /** + * Sets the container recursive property. Set this to false to limit the + * files directly under the root file. + * <p> + * Note : This is meaningful only if the root really is a directory. + * </p> + * + * @param recursive + * the New value for recursive property. + */ + public void setRecursive(boolean recursive) { + this.recursive = recursive; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem() + */ + @Override + public Object addItem() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object ) + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "File system container does not support this operation"); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/GeneratedPropertyContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/GeneratedPropertyContainer.java new file mode 100644 index 0000000000..8d7a1512f9 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/GeneratedPropertyContainer.java @@ -0,0 +1,776 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.filter.UnsupportedFilterException; +import com.vaadin.shared.data.sort.SortDirection; + +/** + * Container wrapper that adds support for generated properties. This container + * only supports adding new generated properties. Adding new normal properties + * should be done for the wrapped container. + * + * <p> + * Removing properties from this container does not remove anything from the + * wrapped container but instead only hides them from the results. These + * properties can be returned to this container by calling + * {@link #addContainerProperty(Object, Class, Object)} with same property id + * which was removed. + * + * <p> + * If wrapped container is Filterable and/or Sortable it should only be handled + * through this container as generated properties need to be handled in a + * specific way when sorting/filtering. + * + * <p> + * Items returned by this container do not support adding or removing + * properties. Generated properties are always read-only. Trying to make them + * editable throws an exception. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class GeneratedPropertyContainer extends AbstractContainer + implements Container.Indexed, Container.Sortable, Container.Filterable, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier { + + private final Container.Indexed wrappedContainer; + private final Map<Object, PropertyValueGenerator<?>> propertyGenerators; + private final Map<Filter, List<Filter>> activeFilters; + private Sortable sortableContainer = null; + private Filterable filterableContainer = null; + + /* Removed properties which are hidden but not actually removed */ + private final Set<Object> removedProperties = new HashSet<Object>(); + + /** + * Property implementation for generated properties + */ + protected static class GeneratedProperty<T> implements Property<T> { + + private Item item; + private Object itemId; + private Object propertyId; + private PropertyValueGenerator<T> generator; + + public GeneratedProperty(Item item, Object propertyId, Object itemId, + PropertyValueGenerator<T> generator) { + this.item = item; + this.itemId = itemId; + this.propertyId = propertyId; + this.generator = generator; + } + + @Override + public T getValue() { + return generator.getValue(item, itemId, propertyId); + } + + @Override + public void setValue(T newValue) throws ReadOnlyException { + throw new ReadOnlyException("Generated properties are read only"); + } + + @Override + public Class<? extends T> getType() { + return generator.getType(); + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public void setReadOnly(boolean newStatus) { + if (newStatus) { + // No-op + return; + } + throw new UnsupportedOperationException( + "Generated properties are read only"); + } + } + + /** + * Item implementation for generated properties, used to wrap the Item that + * belongs to the wrapped container. To reach that Item use + * {@link #getWrappedItem()} + */ + public class GeneratedPropertyItem implements Item { + + private Item wrappedItem; + private Object itemId; + + protected GeneratedPropertyItem(Object itemId, Item item) { + this.itemId = itemId; + wrappedItem = item; + } + + @Override + public Property getItemProperty(Object id) { + if (propertyGenerators.containsKey(id)) { + return createProperty(wrappedItem, id, itemId, + propertyGenerators.get(id)); + } + return wrappedItem.getItemProperty(id); + } + + @Override + public Collection<?> getItemPropertyIds() { + Set<Object> wrappedProperties = new LinkedHashSet<Object>( + wrappedItem.getItemPropertyIds()); + wrappedProperties.removeAll(removedProperties); + wrappedProperties.addAll(propertyGenerators.keySet()); + return wrappedProperties; + } + + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "GeneratedPropertyItem does not support adding properties"); + } + + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "GeneratedPropertyItem does not support removing properties"); + } + + /** + * Tests if the given object is the same as the this object. Two Items + * from the same container with the same ID are equal. + * + * @param obj + * an object to compare with this object + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null + || !obj.getClass().equals(GeneratedPropertyItem.class)) { + return false; + } + final GeneratedPropertyItem li = (GeneratedPropertyItem) obj; + return getContainer() == li.getContainer() + && itemId.equals(li.itemId); + } + + @Override + public int hashCode() { + return itemId.hashCode(); + } + + private GeneratedPropertyContainer getContainer() { + return GeneratedPropertyContainer.this; + } + + /** + * Returns the wrapped Item that belongs to the wrapped container + * + * @return wrapped item. + * @since 7.6.8 + */ + public Item getWrappedItem() { + return wrappedItem; + } + }; + + /** + * Base implementation for item add or remove events. This is used when an + * event is fired from wrapped container and needs to be reconstructed to + * act like it actually came from this container. + */ + protected abstract class GeneratedItemAddOrRemoveEvent + implements Serializable { + + private Object firstItemId; + private int firstIndex; + private int count; + + protected GeneratedItemAddOrRemoveEvent(Object itemId, int first, + int count) { + firstItemId = itemId; + firstIndex = first; + this.count = count; + } + + public Container getContainer() { + return GeneratedPropertyContainer.this; + } + + public Object getFirstItemId() { + return firstItemId; + } + + public int getFirstIndex() { + return firstIndex; + } + + public int getAffectedItemsCount() { + return count; + } + }; + + protected class GeneratedItemRemoveEvent + extends GeneratedItemAddOrRemoveEvent implements ItemRemoveEvent { + + protected GeneratedItemRemoveEvent(ItemRemoveEvent event) { + super(event.getFirstItemId(), event.getFirstIndex(), + event.getRemovedItemsCount()); + } + + @Override + public int getRemovedItemsCount() { + return super.getAffectedItemsCount(); + } + } + + protected class GeneratedItemAddEvent extends GeneratedItemAddOrRemoveEvent + implements ItemAddEvent { + + protected GeneratedItemAddEvent(ItemAddEvent event) { + super(event.getFirstItemId(), event.getFirstIndex(), + event.getAddedItemsCount()); + } + + @Override + public int getAddedItemsCount() { + return super.getAffectedItemsCount(); + } + + } + + /** + * Constructor for GeneratedPropertyContainer. + * + * @param container + * underlying indexed container + */ + public GeneratedPropertyContainer(Container.Indexed container) { + wrappedContainer = container; + propertyGenerators = new HashMap<Object, PropertyValueGenerator<?>>(); + + if (wrappedContainer instanceof Sortable) { + sortableContainer = (Sortable) wrappedContainer; + } + + if (wrappedContainer instanceof Filterable) { + activeFilters = new HashMap<Filter, List<Filter>>(); + filterableContainer = (Filterable) wrappedContainer; + } else { + activeFilters = null; + } + + // ItemSetChangeEvents + if (wrappedContainer instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) wrappedContainer) + .addItemSetChangeListener(new ItemSetChangeListener() { + + @Override + public void containerItemSetChange( + ItemSetChangeEvent event) { + if (event instanceof ItemAddEvent) { + final ItemAddEvent addEvent = (ItemAddEvent) event; + fireItemSetChange( + new GeneratedItemAddEvent(addEvent)); + } else if (event instanceof ItemRemoveEvent) { + final ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + fireItemSetChange(new GeneratedItemRemoveEvent( + removeEvent)); + } else { + fireItemSetChange(); + } + } + }); + } + + // PropertySetChangeEvents + if (wrappedContainer instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) wrappedContainer) + .addPropertySetChangeListener( + new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange( + PropertySetChangeEvent event) { + fireContainerPropertySetChange(); + } + }); + } + } + + /* Functions related to generated properties */ + + /** + * Add a new PropertyValueGenerator with given property id. This will + * override any existing properties with the same property id. Fires a + * PropertySetChangeEvent. + * + * @param propertyId + * property id + * @param generator + * a property value generator + */ + public void addGeneratedProperty(Object propertyId, + PropertyValueGenerator<?> generator) { + propertyGenerators.put(propertyId, generator); + fireContainerPropertySetChange(); + } + + /** + * Removes any possible PropertyValueGenerator with given property id. Fires + * a PropertySetChangeEvent. + * + * @param propertyId + * property id + */ + public void removeGeneratedProperty(Object propertyId) { + if (propertyGenerators.containsKey(propertyId)) { + propertyGenerators.remove(propertyId); + fireContainerPropertySetChange(); + } + } + + private Item createGeneratedPropertyItem(final Object itemId, + final Item item) { + return new GeneratedPropertyItem(itemId, item); + } + + private <T> Property<T> createProperty(final Item item, + final Object propertyId, final Object itemId, + final PropertyValueGenerator<T> generator) { + return new GeneratedProperty<T>(item, propertyId, itemId, generator); + } + + /* Listener functionality */ + + @Override + public void addItemSetChangeListener(ItemSetChangeListener listener) { + super.addItemSetChangeListener(listener); + } + + @Override + public void addListener(ItemSetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removeItemSetChangeListener(ItemSetChangeListener listener) { + super.removeItemSetChangeListener(listener); + } + + @Override + public void removeListener(ItemSetChangeListener listener) { + super.removeListener(listener); + } + + @Override + public void addPropertySetChangeListener( + PropertySetChangeListener listener) { + super.addPropertySetChangeListener(listener); + } + + @Override + public void addListener(PropertySetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removePropertySetChangeListener( + PropertySetChangeListener listener) { + super.removePropertySetChangeListener(listener); + } + + @Override + public void removeListener(PropertySetChangeListener listener) { + super.removeListener(listener); + } + + /* Filtering functionality */ + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + + List<Filter> addedFilters = new ArrayList<Filter>(); + for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators + .entrySet()) { + Object property = entry.getKey(); + if (filter.appliesToProperty(property)) { + // Have generated property modify filter to fit the original + // data in the container. + Filter modifiedFilter = entry.getValue().modifyFilter(filter); + filterableContainer.addContainerFilter(modifiedFilter); + // Keep track of added filters + addedFilters.add(modifiedFilter); + } + } + + if (addedFilters.isEmpty()) { + // No generated property modified this filter, use it as is + addedFilters.add(filter); + filterableContainer.addContainerFilter(filter); + } + // Map filter to actually added filters + activeFilters.put(filter, addedFilters); + } + + @Override + public void removeContainerFilter(Filter filter) { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + + if (activeFilters.containsKey(filter)) { + for (Filter f : activeFilters.get(filter)) { + filterableContainer.removeContainerFilter(f); + } + activeFilters.remove(filter); + } + } + + @Override + public void removeAllContainerFilters() { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + filterableContainer.removeAllContainerFilters(); + activeFilters.clear(); + } + + @Override + public Collection<Filter> getContainerFilters() { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + return Collections.unmodifiableSet(activeFilters.keySet()); + } + + /* Sorting functionality */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + if (sortableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not Sortable"); + } + + if (propertyId.length == 0) { + sortableContainer.sort(propertyId, ascending); + return; + } + + List<Object> actualSortProperties = new ArrayList<Object>(); + List<Boolean> actualSortDirections = new ArrayList<Boolean>(); + + for (int i = 0; i < propertyId.length; ++i) { + Object property = propertyId[i]; + SortDirection direction; + boolean isAscending = i < ascending.length ? ascending[i] : true; + if (isAscending) { + direction = SortDirection.ASCENDING; + } else { + direction = SortDirection.DESCENDING; + } + + if (propertyGenerators.containsKey(property)) { + // Sorting by a generated property. Generated property should + // modify sort orders to work with original properties in the + // container. + for (SortOrder s : propertyGenerators.get(property) + .getSortProperties( + new SortOrder(property, direction))) { + actualSortProperties.add(s.getPropertyId()); + actualSortDirections + .add(s.getDirection() == SortDirection.ASCENDING); + } + } else { + actualSortProperties.add(property); + actualSortDirections.add(isAscending); + } + } + + boolean[] actualAscending = new boolean[actualSortDirections.size()]; + for (int i = 0; i < actualAscending.length; ++i) { + actualAscending[i] = actualSortDirections.get(i); + } + + sortableContainer.sort(actualSortProperties.toArray(), actualAscending); + } + + @Override + public Collection<?> getSortableContainerPropertyIds() { + if (sortableContainer == null) { + return Collections.emptySet(); + } + + Set<Object> sortablePropertySet = new HashSet<Object>( + sortableContainer.getSortableContainerPropertyIds()); + for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators + .entrySet()) { + Object property = entry.getKey(); + SortOrder order = new SortOrder(property, SortDirection.ASCENDING); + if (entry.getValue().getSortProperties(order).length > 0) { + sortablePropertySet.add(property); + } else { + sortablePropertySet.remove(property); + } + } + + return sortablePropertySet; + } + + /* Item related overrides */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + Item item = wrappedContainer.addItemAfter(previousItemId, newItemId); + if (item == null) { + return null; + } + return createGeneratedPropertyItem(newItemId, item); + } + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + Item item = wrappedContainer.addItem(itemId); + if (item == null) { + return null; + } + return createGeneratedPropertyItem(itemId, item); + } + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + Item item = wrappedContainer.addItemAt(index, newItemId); + if (item == null) { + return null; + } + return createGeneratedPropertyItem(newItemId, item); + } + + @Override + public Item getItem(Object itemId) { + Item item = wrappedContainer.getItem(itemId); + if (item == null) { + return null; + } + + return createGeneratedPropertyItem(itemId, item); + } + + /* Property related overrides */ + + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + if (propertyGenerators.keySet().contains(propertyId)) { + return getItem(itemId).getItemProperty(propertyId); + } else if (!removedProperties.contains(propertyId)) { + return wrappedContainer.getContainerProperty(itemId, propertyId); + } + return null; + } + + /** + * Returns a list of propety ids available in this container. This + * collection will contain properties for generated properties. Removed + * properties will not show unless there is a generated property overriding + * those. + */ + @Override + public Collection<?> getContainerPropertyIds() { + Set<Object> wrappedProperties = new LinkedHashSet<Object>( + wrappedContainer.getContainerPropertyIds()); + wrappedProperties.removeAll(removedProperties); + wrappedProperties.addAll(propertyGenerators.keySet()); + return wrappedProperties; + } + + /** + * Adds a previously removed property back to GeneratedPropertyContainer. + * Adding a property that is not previously removed causes an + * UnsupportedOperationException. + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + if (!removedProperties.contains(propertyId)) { + throw new UnsupportedOperationException( + "GeneratedPropertyContainer does not support adding properties."); + } + removedProperties.remove(propertyId); + fireContainerPropertySetChange(); + return true; + } + + /** + * Marks the given property as hidden. This property from wrapped container + * will be removed from {@link #getContainerPropertyIds()} and is no longer + * be available in Items retrieved from this container. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + if (wrappedContainer.getContainerPropertyIds().contains(propertyId) + && removedProperties.add(propertyId)) { + fireContainerPropertySetChange(); + return true; + } + return false; + } + + /* Type related overrides */ + + @Override + public Class<?> getType(Object propertyId) { + if (propertyGenerators.containsKey(propertyId)) { + return propertyGenerators.get(propertyId).getType(); + } else { + return wrappedContainer.getType(propertyId); + } + } + + /* Unmodified functions */ + + @Override + public Object nextItemId(Object itemId) { + return wrappedContainer.nextItemId(itemId); + } + + @Override + public Object prevItemId(Object itemId) { + return wrappedContainer.prevItemId(itemId); + } + + @Override + public Object firstItemId() { + return wrappedContainer.firstItemId(); + } + + @Override + public Object lastItemId() { + return wrappedContainer.lastItemId(); + } + + @Override + public boolean isFirstId(Object itemId) { + return wrappedContainer.isFirstId(itemId); + } + + @Override + public boolean isLastId(Object itemId) { + return wrappedContainer.isLastId(itemId); + } + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + return wrappedContainer.addItemAfter(previousItemId); + } + + @Override + public Collection<?> getItemIds() { + return wrappedContainer.getItemIds(); + } + + @Override + public int size() { + return wrappedContainer.size(); + } + + @Override + public boolean containsId(Object itemId) { + return wrappedContainer.containsId(itemId); + } + + @Override + public Object addItem() throws UnsupportedOperationException { + return wrappedContainer.addItem(); + } + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return wrappedContainer.removeItem(itemId); + } + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + return wrappedContainer.removeAllItems(); + } + + @Override + public int indexOfId(Object itemId) { + return wrappedContainer.indexOfId(itemId); + } + + @Override + public Object getIdByIndex(int index) { + return wrappedContainer.getIdByIndex(index); + } + + @Override + public List<?> getItemIds(int startIndex, int numberOfItems) { + return wrappedContainer.getItemIds(startIndex, numberOfItems); + } + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + return wrappedContainer.addItemAt(index); + } + + /** + * Returns the original underlying container. + * + * @return the original underlying container + */ + public Container.Indexed getWrappedContainer() { + return wrappedContainer; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/HierarchicalContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/HierarchicalContainer.java new file mode 100644 index 0000000000..115fd91791 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/HierarchicalContainer.java @@ -0,0 +1,860 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; + +/** + * A specialized Container whose contents can be accessed like it was a + * tree-like structure. + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings("serial") +public class HierarchicalContainer extends IndexedContainer + implements Container.Hierarchical { + + /** + * Set of IDs of those contained Items that can't have children. + */ + private final HashSet<Object> noChildrenAllowed = new HashSet<Object>(); + + /** + * Mapping from Item ID to parent Item ID. + */ + private final HashMap<Object, Object> parent = new HashMap<Object, Object>(); + + /** + * Mapping from Item ID to parent Item ID for items included in the filtered + * container. + */ + private HashMap<Object, Object> filteredParent = null; + + /** + * Mapping from Item ID to a list of child IDs. + */ + private final HashMap<Object, LinkedList<Object>> children = new HashMap<Object, LinkedList<Object>>(); + + /** + * Mapping from Item ID to a list of child IDs when filtered + */ + private HashMap<Object, LinkedList<Object>> filteredChildren = null; + + /** + * List that contains all root elements of the container. + */ + private final LinkedList<Object> roots = new LinkedList<Object>(); + + /** + * List that contains all filtered root elements of the container. + */ + private LinkedList<Object> filteredRoots = null; + + /** + * Determines how filtering of the container is done. + */ + private boolean includeParentsWhenFiltering = true; + + /** + * Counts how many nested contents change disable calls are in progress. + * + * Pending events are only fired when the counter reaches zero again. + */ + private int contentChangedEventsDisabledCount = 0; + + private boolean contentsChangedEventPending; + + /* + * Can the specified Item have any children? Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + if (noChildrenAllowed.contains(itemId)) { + return false; + } + return containsId(itemId); + } + + /* + * Gets the IDs of the children of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> getChildren(Object itemId) { + LinkedList<Object> c; + + if (filteredChildren != null) { + c = filteredChildren.get(itemId); + } else { + c = children.get(itemId); + } + + if (c == null) { + return null; + } + return Collections.unmodifiableCollection(c); + } + + /* + * Gets the ID of the parent of the specified Item. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getParent(Object itemId) { + if (filteredParent != null) { + return filteredParent.get(itemId); + } + return parent.get(itemId); + } + + /* + * Is the Item corresponding to the given ID a leaf node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean hasChildren(Object itemId) { + if (filteredChildren != null) { + return filteredChildren.containsKey(itemId); + } else { + return children.containsKey(itemId); + } + } + + /* + * Is the Item corresponding to the given ID a root node? Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public boolean isRoot(Object itemId) { + // If the container is filtered the itemId must be among filteredRoots + // to be a root. + if (filteredRoots != null) { + if (!filteredRoots.contains(itemId)) { + return false; + } + } else { + // Container is not filtered + if (parent.containsKey(itemId)) { + return false; + } + } + + return containsId(itemId); + } + + /* + * Gets the IDs of the root elements in the container. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Collection<?> rootItemIds() { + if (filteredRoots != null) { + return Collections.unmodifiableCollection(filteredRoots); + } else { + return Collections.unmodifiableCollection(roots); + } + } + + /** + * <p> + * Sets the given Item's capability to have children. If the Item identified + * with the itemId already has children and the areChildrenAllowed is false + * this method fails and <code>false</code> is returned; the children must + * be first explicitly removed with + * {@link #setParent(Object itemId, Object newParentId)} or + * {@link com.vaadin.data.Container#removeItem(Object itemId)}. + * </p> + * + * @param itemId + * the ID of the Item in the container whose child capability is + * to be set. + * @param childrenAllowed + * the boolean value specifying if the Item can have children or + * not. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Updates status + if (childrenAllowed) { + noChildrenAllowed.remove(itemId); + } else { + noChildrenAllowed.add(itemId); + } + + return true; + } + + /** + * <p> + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (<code>canHaveChildren(newParentId) == true</code>). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent <code>null</code>. + * </p> + * + * @param itemId + * the ID of the item to be set as the child of the Item + * identified with newParentId. + * @param newParentId + * the ID of the Item that's to be the new parent of the Item + * identified with itemId. + * @return <code>true</code> if the operation succeeded, <code>false</code> + * if not + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + + // Checks that the item is in the container + if (!containsId(itemId)) { + return false; + } + + // Gets the old parent + final Object oldParentId = parent.get(itemId); + + // Checks if no change is necessary + if ((newParentId == null && oldParentId == null) + || ((newParentId != null) && newParentId.equals(oldParentId))) { + return true; + } + + // Making root? + if (newParentId == null) { + // The itemId should become a root so we need to + // - Remove it from the old parent's children list + // - Add it as a root + // - Remove it from the item -> parent list (parent is null for + // roots) + + // Removes from old parents children list + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + + } + + // Add to be a root + roots.add(itemId); + + // Updates parent + parent.remove(itemId); + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + // We get here when the item should not become a root and we need to + // - Verify the new parent exists and can have children + // - Check that the new parent is not a child of the selected itemId + // - Updated the item -> parent mapping to point to the new parent + // - Remove the item from the roots list if it was a root + // - Remove the item from the old parent's children list if it was not a + // root + + // Checks that the new parent exists in container and can have + // children + if (!containsId(newParentId) + || noChildrenAllowed.contains(newParentId)) { + return false; + } + + // Checks that setting parent doesn't result to a loop + Object o = newParentId; + while (o != null && !o.equals(itemId)) { + o = parent.get(o); + } + if (o != null) { + return false; + } + + // Updates parent + parent.put(itemId, newParentId); + LinkedList<Object> pcl = children.get(newParentId); + if (pcl == null) { + // Create an empty list for holding children if one were not + // previously created + pcl = new LinkedList<Object>(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Removes from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList<Object> l = children.get(oldParentId); + if (l != null) { + l.remove(itemId); + if (l.isEmpty()) { + children.remove(oldParentId); + } + } + } + + if (hasFilters()) { + // Refilter the container if setParent is called when filters + // are applied. Changing parent can change what is included in + // the filtered version (if includeParentsWhenFiltering==true). + doFilterContainer(hasFilters()); + } + + fireItemSetChange(); + + return true; + } + + private boolean hasFilters() { + return (filteredRoots != null); + } + + /** + * Moves a node (an Item) in the container immediately after a sibling node. + * The two nodes must have the same parent in the container. + * + * @param itemId + * the identifier of the moved node (Item) + * @param siblingId + * the identifier of the reference node (Item), after which the + * other node will be located + */ + public void moveAfterSibling(Object itemId, Object siblingId) { + Object parent2 = getParent(itemId); + LinkedList<Object> childrenList; + if (parent2 == null) { + childrenList = roots; + } else { + childrenList = children.get(parent2); + } + if (siblingId == null) { + childrenList.remove(itemId); + childrenList.addFirst(itemId); + + } else { + int oldIndex = childrenList.indexOf(itemId); + int indexOfSibling = childrenList.indexOf(siblingId); + if (indexOfSibling != -1 && oldIndex != -1) { + int newIndex; + if (oldIndex > indexOfSibling) { + newIndex = indexOfSibling + 1; + } else { + newIndex = indexOfSibling; + } + childrenList.remove(oldIndex); + childrenList.add(newIndex, itemId); + } else { + throw new IllegalArgumentException( + "Given identifiers no not have the same parent."); + } + } + fireItemSetChange(); + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem() + */ + @Override + public Object addItem() { + disableContentsChangeEvents(); + try { + final Object itemId = super.addItem(); + if (itemId == null) { + return null; + } + + if (!roots.contains(itemId)) { + roots.add(itemId); + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + } + return itemId; + } finally { + enableAndFireContentsChangeEvents(); + } + } + + @Override + protected void fireItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + if (contentsChangeEventsOn()) { + super.fireItemSetChange(event); + } else { + contentsChangedEventPending = true; + } + } + + private boolean contentsChangeEventsOn() { + return contentChangedEventsDisabledCount == 0; + } + + private void disableContentsChangeEvents() { + contentChangedEventsDisabledCount++; + } + + private void enableAndFireContentsChangeEvents() { + if (contentChangedEventsDisabledCount <= 0) { + getLogger().log(Level.WARNING, + "Mismatched calls to disable and enable contents change events in HierarchicalContainer"); + contentChangedEventsDisabledCount = 0; + } else { + contentChangedEventsDisabledCount--; + } + if (contentChangedEventsDisabledCount == 0) { + if (contentsChangedEventPending) { + fireItemSetChange(); + } + contentsChangedEventPending = false; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) { + disableContentsChangeEvents(); + try { + final Item item = super.addItem(itemId); + if (item == null) { + return null; + } + + roots.add(itemId); + + if (filteredRoots != null) { + if (passesFilters(itemId)) { + filteredRoots.add(itemId); + } + } + return item; + } finally { + enableAndFireContentsChangeEvents(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeAllItems() + */ + @Override + public boolean removeAllItems() { + disableContentsChangeEvents(); + try { + final boolean success = super.removeAllItems(); + + if (success) { + roots.clear(); + parent.clear(); + children.clear(); + noChildrenAllowed.clear(); + if (filteredRoots != null) { + filteredRoots = null; + } + if (filteredChildren != null) { + filteredChildren = null; + } + if (filteredParent != null) { + filteredParent = null; + } + } + return success; + } finally { + enableAndFireContentsChangeEvents(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#removeItem(java.lang.Object ) + */ + @Override + public boolean removeItem(Object itemId) { + disableContentsChangeEvents(); + try { + final boolean success = super.removeItem(itemId); + + if (success) { + // Remove from roots if this was a root + if (roots.remove(itemId)) { + + // If filtering is enabled we might need to remove it from + // the filtered list also + if (filteredRoots != null) { + filteredRoots.remove(itemId); + } + } + + // Clear the children list. Old children will now become root + // nodes + LinkedList<Object> childNodeIds = children.remove(itemId); + if (childNodeIds != null) { + if (filteredChildren != null) { + filteredChildren.remove(itemId); + } + for (Object childId : childNodeIds) { + setParent(childId, null); + } + } + + // Parent of the item that we are removing will contain the item + // id in its children list + final Object parentItemId = parent.get(itemId); + if (parentItemId != null) { + final LinkedList<Object> c = children.get(parentItemId); + if (c != null) { + c.remove(itemId); + + if (c.isEmpty()) { + children.remove(parentItemId); + } + + // Found in the children list so might also be in the + // filteredChildren list + if (filteredChildren != null) { + LinkedList<Object> f = filteredChildren + .get(parentItemId); + if (f != null) { + f.remove(itemId); + if (f.isEmpty()) { + filteredChildren.remove(parentItemId); + } + } + } + } + } + parent.remove(itemId); + if (filteredParent != null) { + // Item id no longer has a parent as the item id is not in + // the container. + filteredParent.remove(itemId); + } + noChildrenAllowed.remove(itemId); + } + + return success; + } finally { + enableAndFireContentsChangeEvents(); + } + } + + /** + * Removes the Item identified by given itemId and all its children. + * + * @see #removeItem(Object) + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public boolean removeItemRecursively(Object itemId) { + disableContentsChangeEvents(); + try { + boolean removeItemRecursively = removeItemRecursively(this, itemId); + return removeItemRecursively; + } finally { + enableAndFireContentsChangeEvents(); + } + } + + /** + * Removes the Item identified by given itemId and all its children from the + * given Container. + * + * @param container + * the container where the item is to be removed + * @param itemId + * the identifier of the Item to be removed + * @return true if the operation succeeded + */ + public static boolean removeItemRecursively( + Container.Hierarchical container, Object itemId) { + boolean success = true; + Collection<?> children2 = container.getChildren(itemId); + if (children2 != null) { + Object[] array = children2.toArray(); + for (int i = 0; i < array.length; i++) { + boolean removeItemRecursively = removeItemRecursively(container, + array[i]); + if (!removeItemRecursively) { + success = false; + } + } + } + // remove the root of subtree if children where succesfully removed + if (success) { + success = container.removeItem(itemId); + } + return success; + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#doSort() + */ + @Override + protected void doSort() { + super.doSort(); + + Collections.sort(roots, getItemSorter()); + for (LinkedList<Object> childList : children.values()) { + Collections.sort(childList, getItemSorter()); + } + } + + /** + * Used to control how filtering works. @see + * {@link #setIncludeParentsWhenFiltering(boolean)} for more information. + * + * @return true if all parents for items that match the filter are included + * when filtering, false if only the matching items are included + */ + public boolean isIncludeParentsWhenFiltering() { + return includeParentsWhenFiltering; + } + + /** + * Controls how the filtering of the container works. Set this to true to + * make filtering include parents for all matched items in addition to the + * items themselves. Setting this to false causes the filtering to only + * include the matching items and make items with excluded parents into root + * items. + * + * @param includeParentsWhenFiltering + * true to include all parents for items that match the filter, + * false to only include the matching items + */ + public void setIncludeParentsWhenFiltering( + boolean includeParentsWhenFiltering) { + this.includeParentsWhenFiltering = includeParentsWhenFiltering; + if (filteredRoots != null) { + // Currently filtered so needs to be re-filtered + doFilterContainer(true); + } + } + + /* + * Overridden to provide filtering for root & children items. + * + * (non-Javadoc) + * + * @see com.vaadin.data.util.IndexedContainer#updateContainerFiltering() + */ + @Override + protected boolean doFilterContainer(boolean hasFilters) { + if (!hasFilters) { + // All filters removed + filteredRoots = null; + filteredChildren = null; + filteredParent = null; + + return super.doFilterContainer(hasFilters); + } + + // Reset data structures + filteredRoots = new LinkedList<Object>(); + filteredChildren = new HashMap<Object, LinkedList<Object>>(); + filteredParent = new HashMap<Object, Object>(); + + if (includeParentsWhenFiltering) { + // Filter so that parents for items that match the filter are also + // included + HashSet<Object> includedItems = new HashSet<Object>(); + for (Object rootId : roots) { + if (filterIncludingParents(rootId, includedItems)) { + filteredRoots.add(rootId); + addFilteredChildrenRecursively(rootId, includedItems); + } + } + // includedItemIds now contains all the item ids that should be + // included. Filter IndexedContainer based on this + filterOverride = includedItems; + super.doFilterContainer(hasFilters); + filterOverride = null; + + return true; + } else { + // Filter by including all items that pass the filter and make items + // with no parent new root items + + // Filter IndexedContainer first so getItemIds return the items that + // match + super.doFilterContainer(hasFilters); + + LinkedHashSet<Object> filteredItemIds = new LinkedHashSet<Object>( + getItemIds()); + + for (Object itemId : filteredItemIds) { + Object itemParent = parent.get(itemId); + if (itemParent == null + || !filteredItemIds.contains(itemParent)) { + // Parent is not included or this was a root, in both cases + // this should be a filtered root + filteredRoots.add(itemId); + } else { + // Parent is included. Add this to the children list (create + // it first if necessary) + addFilteredChild(itemParent, itemId); + } + } + + return true; + } + } + + /** + * Adds the given childItemId as a filteredChildren for the parentItemId and + * sets it filteredParent. + * + * @param parentItemId + * @param childItemId + */ + private void addFilteredChild(Object parentItemId, Object childItemId) { + LinkedList<Object> parentToChildrenList = filteredChildren + .get(parentItemId); + if (parentToChildrenList == null) { + parentToChildrenList = new LinkedList<Object>(); + filteredChildren.put(parentItemId, parentToChildrenList); + } + filteredParent.put(childItemId, parentItemId); + parentToChildrenList.add(childItemId); + + } + + /** + * Recursively adds all items in the includedItems list to the + * filteredChildren map in the same order as they are in the children map. + * Starts from parentItemId and recurses down as long as child items that + * should be included are found. + * + * @param parentItemId + * The item id to start recurse from. Not added to a + * filteredChildren list + * @param includedItems + * Set containing the item ids for the items that should be + * included in the filteredChildren map + */ + private void addFilteredChildrenRecursively(Object parentItemId, + HashSet<Object> includedItems) { + LinkedList<Object> childList = children.get(parentItemId); + if (childList == null) { + return; + } + + for (Object childItemId : childList) { + if (includedItems.contains(childItemId)) { + addFilteredChild(parentItemId, childItemId); + addFilteredChildrenRecursively(childItemId, includedItems); + } + } + } + + /** + * Scans the itemId and all its children for which items should be included + * when filtering. All items which passes the filters are included. + * Additionally all items that have a child node that should be included are + * also themselves included. + * + * @param itemId + * @param includedItems + * @return true if the itemId should be included in the filtered container. + */ + private boolean filterIncludingParents(Object itemId, + HashSet<Object> includedItems) { + boolean toBeIncluded = passesFilters(itemId); + + LinkedList<Object> childList = children.get(itemId); + if (childList != null) { + for (Object childItemId : children.get(itemId)) { + toBeIncluded |= filterIncludingParents(childItemId, + includedItems); + } + } + + if (toBeIncluded) { + includedItems.add(itemId); + } + return toBeIncluded; + } + + private Set<Object> filterOverride = null; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.IndexedContainer#passesFilters(java.lang.Object) + */ + @Override + protected boolean passesFilters(Object itemId) { + if (filterOverride != null) { + return filterOverride.contains(itemId); + } else { + return super.passesFilters(itemId); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(HierarchicalContainer.class.getName()); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/IndexedContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/IndexedContainer.java new file mode 100644 index 0000000000..4ff253394a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/IndexedContainer.java @@ -0,0 +1,1201 @@ +/* + * Copyright 2000-2016 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.data.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * An implementation of the <code>{@link Container.Indexed}</code> interface + * with all important features. + * </p> + * + * Features: + * <ul> + * <li>{@link Container.Indexed} + * <li>{@link Container.Ordered} + * <li>{@link Container.Sortable} + * <li>{@link Container.Filterable} + * <li>{@link Cloneable} (deprecated, might be removed in the future) + * <li>Sends all needed events on content changes. + * </ul> + * + * @see com.vaadin.data.Container + * + * @author Vaadin Ltd. + * @since 3.0 + */ + +@SuppressWarnings("serial") +// item type is really IndexedContainerItem, but using Item not to show it in +// public API +public class IndexedContainer + extends AbstractInMemoryContainer<Object, Object, Item> + implements Container.PropertySetChangeNotifier, + Property.ValueChangeNotifier, Container.Sortable, Cloneable, + Container.Filterable, Container.SimpleFilterable { + + /* Internal structure */ + + /** + * Linked list of ordered Property IDs. + */ + private ArrayList<Object> propertyIds = new ArrayList<Object>(); + + /** + * Property ID to type mapping. + */ + private Hashtable<Object, Class<?>> types = new Hashtable<Object, Class<?>>(); + + /** + * Hash of Items, where each Item is implemented as a mapping from Property + * ID to Property value. + */ + private Hashtable<Object, Map<Object, Object>> items = new Hashtable<Object, Map<Object, Object>>(); + + /** + * Set of properties that are read-only. + */ + private HashSet<Property<?>> readOnlyProperties = new HashSet<Property<?>>(); + + /** + * List of all Property value change event listeners listening all the + * properties. + */ + private LinkedList<Property.ValueChangeListener> propertyValueChangeListeners = null; + + /** + * Data structure containing all listeners interested in changes to single + * Properties. The data structure is a hashtable mapping Property IDs to a + * hashtable that maps Item IDs to a linked list of listeners listening + * Property identified by given Property ID and Item ID. + */ + private Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>> singlePropertyValueChangeListeners = null; + + private HashMap<Object, Object> defaultPropertyValues; + + private int nextGeneratedItemId = 1; + + /* Container constructors */ + + public IndexedContainer() { + super(); + } + + public IndexedContainer(Collection<?> itemIds) { + this(); + if (items != null) { + for (final Iterator<?> i = itemIds.iterator(); i.hasNext();) { + Object itemId = i.next(); + internalAddItemAtEnd(itemId, new IndexedContainerItem(itemId), + false); + } + filterAll(); + } + } + + /* Container methods */ + + @Override + protected Item getUnfilteredItem(Object itemId) { + if (itemId != null && items.containsKey(itemId)) { + return new IndexedContainerItem(itemId); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + @Override + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Gets the type of a Property stored in the list. + * + * @param id + * the ID of the Property. + * @return Type of the requested Property + */ + @Override + public Class<?> getType(Object propertyId) { + return types.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + // map lookup more efficient than propertyIds if there are many + // properties + if (!containsId(itemId) || propertyId == null + || !types.containsKey(propertyId)) { + return null; + } + + return new IndexedContainerProperty(itemId, propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) { + + // Fails, if nulls are given + if (propertyId == null || type == null) { + return false; + } + + // Fails if the Property is already present + if (propertyIds.contains(propertyId)) { + return false; + } + + // Adds the Property to Property list and types + propertyIds.add(propertyId); + types.put(propertyId, type); + + // If default value is given, set it + if (defaultValue != null) { + // for existing rows + for (final Iterator<?> i = getAllItemIds().iterator(); i + .hasNext();) { + getItem(i.next()).getItemProperty(propertyId) + .setValue(defaultValue); + } + // store for next rows + if (defaultPropertyValues == null) { + defaultPropertyValues = new HashMap<Object, Object>(); + } + defaultPropertyValues.put(propertyId, defaultValue); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() { + int origSize = size(); + Object firstItem = getFirstVisibleItem(); + + internalRemoveAllItems(); + + items.clear(); + + // fire event only if the visible view changed, regardless of whether + // filtered out items were removed or not + if (origSize != 0) { + // Sends a change event + fireItemsRemoved(0, firstItem, origSize); + } + + return true; + } + + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. + */ + @Override + public Object addItem() { + + // Creates a new id + final Object id = generateId(); + + // Adds the Item into container + addItem(id); + + return id; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) { + Item item = internalAddItemAtEnd(itemId, + new IndexedContainerItem(itemId), false); + if (item == null) { + return null; + } else if (!isFiltered()) { + // always the last item + fireItemAdded(size() - 1, itemId, item); + } else if (passesFilters(itemId) && !containsId(itemId)) { + getFilteredItemIds().add(itemId); + // always the last item + fireItemAdded(size() - 1, itemId, item); + } + return item; + } + + /** + * Helper method to add default values for items if available + * + * @param t + * data table of added item + */ + private void addDefaultValues(Hashtable<Object, Object> t) { + if (defaultPropertyValues != null) { + for (Object key : defaultPropertyValues.keySet()) { + t.put(key, defaultPropertyValues.get(key)); + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) { + if (itemId == null || items.remove(itemId) == null) { + return false; + } + int origSize = size(); + int position = indexOfId(itemId); + if (internalRemoveItem(itemId)) { + // fire event only if the visible view changed, regardless of + // whether filtered out items were removed or not + if (size() != origSize) { + fireItemRemoved(position, itemId); + } + + return true; + } else { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object ) + */ + @Override + public boolean removeContainerProperty(Object propertyId) { + + // Fails if the Property is not present + if (!propertyIds.contains(propertyId)) { + return false; + } + + // Removes the Property to Property list and types + propertyIds.remove(propertyId); + types.remove(propertyId); + if (defaultPropertyValues != null) { + defaultPropertyValues.remove(propertyId); + } + + // If remove the Property from all Items + for (final Iterator<Object> i = getAllItemIds().iterator(); i + .hasNext();) { + items.get(i.next()).remove(propertyId); + } + + // Sends a change event + fireContainerPropertySetChange(); + + return true; + } + + /* Container.Ordered methods */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) { + return internalAddItemAfter(previousItemId, newItemId, + new IndexedContainerItem(newItemId), true); + } + + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. + */ + @Override + public Object addItemAfter(Object previousItemId) { + + // Creates a new id + final Object id = generateId(); + + if (addItemAfter(previousItemId, id) != null) { + return id; + } else { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object) + */ + @Override + public Item addItemAt(int index, Object newItemId) { + return internalAddItemAt(index, newItemId, + new IndexedContainerItem(newItemId), true); + } + + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. + */ + @Override + public Object addItemAt(int index) { + + // Creates a new id + final Object id = generateId(); + + // Adds the Item into container + addItemAt(index, id); + + return id; + } + + /** + * Generates an unique identifier for use as an item id. Guarantees that the + * generated id is not currently used as an id. + * + * @return + */ + private Serializable generateId() { + Serializable id; + do { + id = Integer.valueOf(nextGeneratedItemId++); + } while (items.containsKey(id)); + + return id; + } + + @Override + protected void registerNewItem(int index, Object newItemId, Item item) { + Hashtable<Object, Object> t = new Hashtable<Object, Object>(); + items.put(newItemId, t); + addDefaultValues(t); + } + + /* Event notifiers */ + + /** + * An <code>event</code> object specifying the list whose Item set has + * changed. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public static class ItemSetChangeEvent extends BaseItemSetChangeEvent { + + private final int addedItemIndex; + + private ItemSetChangeEvent(IndexedContainer source, + int addedItemIndex) { + super(source); + this.addedItemIndex = addedItemIndex; + } + + /** + * Iff one item is added, gives its index. + * + * @return -1 if either multiple items are changed or some other change + * than add is done. + */ + public int getAddedItemIndex() { + return addedItemIndex; + } + + } + + /** + * An <code>event</code> object specifying the Property in a list whose + * value has changed. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + private static class PropertyValueChangeEvent extends EventObject + implements Property.ValueChangeEvent, Serializable { + + private PropertyValueChangeEvent(Property source) { + super(source); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeEvent#getProperty() + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + + } + + @Override + public void addPropertySetChangeListener( + Container.PropertySetChangeListener listener) { + super.addPropertySetChangeListener(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addPropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Deprecated + @Override + public void addListener(Container.PropertySetChangeListener listener) { + addPropertySetChangeListener(listener); + } + + @Override + public void removePropertySetChangeListener( + Container.PropertySetChangeListener listener) { + super.removePropertySetChangeListener(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Deprecated + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + removePropertySetChangeListener(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com. + * vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addValueChangeListener(Property.ValueChangeListener listener) { + if (propertyValueChangeListeners == null) { + propertyValueChangeListeners = new LinkedList<Property.ValueChangeListener>(); + } + propertyValueChangeListeners.add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addValueChangeListener(com.vaadin.data.Property.ValueChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Property.ValueChangeListener listener) { + addValueChangeListener(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com + * .vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeValueChangeListener( + Property.ValueChangeListener listener) { + if (propertyValueChangeListeners != null) { + propertyValueChangeListeners.remove(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeValueChangeListener(com.vaadin.data.Property.ValueChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Property.ValueChangeListener listener) { + removeValueChangeListener(listener); + } + + /** + * Sends a Property value change event to all interested listeners. + * + * @param source + * the IndexedContainerProperty object. + */ + private void firePropertyValueChange(IndexedContainerProperty source) { + + // Sends event to listeners listening all value changes + if (propertyValueChangeListeners != null) { + final Object[] l = propertyValueChangeListeners.toArray(); + final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent( + source); + for (int i = 0; i < l.length; i++) { + ((Property.ValueChangeListener) l[i]).valueChange(event); + } + } + + // Sends event to single property value change listeners + if (singlePropertyValueChangeListeners != null) { + final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(source.propertyId); + if (propertySetToListenerListMap != null) { + final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(source.itemId); + if (listenerList != null) { + final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent( + source); + Object[] listeners = listenerList.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Property.ValueChangeListener) listeners[i]) + .valueChange(event); + } + } + } + } + + } + + @Override + public Collection<?> getListeners(Class<?> eventType) { + if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) { + if (propertyValueChangeListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertyValueChangeListeners); + } + } + return super.getListeners(eventType); + } + + @Override + protected void fireItemAdded(int position, Object itemId, Item item) { + if (position >= 0) { + super.fireItemAdded(position, itemId, item); + } + } + + @Override + protected void fireItemSetChange() { + fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, -1)); + } + + /** + * Adds new single Property change listener. + * + * @param propertyId + * the ID of the Property to add. + * @param itemId + * the ID of the Item . + * @param listener + * the listener to be added. + */ + private void addSinglePropertyChangeListener(Object propertyId, + Object itemId, Property.ValueChangeListener listener) { + if (listener != null) { + if (singlePropertyValueChangeListeners == null) { + singlePropertyValueChangeListeners = new Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>(); + } + Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap == null) { + propertySetToListenerListMap = new Hashtable<Object, List<Property.ValueChangeListener>>(); + singlePropertyValueChangeListeners.put(propertyId, + propertySetToListenerListMap); + } + List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(itemId); + if (listenerList == null) { + listenerList = new LinkedList<Property.ValueChangeListener>(); + propertySetToListenerListMap.put(itemId, listenerList); + } + listenerList.add(listener); + } + } + + /** + * Removes a previously registered single Property change listener. + * + * @param propertyId + * the ID of the Property to remove. + * @param itemId + * the ID of the Item. + * @param listener + * the listener to be removed. + */ + private void removeSinglePropertyChangeListener(Object propertyId, + Object itemId, Property.ValueChangeListener listener) { + if (listener != null && singlePropertyValueChangeListeners != null) { + final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap != null) { + final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap + .get(itemId); + if (listenerList != null) { + listenerList.remove(listener); + if (listenerList.isEmpty()) { + propertySetToListenerListMap.remove(itemId); + } + } + if (propertySetToListenerListMap.isEmpty()) { + singlePropertyValueChangeListeners.remove(propertyId); + } + } + if (singlePropertyValueChangeListeners.isEmpty()) { + singlePropertyValueChangeListeners = null; + } + } + } + + /* Internal Item and Property implementations */ + + /* + * A class implementing the com.vaadin.data.Item interface to be contained + * in the list. + * + * @author Vaadin Ltd. + * + * + * @since 3.0 + */ + class IndexedContainerItem implements Item { + + /** + * Item ID in the host container for this Item. + */ + private final Object itemId; + + /** + * Constructs a new ListItem instance and connects it to a host + * container. + * + * @param itemId + * the Item ID of the new Item. + */ + private IndexedContainerItem(Object itemId) { + this.itemId = itemId; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Item#getItemProperty(java.lang.Object) + */ + @Override + public Property getItemProperty(Object id) { + if (!propertyIds.contains(id)) { + return null; + } + + return new IndexedContainerProperty(itemId, id); + } + + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Gets the <code>String</code> representation of the contents of the + * Item. The format of the string is a space separated catenation of the + * <code>String</code> representations of the values of the Properties + * contained by the Item. + * + * @return <code>String</code> representation of the Item contents + */ + @Override + public String toString() { + String retValue = ""; + + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + final Object propertyId = i.next(); + retValue += getItemProperty(propertyId).getValue(); + if (i.hasNext()) { + retValue += " "; + } + } + + return retValue; + } + + /** + * Calculates a integer hash-code for the Item that's unique inside the + * list. Two Items inside the same list have always different + * hash-codes, though Items in different lists may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return itemId.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two Items + * got from a list container with the same ID are equal. + * + * @param obj + * an object to compare with this object + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null + || !obj.getClass().equals(IndexedContainerItem.class)) { + return false; + } + final IndexedContainerItem li = (IndexedContainerItem) obj; + return getHost() == li.getHost() && itemId.equals(li.itemId); + } + + private IndexedContainer getHost() { + return IndexedContainer.this; + } + + /** + * IndexedContainerItem does not support adding new properties. Add + * properties at container level. See + * {@link IndexedContainer#addContainerProperty(Object, Class, Object)} + * + * @see com.vaadin.data.Item#addProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException("Indexed container item " + + "does not support adding new properties"); + } + + /** + * Indexed container does not support removing properties. Remove + * properties at container level. See + * {@link IndexedContainer#removeContainerProperty(Object)} + * + * @see com.vaadin.data.Item#removeProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "Indexed container item does not support property removal"); + } + + } + + /** + * A class implementing the {@link Property} interface to be contained in + * the {@link IndexedContainerItem} contained in the + * {@link IndexedContainer}. + * + * @author Vaadin Ltd. + * + * @since 3.0 + */ + private class IndexedContainerProperty<T> + implements Property<T>, Property.ValueChangeNotifier { + + /** + * ID of the Item, where this property resides. + */ + private final Object itemId; + + /** + * Id of the Property. + */ + private final Object propertyId; + + /** + * Constructs a new {@link IndexedContainerProperty} object. + * + * @param itemId + * the ID of the Item to connect the new Property to. + * @param propertyId + * the Property ID of the new Property. + * @param host + * the list that contains the Item to contain the new + * Property. + */ + private IndexedContainerProperty(Object itemId, Object propertyId) { + if (itemId == null || propertyId == null) { + // Null ids are not accepted + throw new NullPointerException( + "Container item or property ids can not be null"); + } + this.propertyId = propertyId; + this.itemId = itemId; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class<T> getType() { + return (Class<T>) types.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#getValue() + */ + @Override + public T getValue() { + return (T) items.get(itemId).get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#isReadOnly() + */ + @Override + public boolean isReadOnly() { + return readOnlyProperties.contains(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean newStatus) { + if (newStatus) { + readOnlyProperties.add(this); + } else { + readOnlyProperties.remove(this); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property#setValue(java.lang.Object) + */ + @Override + public void setValue(Object newValue) + throws Property.ReadOnlyException { + // Gets the Property set + final Map<Object, Object> propertySet = items.get(itemId); + + // Support null values on all types + if (newValue == null) { + propertySet.remove(propertyId); + } else if (getType().isAssignableFrom(newValue.getClass())) { + propertySet.put(propertyId, newValue); + } else { + throw new IllegalArgumentException( + "Value is of invalid type, got " + + newValue.getClass().getName() + " but " + + getType().getName() + " was expected"); + } + + // update the container filtering if this property is being filtered + if (isPropertyFiltered(propertyId)) { + filterAll(); + } + + firePropertyValueChange(this); + } + + /** + * Calculates a integer hash-code for the Property that's unique inside + * the Item containing the Property. Two different Properties inside the + * same Item contained in the same list always have different + * hash-codes, though Properties in different Items may have identical + * hash-codes. + * + * @return A locally unique hash-code as integer + */ + @Override + public int hashCode() { + return itemId.hashCode() ^ propertyId.hashCode(); + } + + /** + * Tests if the given object is the same as the this object. Two + * Properties got from an Item with the same ID are equal. + * + * @param obj + * an object to compare with this object + * @return <code>true</code> if the given object is the same as this + * object, <code>false</code> if not + */ + @Override + public boolean equals(Object obj) { + if (obj == null + || !obj.getClass().equals(IndexedContainerProperty.class)) { + return false; + } + final IndexedContainerProperty lp = (IndexedContainerProperty) obj; + return lp.getHost() == getHost() && lp.propertyId.equals(propertyId) + && lp.itemId.equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener( + * com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addValueChangeListener( + Property.ValueChangeListener listener) { + addSinglePropertyChangeListener(propertyId, itemId, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addValueChangeListener(com.vaadin.data.Property.ValueChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Property.ValueChangeListener listener) { + addValueChangeListener(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener + * (com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeValueChangeListener( + Property.ValueChangeListener listener) { + removeSinglePropertyChangeListener(propertyId, itemId, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeValueChangeListener(com.vaadin.data.Property.ValueChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Property.ValueChangeListener listener) { + removeValueChangeListener(listener); + } + + private IndexedContainer getHost() { + return IndexedContainer.this; + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sortContainer(propertyId, ascending); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds + * () + */ + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getSortablePropertyIds(); + } + + @Override + public ItemSorter getItemSorter() { + return super.getItemSorter(); + } + + @Override + public void setItemSorter(ItemSorter itemSorter) { + super.setItemSorter(itemSorter); + } + + /** + * Supports cloning of the IndexedContainer cleanly. + * + * @throws CloneNotSupportedException + * if an object cannot be cloned. . + * + * @deprecated As of 6.6. Cloning support might be removed from + * IndexedContainer in the future + */ + @Deprecated + @Override + public Object clone() throws CloneNotSupportedException { + + // Creates the clone + final IndexedContainer nc = new IndexedContainer(); + + // Clone the shallow properties + nc.setAllItemIds(getAllItemIds() != null + ? (ListSet<Object>) ((ListSet<Object>) getAllItemIds()).clone() + : null); + nc.setItemSetChangeListeners(getItemSetChangeListeners() != null + ? new LinkedList<Container.ItemSetChangeListener>( + getItemSetChangeListeners()) + : null); + nc.propertyIds = propertyIds != null + ? (ArrayList<Object>) propertyIds.clone() : null; + nc.setPropertySetChangeListeners(getPropertySetChangeListeners() != null + ? new LinkedList<Container.PropertySetChangeListener>( + getPropertySetChangeListeners()) + : null); + nc.propertyValueChangeListeners = propertyValueChangeListeners != null + ? (LinkedList<Property.ValueChangeListener>) propertyValueChangeListeners + .clone() + : null; + nc.readOnlyProperties = readOnlyProperties != null + ? (HashSet<Property<?>>) readOnlyProperties.clone() : null; + nc.singlePropertyValueChangeListeners = singlePropertyValueChangeListeners != null + ? (Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>) singlePropertyValueChangeListeners + .clone() + : null; + + nc.types = types != null ? (Hashtable<Object, Class<?>>) types.clone() + : null; + + nc.setFilters( + (HashSet<Filter>) ((HashSet<Filter>) getFilters()).clone()); + + nc.setFilteredItemIds(getFilteredItemIds() == null ? null + : (ListSet<Object>) ((ListSet<Object>) getFilteredItemIds()) + .clone()); + + // Clone property-values + if (items == null) { + nc.items = null; + } else { + nc.items = new Hashtable<Object, Map<Object, Object>>(); + for (final Iterator<?> i = items.keySet().iterator(); i + .hasNext();) { + final Object id = i.next(); + final Hashtable<Object, Object> it = (Hashtable<Object, Object>) items + .get(id); + nc.items.put(id, (Map<Object, Object>) it.clone()); + } + } + + return nc; + } + + @Override + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + try { + addFilter(new SimpleStringFilter(propertyId, filterString, + ignoreCase, onlyMatchPrefix)); + } catch (UnsupportedFilterException e) { + // the filter instance created here is always valid for in-memory + // containers + } + } + + @Override + public void removeAllContainerFilters() { + removeAllFilters(); + } + + @Override + public void removeContainerFilters(Object propertyId) { + removeFilters(propertyId); + } + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + addFilter(filter); + } + + @Override + public void removeContainerFilter(Filter filter) { + removeFilter(filter); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public boolean hasContainerFilters() { + return super.hasContainerFilters(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return super.getContainerFilters(); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java new file mode 100644 index 0000000000..d3559b5652 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java @@ -0,0 +1,103 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.io.Serializable; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.data.util.sqlcontainer.query.FreeformQuery; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * CacheFlushNotifier is a simple static notification mechanism to inform other + * SQLContainers that the contents of their caches may have become stale. + */ +class CacheFlushNotifier implements Serializable { + /* + * SQLContainer instance reference list and dead reference queue. Used for + * the cache flush notification feature. + */ + private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>(); + private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>(); + + /** + * Adds the given SQLContainer to the cache flush notification receiver list + * + * @param c + * Container to add + */ + public static void addInstance(SQLContainer c) { + removeDeadReferences(); + if (c != null) { + allInstances.add(new WeakReference<SQLContainer>(c, deadInstances)); + } + } + + /** + * Removes dead references from instance list + */ + private static void removeDeadReferences() { + java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances + .poll(); + while (dead != null) { + allInstances.remove(dead); + dead = deadInstances.poll(); + } + } + + /** + * Iterates through the instances and notifies containers which are + * connected to the same table or are using the same query string. + * + * @param c + * SQLContainer that issued the cache flush notification + */ + public static void notifyOfCacheFlush(SQLContainer c) { + removeDeadReferences(); + for (WeakReference<SQLContainer> wr : allInstances) { + if (wr.get() != null) { + SQLContainer wrc = wr.get(); + if (wrc == null) { + continue; + } + /* + * If the reference points to the container sending the + * notification, do nothing. + */ + if (wrc.equals(c)) { + continue; + } + /* Compare QueryDelegate types and tableName/queryString */ + QueryDelegate wrQd = wrc.getQueryDelegate(); + QueryDelegate qd = c.getQueryDelegate(); + if (wrQd instanceof TableQuery && qd instanceof TableQuery + && ((TableQuery) wrQd).getTableName() + .equals(((TableQuery) qd).getTableName())) { + wrc.refresh(); + } else if (wrQd instanceof FreeformQuery + && qd instanceof FreeformQuery + && ((FreeformQuery) wrQd).getQueryString().equals( + ((FreeformQuery) qd).getQueryString())) { + wrc.refresh(); + } + } + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheMap.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheMap.java new file mode 100644 index 0000000000..77919f72b2 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/CacheMap.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum + * number of items. In SQLContainer this is used for RowItem -cache. Cache size + * will be two times the page length parameter of the container. + */ +class CacheMap<K, V> extends LinkedHashMap<K, V> { + private static final long serialVersionUID = 679999766473555231L; + private int cacheLimit = SQLContainer.CACHE_RATIO + * SQLContainer.DEFAULT_PAGE_LENGTH; + + @Override + protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { + return size() > cacheLimit; + } + + void setCacheLimit(int limit) { + cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH; + } + + int getCacheLimit() { + return cacheLimit; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ColumnProperty.java new file mode 100644 index 0000000000..e7d83e118d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ColumnProperty.java @@ -0,0 +1,357 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException; + +/** + * ColumnProperty represents the value of one column in a RowItem. In addition + * to the value, ColumnProperty also contains some basic column attributes such + * as nullability status, read-only status and data type. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual column in a database table. + */ +final public class ColumnProperty implements Property { + private static final long serialVersionUID = -3694463129581802457L; + + private RowItem owner; + + private String propertyId; + + private boolean readOnly; + private boolean allowReadOnlyChange = true; + private boolean nullable = true; + + private Object value; + private Object changedValue; + private Class<?> type; + + private boolean modified; + + private boolean versionColumn; + private boolean primaryKey = false; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private ColumnProperty() { + } + + /** + * Deprecated constructor for ColumnProperty. If this is used the primary + * keys are not identified correctly in some cases for some databases (i.e. + * Oracle). See http://dev.vaadin.com/ticket/9145. + * + * @param propertyId + * @param readOnly + * @param allowReadOnlyChange + * @param nullable + * @param value + * @param type + * + * @deprecated As of 7.0. Use + * {@link #ColumnProperty(String, boolean, boolean, boolean, boolean, Object, Class) + * instead + */ + @Deprecated + public ColumnProperty(String propertyId, boolean readOnly, + boolean allowReadOnlyChange, boolean nullable, Object value, + Class<?> type) { + this(propertyId, readOnly, allowReadOnlyChange, nullable, false, value, + type); + } + + /** + * Creates a new ColumnProperty instance. + * + * @param propertyId + * The ID of this property. + * @param readOnly + * Whether this property is read-only. + * @param allowReadOnlyChange + * Whether the read-only status of this property can be changed. + * @param nullable + * Whether this property accepts null values. + * @param primaryKey + * Whether this property corresponds to a database primary key. + * @param value + * The value of this property. + * @param type + * The type of this property. + */ + public ColumnProperty(String propertyId, boolean readOnly, + boolean allowReadOnlyChange, boolean nullable, boolean primaryKey, + Object value, Class<?> type) { + + if (propertyId == null) { + throw new IllegalArgumentException("Properties must be named."); + } + if (type == null) { + throw new IllegalArgumentException("Property type must be set."); + } + this.propertyId = propertyId; + this.type = type; + this.value = value; + + this.allowReadOnlyChange = allowReadOnlyChange; + this.nullable = nullable; + this.readOnly = readOnly; + this.primaryKey = primaryKey; + } + + /** + * Returns the current value for this property. To get the previous value + * (if one exists) for a modified property use {@link #getOldValue()}. + * + * @return + */ + @Override + public Object getValue() { + if (isModified()) { + return changedValue; + } + return value; + } + + /** + * Returns the original non-modified value of this property if it has been + * modified. + * + * @return The original value if <code>isModified()</code> is true, + * <code>getValue()</code> otherwise. + */ + public Object getOldValue() { + return value; + } + + @Override + public void setValue(Object newValue) + throws ReadOnlyException, ConversionException { + if (newValue == null && !nullable) { + throw new NotNullableException( + "Null values are not allowed for this property."); + } + if (readOnly) { + throw new ReadOnlyException( + "Cannot set value for read-only property."); + } + + /* Check if this property is a date property. */ + boolean isDateProperty = Time.class.equals(getType()) + || Date.class.equals(getType()) + || Timestamp.class.equals(getType()); + + if (newValue != null) { + /* Handle SQL dates, times and Timestamps given as java.util.Date */ + if (isDateProperty) { + /* + * Try to get the millisecond value from the new value of this + * property. Possible type to convert from is java.util.Date. + */ + long millis = 0; + if (newValue instanceof java.util.Date) { + millis = ((java.util.Date) newValue).getTime(); + /* + * Create the new object based on the millisecond value, + * according to the type of this property. + */ + if (Time.class.equals(getType())) { + newValue = new Time(millis); + } else if (Date.class.equals(getType())) { + newValue = new Date(millis); + } else if (Timestamp.class.equals(getType())) { + newValue = new Timestamp(millis); + } + } + } + + if (!getType().isAssignableFrom(newValue.getClass())) { + throw new IllegalArgumentException( + "Illegal value type for ColumnProperty"); + } + + /* + * If the value to be set is the same that has already been set, do + * not set it again. + */ + if (isValueAlreadySet(newValue)) { + return; + } + } + + /* Set the new value and notify container of the change. */ + changedValue = newValue; + modified = true; + owner.getContainer().itemChangeNotification(owner); + } + + private boolean isValueAlreadySet(Object newValue) { + Object referenceValue = isModified() ? changedValue : value; + + return (isNullable() && newValue == null && referenceValue == null) + || newValue.equals(referenceValue); + } + + @Override + public Class<?> getType() { + return type; + } + + @Override + public boolean isReadOnly() { + return readOnly; + } + + /** + * Returns whether the read-only status of this property can be changed + * using {@link #setReadOnly(boolean)}. + * <p> + * Used to prevent setting to read/write mode a property that is not allowed + * to be written by the underlying database. Also used for values like + * VERSION and AUTO_INCREMENT fields that might be set to read-only by the + * container but the database still allows writes. + * + * @return true if the read-only status can be changed, false otherwise. + */ + public boolean isReadOnlyChangeAllowed() { + return allowReadOnlyChange; + } + + @Override + public void setReadOnly(boolean newStatus) { + if (allowReadOnlyChange) { + readOnly = newStatus; + } + } + + public boolean isPrimaryKey() { + return primaryKey; + } + + public String getPropertyId() { + return propertyId; + } + + private static Logger getLogger() { + return Logger.getLogger(ColumnProperty.class.getName()); + } + + public void setOwner(RowItem owner) { + if (owner == null) { + throw new IllegalArgumentException("Owner can not be set to null."); + } + if (this.owner != null) { + throw new IllegalStateException( + "ColumnProperties can only be bound once."); + } + this.owner = owner; + } + + public boolean isModified() { + return modified; + } + + public boolean isVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(boolean versionColumn) { + this.versionColumn = versionColumn; + } + + public boolean isNullable() { + return nullable; + } + + /** + * Return whether the value of this property should be persisted to the + * database. + * + * @return true if the value should be written to the database, false + * otherwise. + */ + public boolean isPersistent() { + if (isVersionColumn()) { + return false; + } else if (isReadOnlyChangeAllowed() && !isReadOnly()) { + return true; + } else { + return false; + } + } + + /** + * Returns whether or not this property is used as a row identifier. + * + * @return true if the property is a row identifier, false otherwise. + */ + public boolean isRowIdentifier() { + return isPrimaryKey() || isVersionColumn(); + } + + /** + * An exception that signals that a <code>null</code> value was passed to + * the <code>setValue</code> method, but the value of this property can not + * be set to <code>null</code>. + */ + @SuppressWarnings("serial") + public class NotNullableException extends RuntimeException { + + /** + * Constructs a new <code>NotNullableException</code> without a detail + * message. + */ + public NotNullableException() { + } + + /** + * Constructs a new <code>NotNullableException</code> with the specified + * detail message. + * + * @param msg + * the detail message + */ + public NotNullableException(String msg) { + super(msg); + } + + /** + * Constructs a new <code>NotNullableException</code> from another + * exception. + * + * @param cause + * The cause of the failure + */ + public NotNullableException(Throwable cause) { + super(cause); + } + } + + public void commit() { + if (isModified()) { + modified = false; + value = changedValue; + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java new file mode 100644 index 0000000000..1a29d531bc --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import com.vaadin.data.util.sqlcontainer.query.TableQuery; + +/** + * An OptimisticLockException is thrown when trying to update or delete a row + * that has been changed since last read from the database. + * + * OptimisticLockException is a runtime exception because optimistic locking is + * turned off by default, and as such will never be thrown in a default + * configuration. In order to turn on optimistic locking, you need to specify + * the version column in your TableQuery instance. + * + * @see TableQuery#setVersionColumn(String) + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public class OptimisticLockException extends RuntimeException { + + private final RowId rowId; + + public OptimisticLockException(RowId rowId) { + super(); + this.rowId = rowId; + } + + public OptimisticLockException(String msg, RowId rowId) { + super(msg); + this.rowId = rowId; + } + + public RowId getRowId() { + return rowId; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java new file mode 100644 index 0000000000..367135782f --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +public class ReadOnlyRowId extends RowId { + private static final long serialVersionUID = -2626764781642012467L; + private final Integer rowNum; + + public ReadOnlyRowId(int rowNum) { + super(); + this.rowNum = rowNum; + } + + @Override + public int hashCode() { + return getRowNum(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(ReadOnlyRowId.class.equals(obj.getClass()))) { + return false; + } + return getRowNum() == (((ReadOnlyRowId) obj).getRowNum()); + } + + public int getRowNum() { + return rowNum; + } + + @Override + public String toString() { + return String.valueOf(getRowNum()); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/Reference.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/Reference.java new file mode 100644 index 0000000000..ce6680089d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/Reference.java @@ -0,0 +1,68 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.io.Serializable; + +/** + * The reference class represents a simple [usually foreign key] reference to + * another SQLContainer. Actual foreign key reference in the database is not + * required, but it is recommended to make sure that certain constraints are + * followed. + */ +@SuppressWarnings("serial") +class Reference implements Serializable { + + /** + * The SQLContainer that this reference points to. + */ + private SQLContainer referencedContainer; + + /** + * The column ID/name in the referencing SQLContainer that contains the key + * used for the reference. + */ + private String referencingColumn; + + /** + * The column ID/name in the referenced SQLContainer that contains the key + * used for the reference. + */ + private String referencedColumn; + + /** + * Constructs a new reference to be used within the SQLContainer to + * reference another SQLContainer. + */ + Reference(SQLContainer referencedContainer, String referencingColumn, + String referencedColumn) { + this.referencedContainer = referencedContainer; + this.referencingColumn = referencingColumn; + this.referencedColumn = referencedColumn; + } + + SQLContainer getReferencedContainer() { + return referencedContainer; + } + + String getReferencingColumn() { + return referencingColumn; + } + + String getReferencedColumn() { + return referencedColumn; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowId.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowId.java new file mode 100644 index 0000000000..5f78df79e1 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowId.java @@ -0,0 +1,78 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * RowId represents identifiers of a single database result set row. + * + * The data structure of a RowId is an Object array which contains the values of + * the primary key columns of the identified row. This allows easy equals() + * -comparison of RowItems. + */ +public class RowId implements Serializable { + private static final long serialVersionUID = -3161778404698901258L; + protected Object[] id; + + /** + * Prevent instantiation without required parameters. + */ + protected RowId() { + } + + public RowId(Object... id) { + if (id == null) { + throw new IllegalArgumentException( + "id parameter must not be null!"); + } + this.id = id; + } + + public Object[] getId() { + return id; + } + + @Override + public int hashCode() { + return Arrays.hashCode(getId()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(RowId.class.equals(obj.getClass()))) { + return false; + } + return Arrays.equals(getId(), ((RowId) obj).getId()); + } + + @Override + public String toString() { + if (getId() == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (Object id : getId()) { + builder.append(id); + builder.append('/'); + } + if (builder.length() > 0) { + return builder.substring(0, builder.length() - 1); + } + return builder.toString(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowItem.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowItem.java new file mode 100644 index 0000000000..ffc8bfc6f7 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/RowItem.java @@ -0,0 +1,145 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * RowItem represents one row of a result set obtained from a QueryDelegate. + * + * Note that depending on the QueryDelegate in use this does not necessarily map + * into an actual row in a database table. + */ +public final class RowItem implements Item { + private static final long serialVersionUID = -6228966439127951408L; + private SQLContainer container; + private RowId id; + private Collection<ColumnProperty> properties; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private RowItem() { + } + + public RowItem(SQLContainer container, RowId id, + Collection<ColumnProperty> properties) { + if (container == null) { + throw new IllegalArgumentException("Container cannot be null."); + } + if (id == null) { + throw new IllegalArgumentException("Row ID cannot be null."); + } + this.container = container; + this.properties = properties; + /* Set this RowItem as owner to the properties */ + if (properties != null) { + for (ColumnProperty p : properties) { + p.setOwner(this); + } + } + this.id = id; + } + + @Override + public Property getItemProperty(Object id) { + if (id instanceof String && id != null) { + for (ColumnProperty cp : properties) { + if (id.equals(cp.getPropertyId())) { + return cp; + } + } + } + return null; + } + + @Override + public Collection<?> getItemPropertyIds() { + Collection<String> ids = new ArrayList<String>(properties.size()); + for (ColumnProperty cp : properties) { + ids.add(cp.getPropertyId()); + } + return Collections.unmodifiableCollection(ids); + } + + /** + * Adding properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Removing properties is not supported. Properties are generated by + * SQLContainer. + */ + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + public RowId getId() { + return id; + } + + public SQLContainer getContainer() { + return container; + } + + public boolean isModified() { + if (properties != null) { + for (ColumnProperty p : properties) { + if (p.isModified()) { + return true; + } + } + } + return false; + } + + @Override + public String toString() { + StringBuffer s = new StringBuffer(); + s.append("ID:"); + s.append(getId().toString()); + for (Object propId : getItemPropertyIds()) { + s.append("|"); + s.append(propId.toString()); + s.append(":"); + Object value = getItemProperty(propId).getValue(); + s.append((null != value) ? value.toString() : null); + } + return s.toString(); + } + + public void commit() { + if (properties != null) { + for (ColumnProperty p : properties) { + p.commit(); + } + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLContainer.java new file mode 100644 index 0000000000..dde57d3610 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLContainer.java @@ -0,0 +1,1875 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.EventObject; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.ContainerHelpers; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.UnsupportedFilterException; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; + +public class SQLContainer implements Container, Container.Filterable, + Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier { + + /** Query delegate */ + private QueryDelegate queryDelegate; + /** Auto commit mode, default = false */ + private boolean autoCommit = false; + + /** Page length = number of items contained in one page */ + private int pageLength = DEFAULT_PAGE_LENGTH; + public static final int DEFAULT_PAGE_LENGTH = 100; + + /** Number of items to cache = CACHE_RATIO x pageLength */ + public static final int CACHE_RATIO = 2; + + /** Amount of cache to overlap with previous page */ + private int cacheOverlap = pageLength; + + /** Item and index caches */ + private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>(); + private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>(); + + /** Container properties = column names, data types and statuses */ + private final List<String> propertyIds = new ArrayList<String>(); + private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>(); + private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>(); + private final Map<String, Boolean> propertyPersistable = new HashMap<String, Boolean>(); + private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>(); + private final Map<String, Boolean> propertyPrimaryKey = new HashMap<String, Boolean>(); + + /** Filters (WHERE) and sorters (ORDER BY) */ + private final List<Filter> filters = new ArrayList<Filter>(); + private final List<OrderBy> sorters = new ArrayList<OrderBy>(); + + /** + * Total number of items available in the data source using the current + * query, filters and sorters. + */ + private int size; + + /** + * Size updating logic. Do not update size from data source if it has been + * updated in the last sizeValidMilliSeconds milliseconds. + */ + private final int sizeValidMilliSeconds = 10000; + private boolean sizeDirty = true; + private Date sizeUpdated = new Date(); + + /** Starting row number of the currently fetched page */ + private int currentOffset; + + /** ItemSetChangeListeners */ + private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners; + + /** + * Temporary storage for modified items and items to be removed and added + */ + private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>(); + private final List<RowItem> addedItems = new ArrayList<RowItem>(); + private final List<RowItem> modifiedItems = new ArrayList<RowItem>(); + + /** List of references to other SQLContainers */ + private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>(); + + /** Cache flush notification system enabled. Disabled by default. */ + private boolean notificationsEnabled; + + /** + * Prevent instantiation without a QueryDelegate. + */ + @SuppressWarnings("unused") + private SQLContainer() { + } + + /** + * Creates and initializes SQLContainer using the given QueryDelegate + * + * @param delegate + * QueryDelegate implementation + * @throws SQLException + */ + public SQLContainer(QueryDelegate delegate) throws SQLException { + if (delegate == null) { + throw new IllegalArgumentException( + "QueryDelegate must not be null."); + } + queryDelegate = delegate; + getPropertyIds(); + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength() + cacheOverlap); + } + + /**************************************/ + /** Methods from interface Container **/ + /**************************************/ + + /** + * Note! If auto commit mode is enabled, this method will still return the + * temporary row ID assigned for the item. Implement + * QueryDelegate.RowIdChangeListener to receive the actual Row ID value + * after the addition has been committed. + * + * {@inheritDoc} + */ + + @Override + public Object addItem() throws UnsupportedOperationException { + Object emptyKey[] = new Object[queryDelegate.getPrimaryKeyColumns() + .size()]; + RowId itemId = new TemporaryRowId(emptyKey); + // Create new empty column properties for the row item. + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + for (String propertyId : propertyIds) { + /* Default settings for new item properties. */ + ColumnProperty cp = new ColumnProperty(propertyId, + propertyReadOnly.get(propertyId), + propertyPersistable.get(propertyId), + propertyNullable.get(propertyId), + propertyPrimaryKey.get(propertyId), null, + getType(propertyId)); + + itemProperties.add(cp); + } + RowItem newRowItem = new RowItem(this, itemId, itemProperties); + + if (autoCommit) { + /* Add and commit instantly */ + try { + if (queryDelegate instanceof TableQuery) { + itemId = ((TableQuery) queryDelegate) + .storeRowImmediately(newRowItem); + } else { + queryDelegate.beginTransaction(); + queryDelegate.storeRow(newRowItem); + queryDelegate.commit(); + } + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row added to DB..."); + return itemId; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to add row to DB. Rolling back.", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + getLogger().log(Level.SEVERE, + "Failed to roll back row addition", e); + } + return null; + } + } else { + addedItems.add(newRowItem); + fireContentsChange(); + return itemId; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#containsId(java.lang.Object) + */ + + @Override + public boolean containsId(Object itemId) { + if (itemId == null) { + return false; + } + + if (cachedItems.containsKey(itemId)) { + return true; + } else { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return itemPassesFilters(item); + } + } + } + if (removedItems.containsKey(itemId)) { + return false; + } + + if (itemId instanceof ReadOnlyRowId) { + int rowNum = ((ReadOnlyRowId) itemId).getRowNum(); + return rowNum >= 0 && rowNum < size; + } + + if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) { + try { + return queryDelegate + .containsRowWithKey(((RowId) itemId).getId()); + } catch (Exception e) { + /* Query failed, just return false. */ + getLogger().log(Level.WARNING, "containsId query failed", e); + } + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + Item item = getItem(itemId); + if (item == null) { + return null; + } + return item.getItemProperty(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + + @Override + public Collection<?> getContainerPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItem(java.lang.Object) + */ + + @Override + public Item getItem(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + int index = indexOfId(itemId); + if (index >= size) { + // The index is in the added items + int offset = index - size; + RowItem item = addedItems.get(offset); + if (itemPassesFilters(item)) { + return item; + } else { + return null; + } + } else { + // load the item into cache + updateOffsetAndCache(index); + } + } + return cachedItems.get(itemId); + } + + /** + * Bypasses in-memory filtering to return items that are cached in memory. + * <em>NOTE</em>: This does not bypass database-level filtering. + * + * @param itemId + * the id of the item to retrieve. + * @return the item represented by itemId. + */ + public Item getItemUnfiltered(Object itemId) { + if (!cachedItems.containsKey(itemId)) { + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + return item; + } + } + } + return cachedItems.get(itemId); + } + + /** + * NOTE! Do not use this method if in any way avoidable. This method doesn't + * (and cannot) use lazy loading, which means that all rows in the database + * will be loaded into memory. + * + * {@inheritDoc} + */ + + @Override + public Collection<?> getItemIds() { + updateCount(); + ArrayList<RowId> ids = new ArrayList<RowId>(); + ResultSet rs = null; + try { + // Load ALL rows :( + queryDelegate.beginTransaction(); + rs = queryDelegate.getResults(0, 0); + List<String> pKeys = queryDelegate.getPrimaryKeyColumns(); + while (rs.next()) { + RowId id = null; + if (pKeys.isEmpty()) { + /* Create a read only itemId */ + id = new ReadOnlyRowId(rs.getRow()); + } else { + /* Generate itemId for the row based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + id = new RowId(itemId); + } + if (id != null && !removedItems.containsKey(id)) { + ids.add(id); + } + } + rs.getStatement().close(); + rs.close(); + queryDelegate.commit(); + } catch (SQLException e) { + getLogger().log(Level.WARNING, "getItemIds() failed, rolling back.", + e); + try { + queryDelegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back state", e1); + } + try { + rs.getStatement().close(); + rs.close(); + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Closing session failed", e1); + } + throw new RuntimeException("Failed to fetch item indexes.", e); + } + for (RowItem item : getFilteredAddedItems()) { + ids.add(item.getId()); + } + return Collections.unmodifiableCollection(ids); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + + @Override + public Class<?> getType(Object propertyId) { + if (!propertyIds.contains(propertyId)) { + return null; + } + return propertyTypes.get(propertyId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#size() + */ + + @Override + public int size() { + updateCount(); + return size + sizeOfAddedItems() - removedItems.size(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + if (!containsId(itemId)) { + return false; + } + for (RowItem item : addedItems) { + if (item.getId().equals(itemId)) { + addedItems.remove(item); + fireContentsChange(); + return true; + } + } + + if (autoCommit) { + /* Remove and commit instantly. */ + Item i = getItem(itemId); + if (i == null) { + return false; + } + try { + queryDelegate.beginTransaction(); + boolean success = queryDelegate.removeRow((RowItem) i); + queryDelegate.commit(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + if (success) { + getLogger().log(Level.FINER, "Row removed from DB..."); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "Failed to remove row, rolling back", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, + "Failed to rollback row removal", ee); + } + throw e; + } + } else { + removedItems.put((RowId) itemId, (RowItem) getItem(itemId)); + cachedItems.remove(itemId); + refresh(); + return true; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeAllItems() + */ + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + if (autoCommit) { + /* Remove and commit instantly. */ + try { + queryDelegate.beginTransaction(); + boolean success = true; + for (Object id : getItemIds()) { + if (!queryDelegate.removeRow((RowItem) getItem(id))) { + success = false; + } + } + if (success) { + queryDelegate.commit(); + getLogger().log(Level.FINER, "All rows removed from DB..."); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } else { + queryDelegate.rollback(); + } + return success; + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + return false; + } catch (OptimisticLockException e) { + getLogger().log(Level.WARNING, + "removeAllItems() failed, rolling back", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Failed to roll back", ee); + } + throw e; + } + } else { + for (Object id : getItemIds()) { + removedItems.put((RowId) id, (RowItem) getItem(id)); + cachedItems.remove(id); + } + refresh(); + return true; + } + } + + /*************************************************/ + /** Methods from interface Container.Filterable **/ + /*************************************************/ + + /** + * {@inheritDoc} + */ + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + // filter.setCaseSensitive(!ignoreCase); + + filters.add(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + + @Override + public void removeContainerFilter(Filter filter) { + filters.remove(filter); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void addContainerFilter(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + if (propertyId == null || !propertyIds.contains(propertyId)) { + return; + } + + /* Generate Filter -object */ + String likeStr = onlyMatchPrefix ? filterString + "%" + : "%" + filterString + "%"; + Like like = new Like(propertyId.toString(), likeStr); + like.setCaseSensitive(!ignoreCase); + filters.add(like); + refresh(); + } + + /** + * {@inheritDoc} + */ + public void removeContainerFilters(Object propertyId) { + ArrayList<Filter> toRemove = new ArrayList<Filter>(); + for (Filter f : filters) { + if (f.appliesToProperty(propertyId)) { + toRemove.add(f); + } + } + filters.removeAll(toRemove); + refresh(); + } + + /** + * {@inheritDoc} + */ + @Override + public void removeAllContainerFilters() { + filters.clear(); + refresh(); + } + + /** + * Returns true if any filters have been applied to the container. + * + * @return true if the container has filters applied, false otherwise + * @since 7.1 + */ + public boolean hasContainerFilters() { + return !getContainerFilters().isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return Collections.unmodifiableCollection(filters); + } + + /**********************************************/ + /** Methods from interface Container.Indexed **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object) + */ + + @Override + public int indexOfId(Object itemId) { + // First check if the id is in the added items + for (int ix = 0; ix < addedItems.size(); ix++) { + RowItem item = addedItems.get(ix); + if (item.getId().equals(itemId)) { + if (itemPassesFilters(item)) { + updateCount(); + return size + ix; + } else { + return -1; + } + } + } + + if (!containsId(itemId)) { + return -1; + } + if (cachedItems.isEmpty()) { + getPage(); + } + // this protects against infinite looping + int counter = 0; + int oldIndex; + while (counter < size) { + if (itemIndexes.containsValue(itemId)) { + for (Integer idx : itemIndexes.keySet()) { + if (itemIndexes.get(idx).equals(itemId)) { + return idx; + } + } + } + oldIndex = currentOffset; + // load in the next page. + int nextIndex = currentOffset + pageLength * CACHE_RATIO + + cacheOverlap; + if (nextIndex >= size) { + // Container wrapped around, start from index 0. + nextIndex = 0; + } + updateOffsetAndCache(nextIndex); + + // Update counter + if (currentOffset > oldIndex) { + counter += currentOffset - oldIndex; + } else { + counter += size - oldIndex; + } + } + // safeguard in case item not found + return -1; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#getIdByIndex(int) + */ + + @Override + public Object getIdByIndex(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException( + "Index is negative! index=" + index); + } + // make sure the size field is valid + updateCount(); + if (index < size) { + if (itemIndexes.keySet().contains(index)) { + return itemIndexes.get(index); + } + updateOffsetAndCache(index); + return itemIndexes.get(index); + } else { + // The index is in the added items + int offset = index - size; + // TODO this is very inefficient if looping - should improve + // getItemIds(int, int) + return getFilteredAddedItems().get(offset).getId(); + } + } + + @Override + public List<Object> getItemIds(int startIndex, int numberOfIds) { + // TODO create a better implementation + return (List<Object>) ContainerHelpers + .getItemIdsUsingGetIdByIndex(startIndex, numberOfIds, this); + } + + /**********************************************/ + /** Methods from interface Container.Ordered **/ + /**********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ + + @Override + public Object nextItemId(Object itemId) { + int index = indexOfId(itemId) + 1; + try { + return getIdByIndex(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + + @Override + public Object prevItemId(Object itemId) { + int prevIndex = indexOfId(itemId) - 1; + try { + return getIdByIndex(prevIndex); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + + @Override + public Object firstItemId() { + updateCount(); + if (size == 0) { + if (addedItems.isEmpty()) { + return null; + } else { + int ix = -1; + do { + ix++; + } while (!itemPassesFilters(addedItems.get(ix)) + && ix < addedItems.size()); + if (ix < addedItems.size()) { + return addedItems.get(ix).getId(); + } + } + } + if (!itemIndexes.containsKey(0)) { + updateOffsetAndCache(0); + } + return itemIndexes.get(0); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ + + @Override + public Object lastItemId() { + if (addedItems.isEmpty()) { + int lastIx = size() - 1; + if (!itemIndexes.containsKey(lastIx)) { + updateOffsetAndCache(size - 1); + } + return itemIndexes.get(lastIx); + } else { + int ix = addedItems.size(); + do { + ix--; + } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0); + if (ix >= 0) { + return addedItems.get(ix).getId(); + } else { + return null; + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ + + @Override + public boolean isFirstId(Object itemId) { + return firstItemId().equals(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + + @Override + public boolean isLastId(Object itemId) { + return lastItemId().equals(itemId); + } + + /***********************************************/ + /** Methods from interface Container.Sortable **/ + /***********************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + sorters.clear(); + if (propertyId == null || propertyId.length == 0) { + refresh(); + return; + } + /* Generate OrderBy -objects */ + boolean asc = true; + for (int i = 0; i < propertyId.length; i++) { + /* Check that the property id is valid */ + if (propertyId[i] instanceof String + && propertyIds.contains(propertyId[i])) { + try { + asc = ascending[i]; + } catch (Exception e) { + getLogger().log(Level.WARNING, "", e); + } + sorters.add(new OrderBy((String) propertyId[i], asc)); + } + } + refresh(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + + @Override + public Collection<?> getSortableContainerPropertyIds() { + return getContainerPropertyIds(); + } + + /**************************************/ + /** Methods specific to SQLContainer **/ + /**************************************/ + + /** + * Refreshes the container - clears all caches and resets size and offset. + * Does NOT remove sorting or filtering rules! + */ + public void refresh() { + refresh(true); + } + + /** + * Refreshes the container. If <code>setSizeDirty</code> is + * <code>false</code>, assumes that the current size is up to date. This is + * used in {@link #updateCount()} to refresh the contents when we know the + * size was just updated. + * + * @param setSizeDirty + */ + private void refresh(boolean setSizeDirty) { + if (setSizeDirty) { + sizeDirty = true; + } + currentOffset = 0; + cachedItems.clear(); + itemIndexes.clear(); + fireContentsChange(); + } + + /** + * Returns modify state of the container. + * + * @return true if contents of this container have been modified + */ + public boolean isModified() { + return !removedItems.isEmpty() || !addedItems.isEmpty() + || !modifiedItems.isEmpty(); + } + + /** + * Set auto commit mode enabled or disabled. Auto commit mode means that all + * changes made to items of this container will be immediately written to + * the underlying data source. + * + * @param autoCommitEnabled + * true to enable auto commit mode + */ + public void setAutoCommit(boolean autoCommitEnabled) { + autoCommit = autoCommitEnabled; + } + + /** + * Returns status of the auto commit mode. + * + * @return true if auto commit mode is enabled + */ + public boolean isAutoCommit() { + return autoCommit; + } + + /** + * Returns the currently set page length. + * + * @return current page length + */ + public int getPageLength() { + return pageLength; + } + + /** + * Sets the page length used in lazy fetching of items from the data source. + * Also resets the cache size to match the new page length. + * + * As a side effect the container will be refreshed. + * + * @param pageLength + * new page length + */ + public void setPageLength(int pageLength) { + setPageLengthInternal(pageLength); + refresh(); + } + + /** + * Sets the page length internally, without refreshing the container. + * + * @param pageLength + * the new page length + */ + private void setPageLengthInternal(int pageLength) { + this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH; + cacheOverlap = getPageLength(); + cachedItems.setCacheLimit(CACHE_RATIO * getPageLength() + cacheOverlap); + } + + /** + * Adds the given OrderBy to this container and refreshes the container + * contents with the new sorting rules. + * + * Note that orderBy.getColumn() must return a column name that exists in + * this container. + * + * @param orderBy + * OrderBy to be added to the container sorting rules + */ + public void addOrderBy(OrderBy orderBy) { + if (orderBy == null) { + return; + } + if (!propertyIds.contains(orderBy.getColumn())) { + throw new IllegalArgumentException( + "The column given for sorting does not exist in this container."); + } + sorters.add(orderBy); + refresh(); + } + + /** + * Commits all the changes, additions and removals made to the items of this + * container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void commit() throws UnsupportedOperationException, SQLException { + try { + getLogger().log(Level.FINER, + "Commiting changes through delegate..."); + queryDelegate.beginTransaction(); + /* Perform buffered deletions */ + for (RowItem item : removedItems.values()) { + try { + if (!queryDelegate.removeRow(item)) { + throw new SQLException( + "Removal failed for row with ID: " + + item.getId()); + } + } catch (IllegalArgumentException e) { + throw new SQLException( + "Removal failed for row with ID: " + item.getId(), + e); + } + } + /* Perform buffered modifications */ + for (RowItem item : modifiedItems) { + if (!removedItems.containsKey(item.getId())) { + if (queryDelegate.storeRow(item) > 0) { + /* + * Also reset the modified state in the item in case it + * is reused e.g. in a form. + */ + item.commit(); + } else { + queryDelegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + item.getId() + + "' has been externally modified."); + } + } + } + /* Perform buffered additions */ + for (RowItem item : addedItems) { + queryDelegate.storeRow(item); + } + queryDelegate.commit(); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + } catch (SQLException e) { + queryDelegate.rollback(); + throw e; + } catch (OptimisticLockException e) { + queryDelegate.rollback(); + throw e; + } + } + + /** + * Rolls back all the changes, additions and removals made to the items of + * this container. + * + * @throws UnsupportedOperationException + * @throws SQLException + */ + public void rollback() throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "Rolling back changes..."); + removedItems.clear(); + addedItems.clear(); + modifiedItems.clear(); + refresh(); + } + + /** + * Notifies this container that a property in the given item has been + * modified. The change will be buffered or made instantaneously depending + * on auto commit mode. + * + * @param changedItem + * item that has a modified property + */ + void itemChangeNotification(RowItem changedItem) { + if (autoCommit) { + try { + queryDelegate.beginTransaction(); + if (queryDelegate.storeRow(changedItem) == 0) { + queryDelegate.rollback(); + refresh(); + throw new ConcurrentModificationException( + "Item with the ID '" + changedItem.getId() + + "' has been externally modified."); + } + queryDelegate.commit(); + if (notificationsEnabled) { + CacheFlushNotifier.notifyOfCacheFlush(this); + } + getLogger().log(Level.FINER, "Row updated to DB..."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "itemChangeNotification failed, rolling back...", e); + try { + queryDelegate.rollback(); + } catch (SQLException ee) { + /* Nothing can be done here */ + getLogger().log(Level.SEVERE, "Rollback failed", e); + } + throw new RuntimeException(e); + } + } else { + if (!(changedItem.getId() instanceof TemporaryRowId) + && !modifiedItems.contains(changedItem)) { + modifiedItems.add(changedItem); + } + } + } + + /** + * Determines a new offset for updating the row cache. The offset is + * calculated from the given index, and will be fixed to match the start of + * a page, based on the value of pageLength. + * + * @param index + * Index of the item that was requested, but not found in cache + */ + private void updateOffsetAndCache(int index) { + + int oldOffset = currentOffset; + + currentOffset = (index / pageLength) * pageLength - cacheOverlap; + + if (currentOffset < 0) { + currentOffset = 0; + } + + if (oldOffset == currentOffset && !cachedItems.isEmpty()) { + return; + } + + getPage(); + } + + /** + * Fetches new count of rows from the data source, if needed. + */ + private void updateCount() { + if (!sizeDirty && new Date().getTime() < sizeUpdated.getTime() + + sizeValidMilliSeconds) { + return; + } + try { + try { + queryDelegate.setFilters(filters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support filtering", e); + } + try { + queryDelegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + getLogger().log(Level.FINE, + "The query delegate doesn't support sorting", e); + } + int newSize = queryDelegate.getCount(); + sizeUpdated = new Date(); + sizeDirty = false; + if (newSize != size) { + size = newSize; + // Size is up to date so don't set it back to dirty in refresh() + refresh(false); + } + getLogger().log(Level.FINER, "Updated row count. New count is: {0}", + size); + } catch (SQLException e) { + throw new RuntimeException("Failed to update item set size.", e); + } + } + + /** + * Fetches property id's (column names and their types) from the data + * source. + * + * @throws SQLException + */ + private void getPropertyIds() throws SQLException { + propertyIds.clear(); + propertyTypes.clear(); + queryDelegate.setFilters(null); + queryDelegate.setOrderBy(null); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + try { + queryDelegate.beginTransaction(); + rs = queryDelegate.getResults(0, 1); + rsmd = rs.getMetaData(); + boolean resultExists = rs.next(); + Class<?> type = null; + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + /* + * Make sure not to add the same colName twice. This can easily + * happen if the SQL query joins many tables with an ID column. + */ + if (!propertyIds.contains(colName)) { + propertyIds.add(colName); + } + /* Try to determine the column's JDBC class by all means. */ + if (resultExists && rs.getObject(i) != null) { + type = rs.getObject(i).getClass(); + } else { + try { + type = Class.forName(rsmd.getColumnClassName(i)); + } catch (Exception e) { + getLogger().log(Level.WARNING, "Class not found", e); + /* On failure revert to Object and hope for the best. */ + type = Object.class; + } + } + /* + * Determine read only and nullability status of the column. A + * column is read only if it is reported as either read only or + * auto increment by the database, and also it is set as the + * version column in a TableQuery delegate. + */ + boolean readOnly = rsmd.isAutoIncrement(i) + || rsmd.isReadOnly(i); + + boolean persistable = !rsmd.isReadOnly(i); + + if (queryDelegate instanceof TableQuery) { + if (rsmd.getColumnLabel(i).equals( + ((TableQuery) queryDelegate).getVersionColumn())) { + readOnly = true; + } + } + + propertyReadOnly.put(colName, readOnly); + propertyPersistable.put(colName, persistable); + propertyNullable.put(colName, + rsmd.isNullable(i) == ResultSetMetaData.columnNullable); + propertyPrimaryKey.put(colName, + queryDelegate.getPrimaryKeyColumns() + .contains(rsmd.getColumnLabel(i))); + propertyTypes.put(colName, type); + } + rs.getStatement().close(); + rs.close(); + queryDelegate.commit(); + getLogger().log(Level.FINER, "Property IDs fetched."); + } catch (SQLException e) { + getLogger().log(Level.WARNING, + "Failed to fetch property ids, rolling back", e); + try { + queryDelegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + } + rs.close(); + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw e; + } + } + + /** + * Fetches a page from the data source based on the values of pageLength and + * currentOffset. Also updates the set of primary keys, used in + * identification of RowItems. + */ + private void getPage() { + updateCount(); + ResultSet rs = null; + ResultSetMetaData rsmd = null; + cachedItems.clear(); + itemIndexes.clear(); + try { + try { + queryDelegate.setOrderBy(sorters); + } catch (UnsupportedOperationException e) { + /* The query delegate doesn't support sorting. */ + /* No need to do anything. */ + getLogger().log(Level.FINE, + "The query delegate doesn't support sorting", e); + } + queryDelegate.beginTransaction(); + int fetchedRows = pageLength * CACHE_RATIO + cacheOverlap; + rs = queryDelegate.getResults(currentOffset, fetchedRows); + rsmd = rs.getMetaData(); + List<String> pKeys = queryDelegate.getPrimaryKeyColumns(); + // } + /* Create new items and column properties */ + ColumnProperty cp = null; + int rowCount = currentOffset; + if (!queryDelegate.implementationRespectsPagingLimits()) { + rowCount = currentOffset = 0; + setPageLengthInternal(size); + } + while (rs.next()) { + List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>(); + /* Generate row itemId based on primary key(s) */ + Object[] itemId = new Object[pKeys.size()]; + for (int i = 0; i < pKeys.size(); i++) { + itemId[i] = rs.getObject(pKeys.get(i)); + } + RowId id = null; + if (pKeys.isEmpty()) { + id = new ReadOnlyRowId(rs.getRow()); + } else { + id = new RowId(itemId); + } + List<String> propertiesToAdd = new ArrayList<String>( + propertyIds); + if (!removedItems.containsKey(id)) { + for (int i = 1; i <= rsmd.getColumnCount(); i++) { + if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) { + continue; + } + String colName = rsmd.getColumnLabel(i); + Object value = rs.getObject(i); + Class<?> type = value != null ? value.getClass() + : Object.class; + if (value == null) { + for (String propName : propertyTypes.keySet()) { + if (propName.equals(rsmd.getColumnLabel(i))) { + type = propertyTypes.get(propName); + break; + } + } + } + /* + * In case there are more than one column with the same + * name, add only the first one. This can easily happen + * if you join many tables where each table has an ID + * column. + */ + if (propertiesToAdd.contains(colName)) { + + cp = new ColumnProperty(colName, + propertyReadOnly.get(colName), + propertyPersistable.get(colName), + propertyNullable.get(colName), + propertyPrimaryKey.get(colName), value, + type); + itemProperties.add(cp); + propertiesToAdd.remove(colName); + } + } + /* Cache item */ + itemIndexes.put(rowCount, id); + + // if an item with the id is contained in the modified + // cache, then use this record and add it to the cached + // items. Otherwise create a new item + int modifiedIndex = indexInModifiedCache(id); + if (modifiedIndex != -1) { + cachedItems.put(id, modifiedItems.get(modifiedIndex)); + } else { + cachedItems.put(id, + new RowItem(this, id, itemProperties)); + } + + rowCount++; + } + } + rs.getStatement().close(); + rs.close(); + queryDelegate.commit(); + getLogger().log(Level.FINER, "Fetched {0} rows starting from {1}", + new Object[] { fetchedRows, currentOffset }); + } catch (SQLException e) { + getLogger().log(Level.WARNING, "Failed to fetch rows, rolling back", + e); + try { + queryDelegate.rollback(); + } catch (SQLException e1) { + getLogger().log(Level.SEVERE, "Failed to roll back", e1); + } + try { + if (rs != null) { + if (rs.getStatement() != null) { + rs.getStatement().close(); + rs.close(); + } + } + } catch (SQLException e1) { + getLogger().log(Level.WARNING, "Failed to close session", e1); + } + throw new RuntimeException("Failed to fetch page.", e); + } + } + + /** + * Returns the index of the item with the given itemId for the modified + * cache. + * + * @param itemId + * @return the index of the item with the itemId in the modified cache. Or + * -1 if not found. + */ + private int indexInModifiedCache(Object itemId) { + for (int ix = 0; ix < modifiedItems.size(); ix++) { + RowItem item = modifiedItems.get(ix); + if (item.getId().equals(itemId)) { + return ix; + } + } + return -1; + } + + private int sizeOfAddedItems() { + return getFilteredAddedItems().size(); + } + + private List<RowItem> getFilteredAddedItems() { + ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems); + if (filters != null && !filters.isEmpty()) { + for (RowItem item : addedItems) { + if (!itemPassesFilters(item)) { + filtered.remove(item); + } + } + } + return filtered; + } + + private boolean itemPassesFilters(RowItem item) { + for (Filter filter : filters) { + if (!filter.passesFilter(item.getId(), item)) { + return false; + } + } + return true; + } + + /** + * Checks is the given column identifier valid to be used with SQLContainer. + * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle + * is used. This is due to the way the SELECT queries are constructed in + * order to implement paging in these databases. + * + * @param identifier + * Column identifier + * @return true if the identifier is valid + */ + private boolean isColumnIdentifierValid(String identifier) { + if (identifier.equalsIgnoreCase("rownum") + && queryDelegate instanceof TableQuery) { + TableQuery tq = (TableQuery) queryDelegate; + if (tq.getSqlGenerator() instanceof MSSQLGenerator + || tq.getSqlGenerator() instanceof OracleGenerator) { + return false; + } + } + return true; + } + + /** + * Returns the QueryDelegate set for this SQLContainer. + * + * @return current querydelegate + */ + protected QueryDelegate getQueryDelegate() { + return queryDelegate; + } + + /************************************/ + /** UNSUPPORTED CONTAINER FEATURES **/ + /************************************/ + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object) + */ + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object) + */ + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Indexed#addItemAt(int) + */ + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /******************************************/ + /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/ + /******************************************/ + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void addItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners == null) { + itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>(); + } + itemSetChangeListeners.add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Container.ItemSetChangeListener listener) { + addItemSetChangeListener(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin + * .data.Container.ItemSetChangeListener) + */ + + @Override + public void removeItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (itemSetChangeListeners != null) { + itemSetChangeListeners.remove(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Container.ItemSetChangeListener listener) { + removeItemSetChangeListener(listener); + } + + protected void fireContentsChange() { + if (itemSetChangeListeners != null) { + final Object[] l = itemSetChangeListeners.toArray(); + final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent( + this); + for (int i = 0; i < l.length; i++) { + ((Container.ItemSetChangeListener) l[i]) + .containerItemSetChange(event); + } + } + } + + /** + * Simple ItemSetChangeEvent implementation. + */ + @SuppressWarnings("serial") + public static class ItemSetChangeEvent extends EventObject + implements Container.ItemSetChangeEvent { + + private ItemSetChangeEvent(SQLContainer source) { + super(source); + } + + @Override + public Container getContainer() { + return (Container) getSource(); + } + } + + /**************************************************/ + /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/ + /**************************************************/ + + /** + * Adds a RowIdChangeListener to the QueryDelegate + * + * @param listener + */ + public void addRowIdChangeListener(RowIdChangeListener listener) { + if (queryDelegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) queryDelegate) + .addListener(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addRowIdChangeListener(RowIdChangeListener)} + **/ + @Deprecated + public void addListener(RowIdChangeListener listener) { + addRowIdChangeListener(listener); + } + + /** + * Removes a RowIdChangeListener from the QueryDelegate + * + * @param listener + */ + public void removeRowIdChangeListener(RowIdChangeListener listener) { + if (queryDelegate instanceof QueryDelegate.RowIdChangeNotifier) { + ((QueryDelegate.RowIdChangeNotifier) queryDelegate) + .removeListener(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeRowIdChangeListener(RowIdChangeListener)} + **/ + @Deprecated + public void removeListener(RowIdChangeListener listener) { + removeRowIdChangeListener(listener); + } + + /** + * Calling this will enable this SQLContainer to send and receive cache + * flush notifications for its lifetime. + */ + public void enableCacheFlushNotifications() { + if (!notificationsEnabled) { + notificationsEnabled = true; + CacheFlushNotifier.addInstance(this); + } + } + + /******************************************/ + /** Referencing mechanism implementation **/ + /******************************************/ + + /** + * Adds a new reference to the given SQLContainer. In addition to the + * container you must provide the column (property) names used for the + * reference in both this and the referenced SQLContainer. + * + * Note that multiple references pointing to the same SQLContainer are not + * supported. + * + * @param refdCont + * Target SQLContainer of the new reference + * @param refingCol + * Column (property) name in this container storing the (foreign + * key) reference + * @param refdCol + * Column (property) name in the referenced container storing the + * referenced key + */ + public void addReference(SQLContainer refdCont, String refingCol, + String refdCol) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + if (!getContainerPropertyIds().contains(refingCol)) { + throw new IllegalArgumentException( + "Given referencing column name is invalid." + + " Please ensure that this container" + + " contains a property ID named: " + refingCol); + } + if (!refdCont.getContainerPropertyIds().contains(refdCol)) { + throw new IllegalArgumentException( + "Given referenced column name is invalid." + + " Please ensure that the referenced container" + + " contains a property ID named: " + refdCol); + } + if (references.keySet().contains(refdCont)) { + throw new IllegalArgumentException( + "An SQLContainer instance can only be referenced once."); + } + references.put(refdCont, new Reference(refdCont, refingCol, refdCol)); + } + + /** + * Removes the reference pointing to the given SQLContainer. + * + * @param refdCont + * Target SQLContainer of the reference + * @return true if successful, false if the reference did not exist + */ + public boolean removeReference(SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + return references.remove(refdCont) == null ? false : true; + } + + /** + * Sets the referenced item. The referencing column of the item in this + * container is updated accordingly. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdItemId + * Item Id of the reference target (from referenced container) + * @param refdCont + * Target SQLContainer of the reference + * @return true if the referenced item was successfully set, false on + * failure + */ + public boolean setReferencedItem(Object itemId, Object refdItemId, + SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + try { + getContainerProperty(itemId, r.getReferencingColumn()) + .setValue(refdCont.getContainerProperty(refdItemId, + r.getReferencedColumn())); + return true; + } catch (Exception e) { + getLogger().log(Level.WARNING, "Setting referenced item failed.", + e); + return false; + } + } + + /** + * Fetches the Item Id of the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return Item Id of the referenced item, or null if not found + */ + public Object getReferencedItemId(Object itemId, SQLContainer refdCont) { + if (refdCont == null) { + throw new IllegalArgumentException( + "Referenced SQLContainer can not be null."); + } + Reference r = references.get(refdCont); + if (r == null) { + throw new IllegalArgumentException( + "Reference to the given SQLContainer not defined."); + } + Object refKey = getContainerProperty(itemId, r.getReferencingColumn()) + .getValue(); + + refdCont.removeAllContainerFilters(); + refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey)); + Object toReturn = refdCont.firstItemId(); + refdCont.removeAllContainerFilters(); + return toReturn; + } + + /** + * Fetches the referenced item from the target SQLContainer. + * + * @param itemId + * Item Id of the reference source (from this container) + * @param refdCont + * Target SQLContainer of the reference + * @return The referenced item, or null if not found + */ + public Item getReferencedItem(Object itemId, SQLContainer refdCont) { + return refdCont.getItem(getReferencedItemId(itemId, refdCont)); + } + + private void writeObject(java.io.ObjectOutputStream out) + throws IOException { + out.defaultWriteObject(); + } + + private void readObject(java.io.ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if (notificationsEnabled) { + /* + * Register instance with CacheFlushNotifier after de-serialization + * if notifications are enabled + */ + CacheFlushNotifier.addInstance(this); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(SQLContainer.class.getName()); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLUtil.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLUtil.java new file mode 100644 index 0000000000..2abe45d5fc --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/SQLUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +import java.io.Serializable; + +public class SQLUtil implements Serializable { + /** + * Escapes different special characters in strings that are passed to SQL. + * Replaces the following: + * + * <list> + * <li>' is replaced with ''</li> + * <li>\x00 is removed</li> + * <li>\ is replaced with \\</li> + * <li>" is replaced with \"</li> + * <li>\x1a is removed</li> </list> + * + * Also note! The escaping done here may or may not be enough to prevent any + * and all SQL injections so it is recommended to check user input before + * giving it to the SQLContainer/TableQuery. + * + * @param constant + * @return \\\'\' + */ + public static String escapeSQL(String constant) { + if (constant == null) { + return null; + } + String fixedConstant = constant; + fixedConstant = fixedConstant.replaceAll("\\\\x00", ""); + fixedConstant = fixedConstant.replaceAll("\\\\x1a", ""); + fixedConstant = fixedConstant.replaceAll("'", "''"); + fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\"); + fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\""); + return fixedConstant; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java new file mode 100644 index 0000000000..5cd3e6fac8 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer; + +public class TemporaryRowId extends RowId { + private static final long serialVersionUID = -641983830469018329L; + + public TemporaryRowId(Object... id) { + super(id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(TemporaryRowId.class.equals(obj.getClass()))) { + return false; + } + Object[] compId = ((TemporaryRowId) obj).getId(); + return id.equals(compId); + } + + @Override + public String toString() { + return "Temporary row id"; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java new file mode 100644 index 0000000000..cf50a641de --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.connection; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +public class J2EEConnectionPool implements JDBCConnectionPool { + + private String dataSourceJndiName; + + private DataSource dataSource = null; + + public J2EEConnectionPool(DataSource dataSource) { + this.dataSource = dataSource; + } + + public J2EEConnectionPool(String dataSourceJndiName) { + this.dataSourceJndiName = dataSourceJndiName; + } + + @Override + public Connection reserveConnection() throws SQLException { + Connection conn = getDataSource().getConnection(); + conn.setAutoCommit(false); + + return conn; + } + + private DataSource getDataSource() throws SQLException { + if (dataSource == null) { + dataSource = lookupDataSource(); + } + return dataSource; + } + + private DataSource lookupDataSource() throws SQLException { + try { + InitialContext ic = new InitialContext(); + return (DataSource) ic.lookup(dataSourceJndiName); + } catch (NamingException e) { + throw new SQLException( + "NamingException - Cannot connect to the database. Cause: " + + e.getMessage()); + } + } + + @Override + public void releaseConnection(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + Logger.getLogger(J2EEConnectionPool.class.getName()) + .log(Level.FINE, "Could not release SQL connection", e); + } + } + } + + @Override + public void destroy() { + dataSource = null; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java new file mode 100644 index 0000000000..842a264caa --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.connection; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Interface for implementing connection pools to be used with SQLContainer. + */ +public interface JDBCConnectionPool extends Serializable { + /** + * Retrieves a connection. + * + * @return a usable connection to the database + * @throws SQLException + */ + public Connection reserveConnection() throws SQLException; + + /** + * Releases a connection that was retrieved earlier. + * + * Note that depending on implementation, the transaction possibly open in + * the connection may or may not be rolled back. + * + * @param conn + * Connection to be released + */ + public void releaseConnection(Connection conn); + + /** + * Destroys the connection pool: close() is called an all the connections in + * the pool, whether available or reserved. + * + * This method was added to fix PostgreSQL -related issues with connections + * that were left hanging 'idle'. + */ + public void destroy(); +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java new file mode 100644 index 0000000000..9cf02a9e68 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java @@ -0,0 +1,181 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.connection; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; + +/** + * Simple implementation of the JDBCConnectionPool interface. Handles loading + * the JDBC driver, setting up the connections and ensuring they are still + * usable upon release. + */ +@SuppressWarnings("serial") +public class SimpleJDBCConnectionPool implements JDBCConnectionPool { + + private int initialConnections = 5; + private int maxConnections = 20; + + private String driverName; + private String connectionUri; + private String userName; + private String password; + + private transient Set<Connection> availableConnections; + private transient Set<Connection> reservedConnections; + + private boolean initialized; + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password) throws SQLException { + if (driverName == null) { + throw new IllegalArgumentException( + "JDBC driver class name must be given."); + } + if (connectionUri == null) { + throw new IllegalArgumentException( + "Database connection URI must be given."); + } + if (userName == null) { + throw new IllegalArgumentException( + "Database username must be given."); + } + if (password == null) { + throw new IllegalArgumentException( + "Database password must be given."); + } + this.driverName = driverName; + this.connectionUri = connectionUri; + this.userName = userName; + this.password = password; + + /* Initialize JDBC driver */ + try { + Class.forName(driverName).newInstance(); + } catch (Exception ex) { + throw new RuntimeException("Specified JDBC Driver: " + driverName + + " - initialization failed.", ex); + } + } + + public SimpleJDBCConnectionPool(String driverName, String connectionUri, + String userName, String password, int initialConnections, + int maxConnections) throws SQLException { + this(driverName, connectionUri, userName, password); + this.initialConnections = initialConnections; + this.maxConnections = maxConnections; + } + + private void initializeConnections() throws SQLException { + availableConnections = new HashSet<Connection>(initialConnections); + reservedConnections = new HashSet<Connection>(initialConnections); + for (int i = 0; i < initialConnections; i++) { + availableConnections.add(createConnection()); + } + initialized = true; + } + + @Override + public synchronized Connection reserveConnection() throws SQLException { + if (!initialized) { + initializeConnections(); + } + if (availableConnections.isEmpty()) { + if (reservedConnections.size() < maxConnections) { + availableConnections.add(createConnection()); + } else { + throw new SQLException("Connection limit has been reached."); + } + } + + Connection c = availableConnections.iterator().next(); + availableConnections.remove(c); + reservedConnections.add(c); + + return c; + } + + @Override + public synchronized void releaseConnection(Connection conn) { + if (conn == null || !initialized) { + return; + } + /* Try to roll back if necessary */ + try { + if (!conn.getAutoCommit()) { + conn.rollback(); + } + } catch (SQLException e) { + /* Roll back failed, close and discard connection */ + try { + conn.close(); + } catch (SQLException e1) { + /* Nothing needs to be done */ + } + reservedConnections.remove(conn); + return; + } + reservedConnections.remove(conn); + availableConnections.add(conn); + } + + private Connection createConnection() throws SQLException { + Connection c = DriverManager.getConnection(connectionUri, userName, + password); + c.setAutoCommit(false); + if (driverName.toLowerCase().contains("mysql")) { + try { + Statement s = c.createStatement(); + s.execute("SET SESSION sql_mode = 'ANSI'"); + s.close(); + } catch (Exception e) { + // Failed to set ansi mode; continue + } + } + return c; + } + + @Override + public void destroy() { + for (Connection c : availableConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + for (Connection c : reservedConnections) { + try { + c.close(); + } catch (SQLException e) { + // No need to do anything + } + } + + } + + private void writeObject(java.io.ObjectOutputStream out) + throws IOException { + initialized = false; + out.defaultWriteObject(); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/AbstractTransactionalQuery.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/AbstractTransactionalQuery.java new file mode 100644 index 0000000000..586fe28171 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/AbstractTransactionalQuery.java @@ -0,0 +1,185 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; + +/** + * Common base class for database query classes that handle connections and + * transactions. + * + * @author Vaadin Ltd + * @since 6.8.9 + */ +public abstract class AbstractTransactionalQuery implements Serializable { + + private JDBCConnectionPool connectionPool; + private transient Connection activeConnection; + + AbstractTransactionalQuery() { + } + + AbstractTransactionalQuery(JDBCConnectionPool connectionPool) { + this.connectionPool = connectionPool; + } + + /** + * Reserves a connection with auto-commit off if no transaction is in + * progress. + * + * @throws IllegalStateException + * if a transaction is already open + * @throws SQLException + * if a connection could not be obtained or configured + */ + public void beginTransaction() + throws UnsupportedOperationException, SQLException { + if (isInTransaction()) { + throw new IllegalStateException("A transaction is already active!"); + } + activeConnection = connectionPool.reserveConnection(); + activeConnection.setAutoCommit(false); + } + + /** + * Commits (if not in auto-commit mode) and releases the active connection. + * + * @throws SQLException + * if not in a transaction managed by this query + */ + public void commit() throws UnsupportedOperationException, SQLException { + if (!isInTransaction()) { + throw new SQLException("No active transaction"); + } + if (!activeConnection.getAutoCommit()) { + activeConnection.commit(); + } + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /** + * Rolls back and releases the active connection. + * + * @throws SQLException + * if not in a transaction managed by this query + */ + public void rollback() throws UnsupportedOperationException, SQLException { + if (!isInTransaction()) { + throw new SQLException("No active transaction"); + } + activeConnection.rollback(); + connectionPool.releaseConnection(activeConnection); + activeConnection = null; + } + + /** + * Check that a transaction is active. + * + * @throws SQLException + * if no active transaction + */ + protected void ensureTransaction() throws SQLException { + if (!isInTransaction()) { + throw new SQLException("No active transaction!"); + } + } + + /** + * Closes a statement and a resultset, then releases the connection if it is + * not part of an active transaction. A failure in closing one of the + * parameters does not prevent closing the rest. + * + * If the statement is a {@link PreparedStatement}, its parameters are + * cleared prior to closing the statement. + * + * Although JDBC specification does state that closing a statement closes + * its result set and closing a connection closes statements and result + * sets, this method does try to close the result set and statement + * explicitly whenever not null. This can guard against bugs in certain JDBC + * drivers and reduce leaks in case e.g. closing the result set succeeds but + * closing the statement or connection fails. + * + * @param conn + * the connection to release + * @param statement + * the statement to close, may be null to skip closing + * @param rs + * the result set to close, may be null to skip closing + * @throws SQLException + * if closing the result set or the statement fails + */ + protected void releaseConnection(Connection conn, Statement statement, + ResultSet rs) throws SQLException { + try { + try { + if (null != rs) { + rs.close(); + } + } finally { + if (null != statement) { + if (statement instanceof PreparedStatement) { + try { + ((PreparedStatement) statement).clearParameters(); + } catch (Exception e) { + // will be closed below anyway + } + } + statement.close(); + } + } + } finally { + releaseConnection(conn); + } + } + + /** + * Returns the currently active connection, reserves and returns a new + * connection if no active connection. + * + * @return previously active or newly reserved connection + * @throws SQLException + */ + protected Connection getConnection() throws SQLException { + if (activeConnection != null) { + return activeConnection; + } + return connectionPool.reserveConnection(); + } + + protected boolean isInTransaction() { + return activeConnection != null; + } + + /** + * Releases the connection if it is not part of an active transaction. + * + * @param conn + * the connection to release + */ + private void releaseConnection(Connection conn) { + if (conn != activeConnection && conn != null) { + connectionPool.releaseConnection(conn); + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java new file mode 100644 index 0000000000..8066d563a4 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java @@ -0,0 +1,495 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class FreeformQuery extends AbstractTransactionalQuery + implements QueryDelegate { + + FreeformQueryDelegate delegate = null; + private String queryString; + private List<String> primaryKeyColumns; + + /** + * Prevent no-parameters instantiation of FreeformQuery + */ + @SuppressWarnings("unused") + private FreeformQuery() { + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if this + * parameter is null or empty. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @deprecated As of 6.7, @see + * {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)} + */ + @Deprecated + public FreeformQuery(String queryString, List<String> primaryKeyColumns, + JDBCConnectionPool connectionPool) { + super(connectionPool); + if (primaryKeyColumns == null) { + primaryKeyColumns = new ArrayList<String>(); + } + if (primaryKeyColumns.contains("")) { + throw new IllegalArgumentException( + "The primary key columns contain an empty string!"); + } else if (queryString == null || "".equals(queryString)) { + throw new IllegalArgumentException( + "The query string may not be empty or null!"); + } else if (connectionPool == null) { + throw new IllegalArgumentException( + "The connectionPool may not be null!"); + } + this.queryString = queryString; + this.primaryKeyColumns = Collections + .unmodifiableList(primaryKeyColumns); + } + + /** + * Creates a new freeform query delegate to be used with the + * {@link SQLContainer}. + * + * @param queryString + * The actual query to perform. + * @param connectionPool + * the JDBCConnectionPool to use to open connections to the SQL + * database. + * @param primaryKeyColumns + * The primary key columns. Read-only mode is forced if none are + * provided. (optional) + */ + public FreeformQuery(String queryString, JDBCConnectionPool connectionPool, + String... primaryKeyColumns) { + this(queryString, Arrays.asList(primaryKeyColumns), connectionPool); + } + + /** + * This implementation of getCount() actually fetches all records from the + * database, which might be a performance issue. Override this method with a + * SELECT COUNT(*) ... query if this is too slow for your needs. + * + * {@inheritDoc} + */ + @Override + public int getCount() throws SQLException { + // First try the delegate + int count = countByDelegate(); + if (count < 0) { + // Couldn't use the delegate, use the bad way. + Statement statement = null; + ResultSet rs = null; + Connection conn = getConnection(); + try { + statement = conn.createStatement( + ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.CONCUR_READ_ONLY); + + rs = statement.executeQuery(queryString); + if (rs.last()) { + count = rs.getRow(); + } else { + count = 0; + } + } finally { + releaseConnection(conn, statement, rs); + } + } + return count; + } + + @SuppressWarnings("deprecation") + private int countByDelegate() throws SQLException { + int count = -1; + if (delegate == null) { + return count; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + PreparedStatement pstmt = null; + ResultSet rs = null; + Connection c = getConnection(); + try { + pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + rs = pstmt.executeQuery(); + if (rs.next()) { + count = rs.getInt(1); + } else { + // The result can be empty when using group by and there + // are no matches (#18043) + count = 0; + } + } finally { + releaseConnection(c, pstmt, rs); + } + return count; + } catch (UnsupportedOperationException e) { + // Count statement generation not supported + } + } + /* Try using regular statement */ + try { + String countQuery = delegate.getCountQuery(); + if (countQuery != null) { + Statement statement = null; + ResultSet rs = null; + Connection conn = getConnection(); + try { + statement = conn.createStatement(); + rs = statement.executeQuery(countQuery); + if (rs.next()) { + count = rs.getInt(1); + } else { + // The result can be empty when using group by and there + // are no matches (#18043) + count = 0; + } + return count; + } finally { + releaseConnection(conn, statement, rs); + } + } + } catch (UnsupportedOperationException e) { + // Count query generation not supported + } + return count; + } + + /** + * Fetches the results for the query. This implementation always fetches the + * entire record set, ignoring the offset and page length parameters. In + * order to support lazy loading of records, you must supply a + * FreeformQueryDelegate that implements the + * FreeformQueryDelegate.getQueryString(int,int) method. + * + * @throws SQLException + * + * @see FreeformQueryDelegate#getQueryString(int, int) + */ + @Override + @SuppressWarnings({ "deprecation", "finally" }) + public ResultSet getResults(int offset, int pagelength) + throws SQLException { + ensureTransaction(); + String query = queryString; + if (delegate != null) { + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getQueryStatement(offset, pagelength); + PreparedStatement pstmt = getConnection() + .prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + return pstmt.executeQuery(); + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getQueryString(offset, pagelength); + } catch (UnsupportedOperationException e) { + // This is fine, we'll just use the default queryString. + } + } + Statement statement = getConnection().createStatement(); + ResultSet rs; + try { + rs = statement.executeQuery(query); + } catch (SQLException e) { + try { + statement.close(); + } finally { + // throw the original exception even if closing the statement + // fails + throw e; + } + } + return rs; + } + + @Override + @SuppressWarnings("deprecation") + public boolean implementationRespectsPagingLimits() { + if (delegate == null) { + return false; + } + /* First try using prepared statement */ + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getCountStatement(); + if (sh != null && sh.getQueryString() != null + && sh.getQueryString().length() > 0) { + return true; + } + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + String queryString = delegate.getQueryString(0, 50); + return queryString != null && queryString.length() > 0; + } catch (UnsupportedOperationException e) { + return false; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setFilters(java + * .util.List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setFilters(filters); + } else if (filters != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setOrderBy(java + * .util.List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (delegate != null) { + delegate.setOrderBy(orderBys); + } else if (orderBys != null) { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .data.util.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) throws SQLException { + if (!isInTransaction()) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot store items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.storeRow(getConnection(), row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#removeRow(com. + * vaadin .data.util.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) throws SQLException { + if (!isInTransaction()) { + throw new IllegalStateException("No transaction is active!"); + } else if (primaryKeyColumns.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot remove items fetched with a read-only freeform query!"); + } + if (delegate != null) { + return delegate.removeRow(getConnection(), row); + } else { + throw new UnsupportedOperationException( + "FreeFormQueryDelegate not set!"); + } + } + + @Override + public synchronized void beginTransaction() + throws UnsupportedOperationException, SQLException { + super.beginTransaction(); + } + + @Override + public synchronized void commit() + throws UnsupportedOperationException, SQLException { + super.commit(); + } + + @Override + public synchronized void rollback() + throws UnsupportedOperationException, SQLException { + super.rollback(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate# + * getPrimaryKeyColumns () + */ + @Override + public List<String> getPrimaryKeyColumns() { + return primaryKeyColumns; + } + + public String getQueryString() { + return queryString; + } + + public FreeformQueryDelegate getDelegate() { + return delegate; + } + + public void setDelegate(FreeformQueryDelegate delegate) { + this.delegate = delegate; + } + + /** + * This implementation of the containsRowWithKey method rewrites existing + * WHERE clauses in the query string. The logic is, however, not very + * complex and some times can do the Wrong Thing<sup>TM</sup>. For the + * situations where this logic is not enough, you can implement the + * getContainsRowQueryString method in FreeformQueryDelegate and this will + * be used instead of the logic. + * + * @see FreeformQueryDelegate#getContainsRowQueryString(Object...) + * + */ + @Override + @SuppressWarnings("deprecation") + public boolean containsRowWithKey(Object... keys) throws SQLException { + String query = null; + boolean contains = false; + if (delegate != null) { + if (delegate instanceof FreeformStatementDelegate) { + try { + StatementHelper sh = ((FreeformStatementDelegate) delegate) + .getContainsRowQueryStatement(keys); + + PreparedStatement pstmt = null; + ResultSet rs = null; + Connection c = getConnection(); + try { + pstmt = c.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + rs = pstmt.executeQuery(); + contains = rs.next(); + return contains; + } finally { + releaseConnection(c, pstmt, rs); + } + } catch (UnsupportedOperationException e) { + // Statement generation not supported, continue... + } + } + try { + query = delegate.getContainsRowQueryString(keys); + } catch (UnsupportedOperationException e) { + query = modifyWhereClause(keys); + } + } else { + query = modifyWhereClause(keys); + } + Statement statement = null; + ResultSet rs = null; + Connection conn = getConnection(); + try { + statement = conn.createStatement(); + rs = statement.executeQuery(query); + contains = rs.next(); + } finally { + releaseConnection(conn, statement, rs); + } + return contains; + } + + private String modifyWhereClause(Object... keys) { + // Build the where rules for the provided keys + StringBuffer where = new StringBuffer(); + for (int ix = 0; ix < primaryKeyColumns.size(); ix++) { + where.append(QueryBuilder.quote(primaryKeyColumns.get(ix))); + if (keys[ix] == null) { + where.append(" IS NULL"); + } else { + where.append(" = '").append(keys[ix]).append("'"); + } + if (ix < primaryKeyColumns.size() - 1) { + where.append(" AND "); + } + } + // Is there already a WHERE clause in the query string? + int index = queryString.toLowerCase().indexOf("where "); + if (index > -1) { + // Rewrite the where clause + return queryString.substring(0, index) + "WHERE " + where + " AND " + + queryString.substring(index + 6); + } + // Append a where clause + return queryString + " WHERE " + where; + } + + private void writeObject(java.io.ObjectOutputStream out) + throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java new file mode 100644 index 0000000000..fac0b1ab45 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java @@ -0,0 +1,130 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface FreeformQueryDelegate extends Serializable { + /** + * Should return the SQL query string to be performed. This method is + * responsible for gluing together the select query from the filters and the + * order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + * @deprecated As of 6.7. Implement {@link FreeformStatementDelegate} + * instead of {@link FreeformQueryDelegate} + */ + @Deprecated + public String getQueryString(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + * @deprecated As of 6.7. Implement {@link FreeformStatementDelegate} + * instead of {@link FreeformQueryDelegate} + */ + @Deprecated + public String getCountQuery() throws UnsupportedOperationException; + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be stored or updated. + * @throws UnsupportedOperationException + * if the implementation is read only. + * @throws SQLException + */ + public int storeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param conn + * the JDBC connection to use + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Generates an SQL Query string that allows the user of the FreeformQuery + * class to customize the query string used by the + * FreeformQuery.containsRowWithKeys() method. This is useful for cases when + * the logic in the containsRowWithKeys method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + * @deprecated As of 6.7. Implement {@link FreeformStatementDelegate} + * instead of {@link FreeformQueryDelegate} + */ + @Deprecated + public String getContainsRowQueryString(Object... keys) + throws UnsupportedOperationException; +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java new file mode 100644 index 0000000000..884a303684 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +/** + * FreeformStatementDelegate is an extension to FreeformQueryDelegate that + * provides definitions for methods that produce StatementHelper objects instead + * of basic query strings. This allows the FreeformQuery query delegate to use + * PreparedStatements instead of regular Statement when accessing the database. + * + * Due to the injection protection and other benefits of prepared statements, it + * is advisable to implement this interface instead of the FreeformQueryDelegate + * whenever possible. + */ +public interface FreeformStatementDelegate extends FreeformQueryDelegate { + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement. This + * method is responsible for gluing together the select query from the + * filters and the order by conditions if these are supported. + * + * @param offset + * the first record (row) to fetch. + * @param pagelength + * the number of records (rows) to fetch. 0 means all records + * starting from offset. + */ + public StatementHelper getQueryStatement(int offset, int limit) + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement that + * will fetch the row count from the DB. Row count should be fetched using + * filters that are currently set to the QueryDelegate. + */ + public StatementHelper getCountStatement() + throws UnsupportedOperationException; + + /** + * Should return a new instance of StatementHelper that contains the query + * string and parameter values required to create a PreparedStatement used + * by the FreeformQuery.containsRowWithKeys() method. This is useful for + * cases when the default logic in said method is not enough to support more + * complex free form queries. + * + * @param keys + * the values of the primary keys + * @throws UnsupportedOperationException + * to use the default logic in FreeformQuery + */ + public StatementHelper getContainsRowQueryStatement(Object... keys) + throws UnsupportedOperationException; +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/OrderBy.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/OrderBy.java new file mode 100644 index 0000000000..ed57967772 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/OrderBy.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.Serializable; + +/** + * OrderBy represents a sorting rule to be applied to a query made by the + * SQLContainer's QueryDelegate. + * + * The sorting rule is simple and contains only the affected column's name and + * the direction of the sort. + */ +public class OrderBy implements Serializable { + private String column; + private boolean isAscending; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private OrderBy() { + } + + public OrderBy(String column, boolean isAscending) { + setColumn(column); + setAscending(isAscending); + } + + public void setColumn(String column) { + this.column = column; + } + + public String getColumn() { + return column; + } + + public void setAscending(boolean isAscending) { + this.isAscending = isAscending; + } + + public boolean isAscending() { + return isAscending; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java new file mode 100644 index 0000000000..16e04a7da0 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java @@ -0,0 +1,239 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; + +public interface QueryDelegate extends Serializable { + /** + * Generates and executes a query to determine the current row count from + * the DB. Row count will be fetched using filters that are currently set to + * the QueryDelegate. + * + * @return row count + * @throws SQLException + */ + public int getCount() throws SQLException; + + /** + * Executes a paged SQL query and returns the ResultSet. The query is + * defined through implementations of this QueryDelegate interface. + * + * @param offset + * the first item of the page to load + * @param pagelength + * the length of the page to load + * @return a ResultSet containing the rows of the page + * @throws SQLException + * if the database access fails. + */ + public ResultSet getResults(int offset, int pagelength) throws SQLException; + + /** + * Allows the SQLContainer implementation to check whether the QueryDelegate + * implementation implements paging in the getResults method. + * + * @see QueryDelegate#getResults(int, int) + * + * @return true if the delegate implements paging + */ + public boolean implementationRespectsPagingLimits(); + + /** + * Sets the filters to apply when performing the SQL query. These are + * translated into a WHERE clause. Default filtering mode will be used. + * + * @param filters + * The filters to apply. + * @throws UnsupportedOperationException + * if the implementation doesn't support filtering. + */ + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException; + + /** + * Sets the order in which to retrieve rows from the database. The result + * can be ordered by zero or more columns and each column can be in + * ascending or descending order. These are translated into an ORDER BY + * clause in the SQL query. + * + * @param orderBys + * A list of the OrderBy conditions. + * @throws UnsupportedOperationException + * if the implementation doesn't support ordering. + */ + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException; + + /** + * Stores a row in the database. The implementation of this interface + * decides how to identify whether to store a new row or update an existing + * one. + * + * @param columnToValueMap + * A map containing the values for all columns to be stored or + * updated. + * @return the number of affected rows in the database table + * @throws UnsupportedOperationException + * if the implementation is read only. + */ + public int storeRow(RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Removes the given RowItem from the database. + * + * @param row + * RowItem to be removed + * @return true on success + * @throws UnsupportedOperationException + * @throws SQLException + */ + public boolean removeRow(RowItem row) + throws UnsupportedOperationException, SQLException; + + /** + * Starts a new database transaction. Used when storing multiple changes. + * + * Note that if a transaction is already open, it will be rolled back when a + * new transaction is started. + * + * @throws SQLException + * if the database access fails. + */ + public void beginTransaction() throws SQLException; + + /** + * Commits a transaction. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void commit() throws SQLException; + + /** + * Rolls a transaction back. If a transaction is not open nothing should + * happen. + * + * @throws SQLException + * if the database access fails. + */ + public void rollback() throws SQLException; + + /** + * Returns a list of primary key column names. The list is either fetched + * from the database (TableQuery) or given as an argument depending on + * implementation. + * + * @return + */ + public List<String> getPrimaryKeyColumns(); + + /** + * Performs a query to find out whether the SQL table contains a row with + * the given set of primary keys. + * + * @param keys + * the primary keys + * @return true if the SQL table contains a row with the provided keys + * @throws SQLException + */ + public boolean containsRowWithKey(Object... keys) throws SQLException; + + /************************/ + /** ROWID CHANGE EVENT **/ + /************************/ + + /** + * An <code>Event</code> object specifying the old and new RowId of an added + * item after the addition has been successfully committed. + */ + public interface RowIdChangeEvent extends Serializable { + /** + * Gets the old (temporary) RowId of the added row that raised this + * event. + * + * @return old RowId + */ + public RowId getOldRowId(); + + /** + * Gets the new, possibly database assigned RowId of the added row that + * raised this event. + * + * @return new RowId + */ + public RowId getNewRowId(); + } + + /** RowId change listener interface. */ + public interface RowIdChangeListener extends Serializable { + /** + * Lets the listener know that a RowId has been changed. + * + * @param event + */ + public void rowIdChange(QueryDelegate.RowIdChangeEvent event); + } + + /** + * The interface for adding and removing <code>RowIdChangeEvent</code> + * listeners. By implementing this interface a class explicitly announces + * that it will generate a <code>RowIdChangeEvent</code> when it performs a + * database commit that may change the RowId. + */ + public interface RowIdChangeNotifier extends Serializable { + /** + * Adds a RowIdChangeListener for the object. + * + * @param listener + * listener to be added + */ + public void addRowIdChangeListener( + QueryDelegate.RowIdChangeListener listener); + + /** + * @deprecated As of 7.0, replaced by + * {@link #addRowIdChangeListener(RowIdChangeListener)} + **/ + @Deprecated + public void addListener(QueryDelegate.RowIdChangeListener listener); + + /** + * Removes the specified RowIdChangeListener from the object. + * + * @param listener + * listener to be removed + */ + public void removeRowIdChangeListener( + QueryDelegate.RowIdChangeListener listener); + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeRowIdChangeListener(RowIdChangeListener)} + **/ + @Deprecated + public void removeListener(QueryDelegate.RowIdChangeListener listener); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/TableQuery.java new file mode 100644 index 0000000000..959c494634 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/TableQuery.java @@ -0,0 +1,869 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.OptimisticLockException; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +@SuppressWarnings("serial") +public class TableQuery extends AbstractTransactionalQuery + implements QueryDelegate, QueryDelegate.RowIdChangeNotifier { + + /** + * Table name (without catalog or schema information). + */ + private String tableName; + private String catalogName; + private String schemaName; + /** + * Cached concatenated version of the table name. + */ + private String fullTableName; + /** + * Primary key column name(s) in the table. + */ + private List<String> primaryKeyColumns; + /** + * Version column name in the table. + */ + private String versionColumn; + + /** Currently set Filters and OrderBys */ + private List<Filter> filters; + private List<OrderBy> orderBys; + + /** SQLGenerator instance to use for generating queries */ + private SQLGenerator sqlGenerator; + + /** Row ID change listeners */ + private LinkedList<RowIdChangeListener> rowIdChangeListeners; + /** Row ID change events, stored until commit() is called */ + private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>(); + + /** Set to true to output generated SQL Queries to System.out */ + private final boolean debug = false; + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. All parameters must be non-null. + * + * The table name must be a simple name with no catalog or schema + * information. If those are needed, use + * {@link #TableQuery(String, String, String, JDBCConnectionPool, SQLGenerator)} + * . + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool, + SQLGenerator sqlGenerator) { + this(null, null, tableName, connectionPool, sqlGenerator); + } + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. Catalog and schema names can be + * null, all other parameters must be non-null. + * + * @param catalogName + * Name of the database catalog (can be null) + * @param schemaName + * Name of the database schema (can be null) + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + * @since 7.1 + */ + public TableQuery(String catalogName, String schemaName, String tableName, + JDBCConnectionPool connectionPool, SQLGenerator sqlGenerator) { + this(catalogName, schemaName, tableName, connectionPool, sqlGenerator, + true); + } + + /** + * Creates a new TableQuery using the given connection pool and table name + * to fetch the data from. All parameters must be non-null. The default SQL + * generator will be used for queries. + * + * The table name must be a simple name with no catalog or schema + * information. If those are needed, use + * {@link #TableQuery(String, String, String, JDBCConnectionPool, SQLGenerator)} + * . + * + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + */ + public TableQuery(String tableName, JDBCConnectionPool connectionPool) { + this(tableName, connectionPool, new DefaultSQLGenerator()); + } + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. Catalog and schema names can be + * null, all other parameters must be non-null. + * + * @param catalogName + * Name of the database catalog (can be null) + * @param schemaName + * Name of the database schema (can be null) + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + * @param escapeNames + * true to escape special characters in catalog, schema and table + * names, false to use the names as-is + * @since 7.1 + */ + protected TableQuery(String catalogName, String schemaName, + String tableName, JDBCConnectionPool connectionPool, + SQLGenerator sqlGenerator, boolean escapeNames) { + super(connectionPool); + if (tableName == null || tableName.trim().length() < 1 + || connectionPool == null || sqlGenerator == null) { + throw new IllegalArgumentException( + "Table name, connection pool and SQL generator parameters must be non-null and non-empty."); + } + if (escapeNames) { + this.catalogName = SQLUtil.escapeSQL(catalogName); + this.schemaName = SQLUtil.escapeSQL(schemaName); + this.tableName = SQLUtil.escapeSQL(tableName); + } else { + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = tableName; + } + this.sqlGenerator = sqlGenerator; + fetchMetaData(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount() + */ + @Override + public int getCount() throws SQLException { + getLogger().log(Level.FINE, "Fetching count..."); + StatementHelper sh = sqlGenerator.generateSelectQuery( + getFullTableName(), filters, null, 0, 0, "COUNT(*)"); + boolean shouldCloseTransaction = false; + if (!isInTransaction()) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet r = null; + int count = -1; + try { + r = executeQuery(sh); + r.next(); + count = r.getInt(1); + } finally { + try { + if (r != null) { + // Do not release connection, it is done in commit() + releaseConnection(null, r.getStatement(), r); + } + } finally { + if (shouldCloseTransaction) { + commit(); + } + } + } + return count; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int, + * int) + */ + @Override + public ResultSet getResults(int offset, int pagelength) + throws SQLException { + StatementHelper sh; + /* + * If no ordering is explicitly set, results will be ordered by the + * first primary key column. + */ + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + for (int i = 0; i < primaryKeyColumns.size(); i++) { + ob.add(new OrderBy(primaryKeyColumns.get(i), true)); + } + sh = sqlGenerator.generateSelectQuery(getFullTableName(), filters, + ob, offset, pagelength, null); + } else { + sh = sqlGenerator.generateSelectQuery(getFullTableName(), filters, + orderBys, offset, pagelength, null); + } + return executeQuery(sh); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate# + * implementationRespectsPagingLimits() + */ + @Override + public boolean implementationRespectsPagingLimits() { + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public int storeRow(RowItem row) + throws UnsupportedOperationException, SQLException { + if (row == null) { + throw new IllegalArgumentException( + "Row argument must be non-null."); + } + StatementHelper sh; + int result = 0; + if (row.getId() instanceof TemporaryRowId) { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateInsertQuery(getFullTableName(), row); + result = executeUpdateReturnKeys(sh, row); + } else { + setVersionColumnFlagInProperty(row); + sh = sqlGenerator.generateUpdateQuery(getFullTableName(), row); + result = executeUpdate(sh); + } + if (versionColumn != null && result == 0) { + throw new OptimisticLockException( + "Someone else changed the row that was being updated.", + row.getId()); + } + return result; + } + + private void setVersionColumnFlagInProperty(RowItem row) { + ColumnProperty versionProperty = (ColumnProperty) row + .getItemProperty(versionColumn); + if (versionProperty != null) { + versionProperty.setVersionColumn(true); + } + } + + /** + * Inserts the given row in the database table immediately. Begins and + * commits the transaction needed. This method was added specifically to + * solve the problem of returning the final RowId immediately on the + * SQLContainer.addItem() call when auto commit mode is enabled in the + * SQLContainer. + * + * @param row + * RowItem to add to the database + * @return Final RowId of the added row + * @throws SQLException + */ + public RowId storeRowImmediately(RowItem row) throws SQLException { + beginTransaction(); + /* Set version column, if one is provided */ + setVersionColumnFlagInProperty(row); + /* Generate query */ + StatementHelper sh = sqlGenerator + .generateInsertQuery(getFullTableName(), row); + Connection connection = null; + PreparedStatement pstmt = null; + ResultSet generatedKeys = null; + connection = getConnection(); + try { + pstmt = connection.prepareStatement(sh.getQueryString(), + primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> {0}", sh.getQueryString()); + int result = pstmt.executeUpdate(); + RowId newId = null; + if (result > 0) { + /* + * If affected rows exist, we'll get the new RowId, commit the + * transaction and return the new RowId. + */ + generatedKeys = pstmt.getGeneratedKeys(); + newId = getNewRowId(row, generatedKeys); + } + // transaction has to be closed in any case + commit(); + return newId; + } finally { + releaseConnection(connection, pstmt, generatedKeys); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util + * .List) + */ + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + if (filters == null) { + this.filters = null; + return; + } + this.filters = Collections.unmodifiableList(filters); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util + * .List) + */ + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + if (orderBys == null) { + this.orderBys = null; + return; + } + this.orderBys = Collections.unmodifiableList(orderBys); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction() + */ + @Override + public void beginTransaction() + throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "DB -> begin transaction"); + super.beginTransaction(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit() + */ + @Override + public void commit() throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "DB -> commit"); + super.commit(); + + /* Handle firing row ID change events */ + RowIdChangeEvent[] unFiredEvents = bufferedEvents + .toArray(new RowIdChangeEvent[] {}); + bufferedEvents.clear(); + if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) { + for (RowIdChangeListener r : rowIdChangeListeners) { + for (RowIdChangeEvent e : unFiredEvents) { + r.rowIdChange(e); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback() + */ + @Override + public void rollback() throws UnsupportedOperationException, SQLException { + getLogger().log(Level.FINE, "DB -> rollback"); + super.rollback(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns() + */ + @Override + public List<String> getPrimaryKeyColumns() { + return Collections.unmodifiableList(primaryKeyColumns); + } + + public String getVersionColumn() { + return versionColumn; + } + + public void setVersionColumn(String column) { + versionColumn = column; + } + + /** + * Returns the table name for the query without catalog and schema + * information. + * + * @return table name, not null + */ + public String getTableName() { + return tableName; + } + + /** + * Returns the catalog name for the query. + * + * @return catalog name, can be null + * @since 7.1 + */ + public String getCatalogName() { + return catalogName; + } + + /** + * Returns the catalog name for the query. + * + * @return catalog name, can be null + * @since 7.1 + */ + public String getSchemaName() { + return schemaName; + } + + /** + * Returns the complete table name obtained by concatenation of the catalog + * and schema names (if any) and the table name. + * + * This method can be overridden if customization is needed. + * + * @return table name in the form it should be used in query and update + * statements + * @since 7.1 + */ + protected String getFullTableName() { + if (fullTableName == null) { + StringBuilder sb = new StringBuilder(); + if (catalogName != null) { + sb.append(catalogName).append("."); + } + if (schemaName != null) { + sb.append(schemaName).append("."); + } + sb.append(tableName); + fullTableName = sb.toString(); + } + return fullTableName; + } + + public SQLGenerator getSqlGenerator() { + return sqlGenerator; + } + + /** + * Executes the given query string using either the active connection if a + * transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return ResultSet of the query + * @throws SQLException + */ + private ResultSet executeQuery(StatementHelper sh) throws SQLException { + ensureTransaction(); + Connection connection = getConnection(); + PreparedStatement pstmt = null; + try { + pstmt = connection.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> {0}", sh.getQueryString()); + return pstmt.executeQuery(); + } catch (SQLException e) { + releaseConnection(null, pstmt, null); + throw e; + } + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdate(StatementHelper sh) throws SQLException { + PreparedStatement pstmt = null; + Connection connection = null; + try { + connection = getConnection(); + pstmt = connection.prepareStatement(sh.getQueryString()); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> {0}", sh.getQueryString()); + int retval = pstmt.executeUpdate(); + return retval; + } finally { + releaseConnection(connection, pstmt, null); + } + } + + /** + * Executes the given update query string using either the active connection + * if a transaction is already open, or a new connection from this query's + * connection pool. + * + * Additionally adds a new RowIdChangeEvent to the event buffer. + * + * @param sh + * an instance of StatementHelper, containing the query string + * and parameter values. + * @param row + * the row item to update + * @return Number of affected rows + * @throws SQLException + */ + private int executeUpdateReturnKeys(StatementHelper sh, RowItem row) + throws SQLException { + PreparedStatement pstmt = null; + ResultSet genKeys = null; + Connection connection = null; + try { + connection = getConnection(); + pstmt = connection.prepareStatement(sh.getQueryString(), + primaryKeyColumns.toArray(new String[0])); + sh.setParameterValuesToStatement(pstmt); + getLogger().log(Level.FINE, "DB -> {0}", sh.getQueryString()); + int result = pstmt.executeUpdate(); + genKeys = pstmt.getGeneratedKeys(); + RowId newId = getNewRowId(row, genKeys); + bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId)); + return result; + } finally { + releaseConnection(connection, pstmt, genKeys); + } + } + + /** + * Fetches name(s) of primary key column(s) from DB metadata. + * + * Also tries to get the escape string to be used in search strings. + */ + private void fetchMetaData() { + Connection connection = null; + ResultSet rs = null; + ResultSet tables = null; + try { + connection = getConnection(); + DatabaseMetaData dbmd = connection.getMetaData(); + if (dbmd != null) { + tables = dbmd.getTables(catalogName, schemaName, tableName, + null); + if (!tables.next()) { + String catalog = (catalogName != null) + ? catalogName.toUpperCase() : null; + String schema = (schemaName != null) + ? schemaName.toUpperCase() : null; + tables = dbmd.getTables(catalog, schema, + tableName.toUpperCase(), null); + if (!tables.next()) { + throw new IllegalArgumentException( + "Table with the name \"" + getFullTableName() + + "\" was not found. Check your database contents."); + } else { + catalogName = catalog; + schemaName = schema; + tableName = tableName.toUpperCase(); + } + } + tables.close(); + rs = dbmd.getPrimaryKeys(catalogName, schemaName, tableName); + List<String> names = new ArrayList<String>(); + while (rs.next()) { + names.add(rs.getString("COLUMN_NAME")); + } + rs.close(); + if (!names.isEmpty()) { + primaryKeyColumns = names; + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Primary key constraints have not been defined for the table \"" + + getFullTableName() + + "\". Use FreeFormQuery to access this table."); + } + for (String colName : primaryKeyColumns) { + if (colName.equalsIgnoreCase("rownum")) { + if (getSqlGenerator() instanceof MSSQLGenerator + || getSqlGenerator() instanceof MSSQLGenerator) { + throw new IllegalArgumentException( + "When using Oracle or MSSQL, a primary key column" + + " named \'rownum\' is not allowed!"); + } + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } finally { + try { + releaseConnection(connection, null, rs); + } catch (SQLException ignore) { + } finally { + try { + if (tables != null) { + tables.close(); + } + } catch (SQLException ignore) { + } + } + } + } + + private RowId getNewRowId(RowItem row, ResultSet genKeys) { + try { + /* Fetch primary key values and generate a map out of them. */ + Map<String, Object> values = new HashMap<String, Object>(); + ResultSetMetaData rsmd = genKeys.getMetaData(); + int colCount = rsmd.getColumnCount(); + if (genKeys.next()) { + for (int i = 1; i <= colCount; i++) { + values.put(rsmd.getColumnName(i), genKeys.getObject(i)); + } + } + /* Generate new RowId */ + List<Object> newRowId = new ArrayList<Object>(); + if (values.size() == 1) { + if (primaryKeyColumns.size() == 1) { + newRowId.add(values.get(values.keySet().iterator().next())); + } else { + for (String s : primaryKeyColumns) { + if (!((ColumnProperty) row.getItemProperty(s)) + .isReadOnlyChangeAllowed()) { + newRowId.add(values + .get(values.keySet().iterator().next())); + } else { + newRowId.add(values.get(s)); + } + } + } + } else { + for (String s : primaryKeyColumns) { + newRowId.add(values.get(s)); + } + } + return new RowId(newRowId.toArray()); + } catch (Exception e) { + getLogger().log(Level.FINE, + "Failed to fetch key values on insert: {0}", + e.getMessage()); + return null; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin + * .addon.sqlcontainer.RowItem) + */ + @Override + public boolean removeRow(RowItem row) + throws UnsupportedOperationException, SQLException { + if (getLogger().isLoggable(Level.FINE)) { + getLogger().log(Level.FINE, "Removing row with id: {0}", + row.getId().getId()[0]); + } + if (executeUpdate(sqlGenerator.generateDeleteQuery(getFullTableName(), + primaryKeyColumns, versionColumn, row)) == 1) { + return true; + } + if (versionColumn != null) { + throw new OptimisticLockException( + "Someone else changed the row that was being deleted.", + row.getId()); + } + return false; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey( + * java.lang.Object[]) + */ + @Override + public boolean containsRowWithKey(Object... keys) throws SQLException { + ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>(); + if (filters != null) { + filtersAndKeys.addAll(filters); + } + int ix = 0; + for (String colName : primaryKeyColumns) { + filtersAndKeys.add(new Equal(colName, keys[ix])); + ix++; + } + StatementHelper sh = sqlGenerator.generateSelectQuery( + getFullTableName(), filtersAndKeys, orderBys, 0, 0, "*"); + + boolean shouldCloseTransaction = false; + if (!isInTransaction()) { + shouldCloseTransaction = true; + beginTransaction(); + } + ResultSet rs = null; + try { + rs = executeQuery(sh); + boolean contains = rs.next(); + return contains; + } finally { + try { + if (rs != null) { + // Do not release connection, it is done in commit() + releaseConnection(null, rs.getStatement(), rs); + } + } finally { + if (shouldCloseTransaction) { + commit(); + } + } + } + } + + /** + * Custom writeObject to call rollback() if object is serialized. + */ + private void writeObject(java.io.ObjectOutputStream out) + throws IOException { + try { + rollback(); + } catch (SQLException ignored) { + } + out.defaultWriteObject(); + } + + /** + * Simple RowIdChangeEvent implementation. + */ + public static class RowIdChangeEvent extends EventObject + implements QueryDelegate.RowIdChangeEvent { + private final RowId oldId; + private final RowId newId; + + private RowIdChangeEvent(RowId oldId, RowId newId) { + super(oldId); + this.oldId = oldId; + this.newId = newId; + } + + @Override + public RowId getNewRowId() { + return newId; + } + + @Override + public RowId getOldRowId() { + return oldId; + } + } + + /** + * Adds RowIdChangeListener to this query + */ + @Override + public void addRowIdChangeListener(RowIdChangeListener listener) { + if (rowIdChangeListeners == null) { + rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>(); + } + rowIdChangeListeners.add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addRowIdChangeListener(com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener)} + **/ + @Override + @Deprecated + public void addListener(RowIdChangeListener listener) { + addRowIdChangeListener(listener); + } + + /** + * Removes the given RowIdChangeListener from this query + */ + @Override + public void removeRowIdChangeListener(RowIdChangeListener listener) { + if (rowIdChangeListeners != null) { + rowIdChangeListeners.remove(listener); + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeRowIdChangeListener(com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(RowIdChangeListener listener) { + removeRowIdChangeListener(listener); + } + + private static final Logger getLogger() { + return Logger.getLogger(TableQuery.class.getName()); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java new file mode 100644 index 0000000000..9c05545e41 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java @@ -0,0 +1,395 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.ColumnProperty; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLUtil; +import com.vaadin.data.util.sqlcontainer.TemporaryRowId; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator; + +/** + * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +@SuppressWarnings("serial") +public class DefaultSQLGenerator implements SQLGenerator { + + private Class<? extends StatementHelper> statementHelperClass = null; + + public DefaultSQLGenerator() { + + } + + /** + * Create a new DefaultSqlGenerator instance that uses the given + * implementation of {@link StatementHelper} + * + * @param statementHelper + */ + public DefaultSQLGenerator( + Class<? extends StatementHelper> statementHelperClazz) { + this(); + statementHelperClass = statementHelperClazz; + } + + /** + * Construct a DefaultSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd) { + QueryBuilder + .setStringDecorator(new StringDecorator(quoteStart, quoteEnd)); + } + + /** + * Same as {@link #DefaultSQLGenerator(String, String)} but with support for + * custom {@link StatementHelper} implementation. + * + * @param quoteStart + * @param quoteEnd + * @param statementHelperClazz + */ + public DefaultSQLGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + this(quoteStart, quoteEnd); + statementHelperClass = statementHelperClazz; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, java.util.List, + * int, int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("SELECT " + toSelect + " FROM ") + .append(SQLUtil.escapeSQL(tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + if (pagelength != 0) { + generateLimits(query, offset, pagelength); + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateUpdateQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateUpdateQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("Updated item must be given."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("UPDATE ").append(tableName).append(" SET"); + + /* Generate column<->value and rowidentifiers map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + Map<String, Object> rowIdentifiers = generateRowIdentifiers(item); + /* Generate columns and values to update */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (first) { + query.append(" " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(", " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(columnToValueMap.get(column), + item.getItemProperty(column).getType()); + first = false; + } + /* Generate identifiers for the row to be updated */ + first = true; + for (String column : rowIdentifiers.keySet()) { + if (first) { + query.append(" WHERE " + QueryBuilder.quote(column) + " = ?"); + } else { + query.append(" AND " + QueryBuilder.quote(column) + " = ?"); + } + sh.addParameterValue(rowIdentifiers.get(column), + item.getItemProperty(column).getType()); + first = false; + } + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateInsertQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateInsertQuery(String tableName, RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException("New item must be given."); + } + if (!(item.getId() instanceof TemporaryRowId)) { + throw new IllegalArgumentException( + "Cannot generate an insert query for item already in database."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("INSERT INTO ").append(tableName).append(" ("); + + /* Generate column<->value map */ + Map<String, Object> columnToValueMap = generateColumnToValueMap(item); + /* Generate column names for insert query */ + boolean first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append(QueryBuilder.quote(column)); + first = false; + } + + /* Generate values for insert query */ + query.append(") VALUES ("); + first = true; + for (String column : columnToValueMap.keySet()) { + if (!first) { + query.append(", "); + } + query.append("?"); + sh.addParameterValue(columnToValueMap.get(column), + item.getItemProperty(column).getType()); + first = false; + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator# + * generateDeleteQuery(java.lang.String, + * com.vaadin.addon.sqlcontainer.RowItem) + */ + @Override + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, + RowItem item) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + if (item == null) { + throw new IllegalArgumentException( + "Item to be deleted must be given."); + } + if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { + throw new IllegalArgumentException( + "Valid keyColumnNames must be provided."); + } + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + query.append("DELETE FROM ").append(tableName).append(" WHERE "); + int count = 1; + for (String keyColName : primaryKeyColumns) { + if ((this instanceof MSSQLGenerator + || this instanceof OracleGenerator) + && keyColName.equalsIgnoreCase("rownum")) { + count++; + continue; + } + if (count > 1) { + query.append(" AND "); + } + if (item.getItemProperty(keyColName).getValue() != null) { + query.append(QueryBuilder.quote(keyColName) + " = ?"); + sh.addParameterValue( + item.getItemProperty(keyColName).getValue(), + item.getItemProperty(keyColName).getType()); + } + count++; + } + if (versionColumn != null) { + if (!item.getItemPropertyIds().contains(versionColumn)) { + throw new IllegalArgumentException(String.format( + "Table '%s' does not contain version column '%s'.", + tableName, versionColumn)); + } + + query.append(String.format(" AND %s = ?", + QueryBuilder.quote(versionColumn))); + sh.addParameterValue(item.getItemProperty(versionColumn).getValue(), + item.getItemProperty(versionColumn).getType()); + } + + sh.setQueryString(query.toString()); + return sh; + } + + /** + * Generates sorting rules as an ORDER BY -clause + * + * @param sb + * StringBuffer to which the clause is appended. + * @param o + * OrderBy object to be added into the sb. + * @param firstOrderBy + * If true, this is the first OrderBy. + * @return + */ + protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o, + boolean firstOrderBy) { + if (firstOrderBy) { + sb.append(" ORDER BY "); + } else { + sb.append(", "); + } + sb.append(QueryBuilder.quote(o.getColumn())); + if (o.isAscending()) { + sb.append(" ASC"); + } else { + sb.append(" DESC"); + } + return sb; + } + + /** + * Generates the LIMIT and OFFSET clause. + * + * @param sb + * StringBuffer to which the clause is appended. + * @param offset + * Value for offset. + * @param pagelength + * Value for pagelength. + * @return StringBuffer with LIMIT and OFFSET clause added. + */ + protected StringBuffer generateLimits(StringBuffer sb, int offset, + int pagelength) { + sb.append(" LIMIT ").append(pagelength).append(" OFFSET ") + .append(offset); + return sb; + } + + protected Map<String, Object> generateColumnToValueMap(RowItem item) { + Map<String, Object> columnToValueMap = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator + || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + if (cp.isPersistent()) { + columnToValueMap.put(cp.getPropertyId(), cp.getValue()); + } + } + return columnToValueMap; + } + + protected Map<String, Object> generateRowIdentifiers(RowItem item) { + Map<String, Object> rowIdentifiers = new HashMap<String, Object>(); + for (Object id : item.getItemPropertyIds()) { + ColumnProperty cp = (ColumnProperty) item.getItemProperty(id); + /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */ + if ((this instanceof MSSQLGenerator + || this instanceof OracleGenerator) + && cp.getPropertyId().equalsIgnoreCase("rownum")) { + continue; + } + + if (cp.isRowIdentifier()) { + Object value; + if (cp.isPrimaryKey()) { + // If the value of a primary key has changed, its old value + // should be used to identify the row (#9145) + value = cp.getOldValue(); + } else { + value = cp.getValue(); + } + rowIdentifiers.put(cp.getPropertyId(), value); + } + } + return rowIdentifiers; + } + + /** + * Returns the statement helper for the generator. Override this to handle + * platform specific data types. + * + * @see http://dev.vaadin.com/ticket/9148 + * @return a new instance of the statement helper + */ + protected StatementHelper getStatementHelper() { + if (statementHelperClass == null) { + return new StatementHelper(); + } + + try { + return statementHelperClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Unable to instantiate custom StatementHelper", e); + } + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java new file mode 100644 index 0000000000..baddc60e3f --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class MSSQLGenerator extends DefaultSQLGenerator { + + public MSSQLGenerator() { + + } + + /** + * Construct a MSSQLGenerator with the specified identifiers for start and + * end of quoted strings. The identifiers may be different depending on the + * database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public MSSQLGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS t"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append("SELECT * FROM (SELECT row_number() OVER ("); + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(") AS rownum, " + toSelect + " FROM ").append(tableName); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS a WHERE a.rownum BETWEEN ").append(offset) + .append(" AND ").append(Integer.toString(offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java new file mode 100644 index 0000000000..0097b5017c --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator; + +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +@SuppressWarnings("serial") +public class OracleGenerator extends DefaultSQLGenerator { + + public OracleGenerator() { + + } + + public OracleGenerator( + Class<? extends StatementHelper> statementHelperClazz) { + super(statementHelperClazz); + } + + /** + * Construct an OracleSQLGenerator with the specified identifiers for start + * and end of quoted strings. The identifiers may be different depending on + * the database engine and it's settings. + * + * @param quoteStart + * the identifier (character) denoting the start of a quoted + * string + * @param quoteEnd + * the identifier (character) denoting the end of a quoted string + */ + public OracleGenerator(String quoteStart, String quoteEnd) { + super(quoteStart, quoteEnd); + } + + public OracleGenerator(String quoteStart, String quoteEnd, + Class<? extends StatementHelper> statementHelperClazz) { + super(quoteStart, quoteEnd, statementHelperClazz); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator# + * generateSelectQuery(java.lang.String, java.util.List, + * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int, + * int, java.lang.String) + */ + @Override + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect) { + if (tableName == null || tableName.trim().equals("")) { + throw new IllegalArgumentException("Table name must be given."); + } + /* Adjust offset and page length parameters to match "row numbers" */ + offset = pagelength > 1 ? ++offset : offset; + pagelength = pagelength > 1 ? --pagelength : pagelength; + toSelect = toSelect == null ? "*" : toSelect; + StatementHelper sh = getStatementHelper(); + StringBuffer query = new StringBuffer(); + + /* Row count request is handled here */ + if ("COUNT(*)".equalsIgnoreCase(toSelect)) { + query.append(String.format( + "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s", + QueryBuilder.quote("rowcount"), tableName)); + if (filters != null && !filters.isEmpty()) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(")"); + sh.setQueryString(query.toString()); + return sh; + } + + /* SELECT without row number constraints */ + if (offset == 0 && pagelength == 0) { + query.append("SELECT ").append(toSelect).append(" FROM ") + .append(tableName); + if (filters != null) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + sh.setQueryString(query.toString()); + return sh; + } + + /* Remaining SELECT cases are handled here */ + query.append(String.format( + "SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s", + QueryBuilder.quote("rownum"), toSelect, tableName)); + if (filters != null) { + query.append(QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (orderBys != null) { + for (OrderBy o : orderBys) { + generateOrderBy(query, o, orderBys.indexOf(o) == 0); + } + } + query.append(String.format(") x) WHERE %s BETWEEN %d AND %d", + QueryBuilder.quote("rownum"), offset, offset + pagelength)); + sh.setQueryString(query.toString()); + return sh; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java new file mode 100644 index 0000000000..6011346b78 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; + +/** + * The SQLGenerator interface is meant to be implemented for each different SQL + * syntax that is to be supported. By default there are implementations for + * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes. + * + * @author Jonatan Kronqvist / Vaadin Ltd + */ +public interface SQLGenerator extends Serializable { + /** + * Generates a SELECT query with the provided parameters. Uses default + * filtering mode (INCLUSIVE). + * + * @param tableName + * Name of the table queried + * @param filters + * The filters, converted into a WHERE clause + * @param orderBys + * The the ordering conditions, converted into an ORDER BY clause + * @param offset + * The offset of the first row to be included + * @param pagelength + * The number of rows to be returned when the query executes + * @param toSelect + * String containing what to select, e.g. "*", "COUNT(*)" + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateSelectQuery(String tableName, + List<Filter> filters, List<OrderBy> orderBys, int offset, + int pagelength, String toSelect); + + /** + * Generates an UPDATE query with the provided parameters. + * + * @param tableName + * Name of the table queried + * @param item + * RowItem containing the updated values update. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateUpdateQuery(String tableName, RowItem item); + + /** + * Generates an INSERT query for inserting a new row with the provided + * values. + * + * @param tableName + * Name of the table queried + * @param item + * New RowItem to be inserted into the database. + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateInsertQuery(String tableName, RowItem item); + + /** + * Generates a DELETE query for deleting data related to the given RowItem + * from the database. + * + * @param tableName + * Name of the table queried + * @param primaryKeyColumns + * the names of the columns holding the primary key. Usually just + * one column, but might be several. + * @param versionColumn + * the column containing the version number of the row, null if + * versioning (optimistic locking) not enabled. + * @param item + * Item to be deleted from the database + * @return StatementHelper instance containing the query string for a + * PreparedStatement and the values required for the parameters + */ + public StatementHelper generateDeleteQuery(String tableName, + List<String> primaryKeyColumns, String versionColumn, RowItem item); +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java new file mode 100644 index 0000000000..43cfb597bb --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java @@ -0,0 +1,179 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * StatementHelper is a simple helper class that assists TableQuery and the + * query generators in filling a PreparedStatement. The actual statement is + * generated by the query generator methods, but the resulting statement and all + * the parameter values are stored in an instance of StatementHelper. + * + * This class will also fill the values with correct setters into the + * PreparedStatement on request. + */ +public class StatementHelper implements Serializable { + + private String queryString; + + private List<Object> parameters = new ArrayList<Object>(); + private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>(); + + public StatementHelper() { + } + + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + public String getQueryString() { + return queryString; + } + + public void addParameterValue(Object parameter) { + if (parameter != null) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, parameter.getClass()); + } else { + throw new IllegalArgumentException( + "You cannot add null parameters using addParamaters(Object). " + + "Use addParameters(Object,Class) instead"); + } + } + + public void addParameterValue(Object parameter, Class<?> type) { + parameters.add(parameter); + dataTypes.put(parameters.size() - 1, type); + } + + public void setParameterValuesToStatement(PreparedStatement pstmt) + throws SQLException { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) == null) { + handleNullValue(i, pstmt); + } else { + pstmt.setObject(i + 1, parameters.get(i)); + } + } + + /* + * The following list contains the data types supported by + * PreparedStatement but not supported by SQLContainer: + * + * [The list is provided as PreparedStatement method signatures] + * + * setNCharacterStream(int parameterIndex, Reader value) + * + * setNClob(int parameterIndex, NClob value) + * + * setNString(int parameterIndex, String value) + * + * setRef(int parameterIndex, Ref x) + * + * setRowId(int parameterIndex, RowId x) + * + * setSQLXML(int parameterIndex, SQLXML xmlObject) + * + * setBytes(int parameterIndex, byte[] x) + * + * setCharacterStream(int parameterIndex, Reader reader) + * + * setClob(int parameterIndex, Clob x) + * + * setURL(int parameterIndex, URL x) + * + * setArray(int parameterIndex, Array x) + * + * setAsciiStream(int parameterIndex, InputStream x) + * + * setBinaryStream(int parameterIndex, InputStream x) + * + * setBlob(int parameterIndex, Blob x) + */ + } + + private void handleNullValue(int i, PreparedStatement pstmt) + throws SQLException { + Class<?> dataType = dataTypes.get(i); + int index = i + 1; + if (BigDecimal.class.equals(dataType)) { + pstmt.setBigDecimal(index, null); + } else if (Boolean.class.equals(dataType)) { + pstmt.setNull(index, Types.BOOLEAN); + } else if (Byte.class.equals(dataType)) { + pstmt.setNull(index, Types.SMALLINT); + } else if (Date.class.equals(dataType)) { + pstmt.setDate(index, null); + } else if (Double.class.equals(dataType)) { + pstmt.setNull(index, Types.DOUBLE); + } else if (Float.class.equals(dataType)) { + pstmt.setNull(index, Types.FLOAT); + } else if (Integer.class.equals(dataType)) { + pstmt.setNull(index, Types.INTEGER); + } else if (Long.class.equals(dataType)) { + pstmt.setNull(index, Types.BIGINT); + } else if (Short.class.equals(dataType)) { + pstmt.setNull(index, Types.SMALLINT); + } else if (String.class.equals(dataType)) { + pstmt.setString(index, null); + } else if (Time.class.equals(dataType)) { + pstmt.setTime(index, null); + } else if (Timestamp.class.equals(dataType)) { + pstmt.setTimestamp(index, null); + } else if (byte[].class.equals(dataType)) { + pstmt.setBytes(index, null); + } else { + + if (handleUnrecognizedTypeNullValue(i, pstmt, dataTypes)) { + return; + } + + throw new SQLException("Data type for parameter " + i + + " not supported by SQLContainer: " + dataType.getName()); + } + } + + /** + * Handle unrecognized null values. Override this to handle null values for + * platform specific data types that are not handled by the default + * implementation of the {@link StatementHelper}. + * + * @param i + * @param pstmt + * @param dataTypes2 + * + * @return true if handled, false otherwise + * + * @see {@link http://dev.vaadin.com/ticket/9148} + */ + protected boolean handleUnrecognizedTypeNullValue(int i, + PreparedStatement pstmt, Map<Integer, Class<?>> dataTypes) + throws SQLException { + return false; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java new file mode 100644 index 0000000000..6eab9f5596 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.And; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class AndTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof And; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder + .getJoinedFilterString(((And) filter).getFilters(), "AND", sh)); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java new file mode 100644 index 0000000000..2cdecd1e6d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Between; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class BetweenTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Between; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Between between = (Between) filter; + sh.addParameterValue(between.getStartValue()); + sh.addParameterValue(between.getEndValue()); + return QueryBuilder.quote(between.getPropertyId()) + " BETWEEN ? AND ?"; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java new file mode 100644 index 0000000000..bcb348dc8a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class CompareTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Compare; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Compare compare = (Compare) filter; + sh.addParameterValue(compare.getValue()); + String prop = QueryBuilder.quote(compare.getPropertyId()); + switch (compare.getOperation()) { + case EQUAL: + return prop + " = ?"; + case GREATER: + return prop + " > ?"; + case GREATER_OR_EQUAL: + return prop + " >= ?"; + case LESS: + return prop + " < ?"; + case LESS_OR_EQUAL: + return prop + " <= ?"; + default: + return ""; + } + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java new file mode 100644 index 0000000000..e593146550 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public interface FilterTranslator extends Serializable { + public boolean translatesFilter(Filter filter); + + public String getWhereStringForFilter(Filter filter, StatementHelper sh); + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java new file mode 100644 index 0000000000..dd7a90828a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class IsNullTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof IsNull; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + IsNull in = (IsNull) filter; + return QueryBuilder.quote(in.getPropertyId()) + " IS NULL"; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java new file mode 100644 index 0000000000..3c27240e9e --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class LikeTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Like; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Like like = (Like) filter; + if (like.isCaseSensitive()) { + sh.addParameterValue(like.getValue()); + return QueryBuilder.quote(like.getPropertyId()) + " LIKE ?"; + } else { + sh.addParameterValue(like.getValue().toUpperCase()); + return "UPPER(" + QueryBuilder.quote(like.getPropertyId()) + + ") LIKE ?"; + } + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java new file mode 100644 index 0000000000..fe98ca24b6 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.filter.Not; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class NotTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Not; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + Not not = (Not) filter; + if (not.getFilter() instanceof IsNull) { + IsNull in = (IsNull) not.getFilter(); + return QueryBuilder.quote(in.getPropertyId()) + " IS NOT NULL"; + } + return "NOT " + + QueryBuilder.getWhereStringForFilter(not.getFilter(), sh); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java new file mode 100644 index 0000000000..2f30acc89f --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class OrTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof Or; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + return QueryBuilder.group(QueryBuilder + .getJoinedFilterString(((Or) filter).getFilters(), "OR", sh)); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java new file mode 100644 index 0000000000..b8fb306076 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class QueryBuilder implements Serializable { + + private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>(); + private static StringDecorator stringDecorator = new StringDecorator("\"", + "\""); + + static { + /* Register all default filter translators */ + addFilterTranslator(new AndTranslator()); + addFilterTranslator(new OrTranslator()); + addFilterTranslator(new LikeTranslator()); + addFilterTranslator(new BetweenTranslator()); + addFilterTranslator(new CompareTranslator()); + addFilterTranslator(new NotTranslator()); + addFilterTranslator(new IsNullTranslator()); + addFilterTranslator(new SimpleStringTranslator()); + } + + public synchronized static void addFilterTranslator( + FilterTranslator translator) { + filterTranslators.add(translator); + } + + /** + * Allows specification of a custom ColumnQuoter instance that handles + * quoting of column names for the current DB dialect. + * + * @param decorator + * the ColumnQuoter instance to use. + */ + public static void setStringDecorator(StringDecorator decorator) { + stringDecorator = decorator; + } + + public static String quote(Object str) { + return stringDecorator.quote(str); + } + + public static String group(String str) { + return stringDecorator.group(str); + } + + /** + * Constructs and returns a string representing the filter that can be used + * in a WHERE clause. + * + * @param filter + * the filter to translate + * @param sh + * the statement helper to update with the value(s) of the filter + * @return a string representing the filter. + */ + public synchronized static String getWhereStringForFilter(Filter filter, + StatementHelper sh) { + for (FilterTranslator ft : filterTranslators) { + if (ft.translatesFilter(filter)) { + return ft.getWhereStringForFilter(filter, sh); + } + } + return ""; + } + + public static String getJoinedFilterString(Collection<Filter> filters, + String joinString, StatementHelper sh) { + StringBuilder result = new StringBuilder(); + for (Filter f : filters) { + result.append(getWhereStringForFilter(f, sh)); + result.append(" ").append(joinString).append(" "); + } + // Remove the last instance of joinString + result.delete(result.length() - joinString.length() - 2, + result.length()); + return result.toString(); + } + + public static String getWhereStringForFilters(List<Filter> filters, + StatementHelper sh) { + if (filters == null || filters.isEmpty()) { + return ""; + } + StringBuilder where = new StringBuilder(" WHERE "); + where.append(getJoinedFilterString(filters, "AND", sh)); + return where.toString(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java new file mode 100644 index 0000000000..312adc5ed7 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class SimpleStringTranslator implements FilterTranslator { + + @Override + public boolean translatesFilter(Filter filter) { + return filter instanceof SimpleStringFilter; + } + + @Override + public String getWhereStringForFilter(Filter filter, StatementHelper sh) { + SimpleStringFilter ssf = (SimpleStringFilter) filter; + // Create a Like filter based on the SimpleStringFilter and execute the + // LikeTranslator + String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%" + : "%" + ssf.getFilterString() + "%"; + Like like = new Like(ssf.getPropertyId().toString(), likeStr); + like.setCaseSensitive(!ssf.isIgnoreCase()); + return new LikeTranslator().getWhereStringForFilter(like, sh); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java new file mode 100644 index 0000000000..f8005f4290 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2016 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.data.util.sqlcontainer.query.generator.filter; + +import java.io.Serializable; + +/** + * The StringDecorator knows how to produce a quoted string using the specified + * quote start and quote end characters. It also handles grouping of a string + * (surrounding it in parenthesis). + * + * Extend this class if you need to support special characters for grouping + * (parenthesis). + * + * @author Vaadin Ltd + */ +public class StringDecorator implements Serializable { + + private final String quoteStart; + private final String quoteEnd; + + /** + * Constructs a StringDecorator that uses the quoteStart and quoteEnd + * characters to create quoted strings. + * + * @param quoteStart + * the character denoting the start of a quote. + * @param quoteEnd + * the character denoting the end of a quote. + */ + public StringDecorator(String quoteStart, String quoteEnd) { + this.quoteStart = quoteStart; + this.quoteEnd = quoteEnd; + } + + /** + * Surround a string with quote characters. + * + * @param str + * the string to quote + * @return the quoted string + */ + public String quote(Object str) { + return quoteStart + str + quoteEnd; + } + + /** + * Groups a string by surrounding it in parenthesis + * + * @param str + * the string to group + * @return the grouped string + */ + public String group(String str) { + return "(" + str + ")"; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/AbstractColorPicker.java b/compatibility-server/src/main/java/com/vaadin/ui/AbstractColorPicker.java new file mode 100644 index 0000000000..d7d12c6d03 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/AbstractColorPicker.java @@ -0,0 +1,588 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collection; + +import org.jsoup.nodes.Attributes; +import org.jsoup.nodes.Element; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc; +import com.vaadin.shared.ui.colorpicker.ColorPickerState; +import com.vaadin.ui.Window.CloseEvent; +import com.vaadin.ui.Window.CloseListener; +import com.vaadin.ui.components.colorpicker.ColorChangeEvent; +import com.vaadin.ui.components.colorpicker.ColorChangeListener; +import com.vaadin.ui.components.colorpicker.ColorPickerPopup; +import com.vaadin.ui.components.colorpicker.ColorSelector; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; + +/** + * An abstract class that defines default implementation for a color picker + * component. + * + * @since 7.0.0 + */ +public abstract class AbstractColorPicker extends AbstractComponent + implements CloseListener, ColorSelector { + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + /** + * Interface for converting 2d-coordinates to a Color + */ + public interface Coordinates2Color extends Serializable { + + /** + * Calculate color from coordinates + * + * @param x + * the x-coordinate + * @param y + * the y-coordinate + * + * @return the color + */ + public Color calculate(int x, int y); + + /** + * Calculate coordinates from color + * + * @param c + * the c + * + * @return the integer array with the coordinates + */ + public int[] calculate(Color c); + } + + public enum PopupStyle { + POPUP_NORMAL("normal"), POPUP_SIMPLE("simple"); + + private String style; + + PopupStyle(String styleName) { + style = styleName; + } + + @Override + public String toString() { + return style; + } + } + + private ColorPickerServerRpc rpc = new ColorPickerServerRpc() { + + @Override + public void openPopup(boolean open) { + showPopup(open); + } + }; + + protected static final String STYLENAME_DEFAULT = "v-colorpicker"; + protected static final String STYLENAME_BUTTON = "v-button"; + protected static final String STYLENAME_AREA = "v-colorpicker-area"; + + protected PopupStyle popupStyle = PopupStyle.POPUP_NORMAL; + + /** The popup window. */ + private ColorPickerPopup window; + + /** The color. */ + protected Color color; + + /** The UI. */ + private UI parent; + + protected String popupCaption = null; + private int positionX = 0; + private int positionY = 0; + + protected boolean rgbVisible = true; + protected boolean hsvVisible = true; + protected boolean swatchesVisible = true; + protected boolean historyVisible = true; + protected boolean textfieldVisible = true; + + /** + * Instantiates a new color picker. + */ + public AbstractColorPicker() { + this("Colors", Color.WHITE); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * the caption of the popup window + */ + public AbstractColorPicker(String popupCaption) { + this(popupCaption, Color.WHITE); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * the caption of the popup window + * @param initialColor + * the initial color + */ + public AbstractColorPicker(String popupCaption, Color initialColor) { + super(); + registerRpc(rpc); + setColor(initialColor); + this.popupCaption = popupCaption; + setDefaultStyles(); + setCaption(""); + } + + @Override + public void setColor(Color color) { + this.color = color; + + if (window != null) { + window.setColor(color); + } + getState().color = color.getCSS(); + } + + @Override + public Color getColor() { + return color; + } + + /** + * Set true if the component should show a default caption (css-code for the + * currently selected color, e.g. #ffffff) when no other caption is + * available. + * + * @param enabled + */ + public void setDefaultCaptionEnabled(boolean enabled) { + getState().showDefaultCaption = enabled; + } + + /** + * Returns true if the component shows the default caption (css-code for the + * currently selected color, e.g. #ffffff) if no other caption is available. + */ + public boolean isDefaultCaptionEnabled() { + return getState(false).showDefaultCaption; + } + + /** + * Sets the position of the popup window + * + * @param x + * the x-coordinate + * @param y + * the y-coordinate + */ + public void setPosition(int x, int y) { + positionX = x; + positionY = y; + + if (window != null) { + window.setPositionX(x); + window.setPositionY(y); + } + } + + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + @Override + public void windowClose(CloseEvent e) { + if (e.getWindow() == window) { + getState().popupVisible = false; + } + } + + /** + * Fired when a color change event occurs + * + * @param event + * The color change event + */ + protected void colorChanged(ColorChangeEvent event) { + setColor(event.getColor()); + fireColorChanged(); + } + + /** + * Notifies the listeners that the selected color has changed + */ + public void fireColorChanged() { + fireEvent(new ColorChangeEvent(this, color)); + } + + /** + * The style for the popup window + * + * @param style + * The style + */ + public void setPopupStyle(PopupStyle style) { + popupStyle = style; + + switch (style) { + case POPUP_NORMAL: { + setRGBVisibility(true); + setHSVVisibility(true); + setSwatchesVisibility(true); + setHistoryVisibility(true); + setTextfieldVisibility(true); + break; + } + + case POPUP_SIMPLE: { + setRGBVisibility(false); + setHSVVisibility(false); + setSwatchesVisibility(true); + setHistoryVisibility(false); + setTextfieldVisibility(false); + break; + } + } + } + + /** + * Gets the style for the popup window + * + * @since 7.5.0 + * @return popup window style + */ + public PopupStyle getPopupStyle() { + return popupStyle; + } + + /** + * Set the visibility of the RGB Tab + * + * @param visible + * The visibility + */ + public void setRGBVisibility(boolean visible) { + + if (!visible && !hsvVisible && !swatchesVisible) { + throw new IllegalArgumentException("Cannot hide all tabs."); + } + + rgbVisible = visible; + if (window != null) { + window.setRGBTabVisible(visible); + } + } + + /** + * Gets the visibility of the RGB Tab + * + * @since 7.5.0 + * @return visibility of the RGB tab + */ + public boolean getRGBVisibility() { + return rgbVisible; + } + + /** + * Set the visibility of the HSV Tab + * + * @param visible + * The visibility + */ + public void setHSVVisibility(boolean visible) { + if (!visible && !rgbVisible && !swatchesVisible) { + throw new IllegalArgumentException("Cannot hide all tabs."); + } + + hsvVisible = visible; + if (window != null) { + window.setHSVTabVisible(visible); + } + } + + /** + * Gets the visibility of the HSV Tab + * + * @since 7.5.0 + * @return visibility of the HSV tab + */ + public boolean getHSVVisibility() { + return hsvVisible; + } + + /** + * Set the visibility of the Swatches Tab + * + * @param visible + * The visibility + */ + public void setSwatchesVisibility(boolean visible) { + if (!visible && !hsvVisible && !rgbVisible) { + throw new IllegalArgumentException("Cannot hide all tabs."); + } + + swatchesVisible = visible; + if (window != null) { + window.setSwatchesTabVisible(visible); + } + } + + /** + * Gets the visibility of the Swatches Tab + * + * @since 7.5.0 + * @return visibility of the swatches tab + */ + public boolean getSwatchesVisibility() { + return swatchesVisible; + } + + /** + * Sets the visibility of the Color History + * + * @param visible + * The visibility + */ + public void setHistoryVisibility(boolean visible) { + historyVisible = visible; + if (window != null) { + window.setHistoryVisible(visible); + } + } + + /** + * Gets the visibility of the Color History + * + * @since 7.5.0 + * @return visibility of color history + */ + public boolean getHistoryVisibility() { + return historyVisible; + } + + /** + * Sets the visibility of the CSS color code text field + * + * @param visible + * The visibility + */ + public void setTextfieldVisibility(boolean visible) { + textfieldVisible = visible; + if (window != null) { + window.setPreviewVisible(visible); + } + } + + /** + * Gets the visibility of CSS color code text field + * + * @since 7.5.0 + * @return visibility of css color code text field + */ + public boolean getTextfieldVisibility() { + return textfieldVisible; + } + + @Override + protected ColorPickerState getState() { + return (ColorPickerState) super.getState(); + } + + @Override + protected ColorPickerState getState(boolean markAsDirty) { + return (ColorPickerState) super.getState(markAsDirty); + } + + /** + * Sets the default styles of the component + * + */ + abstract protected void setDefaultStyles(); + + /** + * Shows a popup-window for color selection. + */ + public void showPopup() { + showPopup(true); + } + + /** + * Hides a popup-window for color selection. + */ + public void hidePopup() { + showPopup(false); + } + + /** + * Shows or hides popup-window depending on the given parameter. If there is + * no such window yet, one is created. + * + * @param open + */ + protected void showPopup(boolean open) { + if (open && !isReadOnly()) { + if (parent == null) { + parent = getUI(); + } + + if (window == null) { + + // Create the popup + window = new ColorPickerPopup(color); + window.setCaption(popupCaption); + + window.setRGBTabVisible(rgbVisible); + window.setHSVTabVisible(hsvVisible); + window.setSwatchesTabVisible(swatchesVisible); + window.setHistoryVisible(historyVisible); + window.setPreviewVisible(textfieldVisible); + + window.setImmediate(true); + window.addCloseListener(this); + window.addColorChangeListener(new ColorChangeListener() { + @Override + public void colorChanged(ColorChangeEvent event) { + AbstractColorPicker.this.colorChanged(event); + } + }); + + window.getHistory().setColor(color); + parent.addWindow(window); + window.setVisible(true); + window.setPositionX(positionX); + window.setPositionY(positionY); + + } else if (!parent.equals(window.getParent())) { + + window.setRGBTabVisible(rgbVisible); + window.setHSVTabVisible(hsvVisible); + window.setSwatchesTabVisible(swatchesVisible); + window.setHistoryVisible(historyVisible); + window.setPreviewVisible(textfieldVisible); + + window.setColor(color); + window.getHistory().setColor(color); + window.setVisible(true); + parent.addWindow(window); + } + + } else if (window != null) { + window.setVisible(false); + parent.removeWindow(window); + } + getState().popupVisible = open; + } + + /** + * Set whether the caption text is rendered as HTML or not. You might need + * to re-theme component to allow higher content than the original text + * style. + * + * If set to true, the captions are passed to the browser as html and the + * developer is responsible for ensuring no harmful html is used. If set to + * false, the content is passed to the browser as plain text. + * + * @param htmlContentAllowed + * <code>true</code> if caption is rendered as HTML, + * <code>false</code> otherwise + * @deprecated as of , use {@link #setCaptionAsHtml(boolean)} instead + */ + @Deprecated + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + setCaptionAsHtml(htmlContentAllowed); + } + + /** + * Return HTML rendering setting + * + * @return <code>true</code> if the caption text is to be rendered as HTML, + * <code>false</code> otherwise + * @deprecated as of , use {@link #isCaptionAsHtml()} instead + */ + @Deprecated + public boolean isHtmlContentAllowed() { + return isCaptionAsHtml(); + } + + @Override + public void readDesign(Element design, DesignContext designContext) { + super.readDesign(design, designContext); + + Attributes attributes = design.attributes(); + if (design.hasAttr("color")) { + // Ignore the # character + String hexColor = DesignAttributeHandler + .readAttribute("color", attributes, String.class) + .substring(1); + setColor(new Color(Integer.parseInt(hexColor, 16))); + } + if (design.hasAttr("popup-style")) { + setPopupStyle(PopupStyle.valueOf( + "POPUP_" + attributes.get("popup-style").toUpperCase())); + } + if (design.hasAttr("position")) { + String[] position = attributes.get("position").split(","); + setPosition(Integer.parseInt(position[0]), + Integer.parseInt(position[1])); + } + } + + @Override + public void writeDesign(Element design, DesignContext designContext) { + super.writeDesign(design, designContext); + + Attributes attribute = design.attributes(); + DesignAttributeHandler.writeAttribute("color", attribute, + color.getCSS(), Color.WHITE.getCSS(), String.class); + DesignAttributeHandler.writeAttribute("popup-style", attribute, + (popupStyle == PopupStyle.POPUP_NORMAL ? "normal" : "simple"), + "normal", String.class); + DesignAttributeHandler.writeAttribute("position", attribute, + positionX + "," + positionY, "0,0", String.class); + } + + @Override + protected Collection<String> getCustomAttributes() { + Collection<String> result = super.getCustomAttributes(); + result.add("color"); + result.add("position"); + result.add("popup-style"); + return result; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/AbstractSelect.java b/compatibility-server/src/main/java/com/vaadin/ui/AbstractSelect.java new file mode 100644 index 0000000000..57c2fc1046 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/AbstractSelect.java @@ -0,0 +1,2353 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EventObject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jsoup.nodes.Element; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion; +import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor; +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.Resource; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.combobox.FilteringMode; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.select.AbstractSelectState; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; +import com.vaadin.ui.declarative.DesignFormatter; +import com.vaadin.v7.data.Validator.InvalidValueException; +import com.vaadin.v7.data.util.converter.LegacyConverter; +import com.vaadin.v7.data.util.converter.LegacyConverterUtil; +import com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException; +import com.vaadin.v7.ui.LegacyAbstractField; + +/** + * <p> + * A class representing a selection of items the user has selected in a UI. The + * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a + * {@link com.vaadin.data.Container}. + * </p> + * + * <p> + * A <code>Select</code> component may be in single- or multiselect mode. + * Multiselect mode means that more than one item can be selected + * simultaneously. + * </p> + * + * @author Vaadin Ltd. + * @since 5.0 + */ +@SuppressWarnings("serial") +// TODO currently cannot specify type more precisely in case of multi-select +public abstract class AbstractSelect extends LegacyAbstractField<Object> + implements Container, Container.Viewer, + Container.PropertySetChangeListener, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier, + Container.ItemSetChangeListener, LegacyComponent { + + public enum ItemCaptionMode { + /** + * Item caption mode: Item's ID converted to a String using + * {@link VaadinSession#getConverterFactory()} is used as caption. + */ + ID, + /** + * Item caption mode: Item's ID's <code>String</code> representation is + * used as caption. + * + * @since 7.5.6 + */ + ID_TOSTRING, + /** + * Item caption mode: Item's <code>String</code> representation is used + * as caption. + */ + ITEM, + /** + * Item caption mode: Index of the item is used as caption. The index + * mode can only be used with the containers implementing the + * {@link com.vaadin.data.Container.Indexed} interface. + */ + INDEX, + /** + * Item caption mode: If an Item has a caption it's used, if not, Item's + * ID converted to a String using + * {@link VaadinSession#getConverterFactory()} is used as caption. + * <b>This is the default</b>. + */ + EXPLICIT_DEFAULTS_ID, + /** + * Item caption mode: Captions must be explicitly specified. + */ + EXPLICIT, + /** + * Item caption mode: Only icons are shown, captions are hidden. + */ + ICON_ONLY, + /** + * Item caption mode: Item captions are read from property specified + * with <code>setItemCaptionPropertyId</code>. + */ + PROPERTY; + } + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#ITEM} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#INDEX} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID} + * instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#ICON_ONLY} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY; + + /** + * @deprecated As of 7.0, use {@link ItemCaptionMode#PROPERTY} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_PROPERTY = ItemCaptionMode.PROPERTY; + + /** + * Interface for option filtering, used to filter options based on user + * entered value. The value is matched to the item caption. + * <code>FilteringMode.OFF</code> (0) turns the filtering off. + * <code>FilteringMode.STARTSWITH</code> (1) matches from the start of the + * caption. <code>FilteringMode.CONTAINS</code> (1) matches anywhere in the + * caption. + */ + public interface Filtering extends Serializable { + + /** + * @deprecated As of 7.0, use {@link FilteringMode#OFF} instead + */ + @Deprecated + public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF; + /** + * @deprecated As of 7.0, use {@link FilteringMode#STARTSWITH} instead + */ + @Deprecated + public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH; + /** + * @deprecated As of 7.0, use {@link FilteringMode#CONTAINS} instead + */ + @Deprecated + public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS; + + /** + * Sets the option filtering mode. + * + * @param filteringMode + * the filtering mode to use + */ + public void setFilteringMode(FilteringMode filteringMode); + + /** + * Gets the current filtering mode. + * + * @return the filtering mode in use + */ + public FilteringMode getFilteringMode(); + + } + + /** + * Select options. + */ + protected Container items; + + /** + * Is the user allowed to add new options? + */ + private boolean allowNewOptions; + + /** + * Keymapper used to map key values. + */ + protected KeyMapper<Object> itemIdMapper = new KeyMapper<Object>(); + + /** + * Item icons. + */ + private final HashMap<Object, Resource> itemIcons = new HashMap<Object, Resource>(); + + /** + * Item captions. + */ + private final HashMap<Object, String> itemCaptions = new HashMap<Object, String>(); + + /** + * Item caption mode. + */ + private ItemCaptionMode itemCaptionMode = ItemCaptionMode.EXPLICIT_DEFAULTS_ID; + + /** + * Item caption source property id. + */ + private Object itemCaptionPropertyId = null; + + /** + * Item icon source property id. + */ + private Object itemIconPropertyId = null; + + /** + * List of property set change event listeners. + */ + private Set<Container.PropertySetChangeListener> propertySetEventListeners = null; + + /** + * List of item set change event listeners. + */ + private Set<Container.ItemSetChangeListener> itemSetEventListeners = null; + + /** + * Item id that represents null selection of this select. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + */ + private Object nullSelectionItemId = null; + + // Null (empty) selection is enabled by default + private boolean nullSelectionAllowed = true; + private NewItemHandler newItemHandler; + + // Caption (Item / Property) change listeners + CaptionChangeListener captionChangeListener; + + /* Constructors */ + + /** + * Creates an empty Select. The caption is not used. + */ + public AbstractSelect() { + setContainerDataSource(new IndexedContainer()); + } + + /** + * Creates an empty Select with caption. + */ + public AbstractSelect(String caption) { + setContainerDataSource(new IndexedContainer()); + setCaption(caption); + } + + /** + * Creates a new select that is connected to a data-source. + * + * @param caption + * the Caption of the component. + * @param dataSource + * the Container datasource to be selected from by this select. + */ + public AbstractSelect(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /** + * Creates a new select that is filled from a collection of option values. + * + * @param caption + * the Caption of this field. + * @param options + * the Collection containing the options. + */ + public AbstractSelect(String caption, Collection<?> options) { + + // Creates the options container and add given options to it + final Container c = new IndexedContainer(); + if (options != null) { + for (final Iterator<?> i = options.iterator(); i.hasNext();) { + c.addItem(i.next()); + } + } + + setCaption(caption); + setContainerDataSource(c); + } + + /* Component methods */ + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Paints select attributes + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + if (getNullSelectionItemId() != null) { + target.addAttribute("nullselectitem", true); + } + } + + // Constructs selected keys array + String[] selectedKeys; + if (isMultiSelect()) { + selectedKeys = new String[((Set<?>) getValue()).size()]; + } else { + selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + } + + // == + // first remove all previous item/property listeners + getCaptionChangeListener().clear(); + // Paints the options and create array of selected id keys + + target.startTag("options"); + int keyIndex = 0; + // Support for external null selection item id + final Collection<?> ids = getItemIds(); + if (isNullSelectionAllowed() && getNullSelectionItemId() != null + && !ids.contains(getNullSelectionItemId())) { + final Object id = getNullSelectionItemId(); + // Paints option + target.startTag("so"); + paintItem(target, id); + if (isSelected(id)) { + selectedKeys[keyIndex++] = itemIdMapper.key(id); + } + target.endTag("so"); + } + + final Iterator<?> i = getItemIds().iterator(); + // Paints the available selection options from data source + while (i.hasNext()) { + // Gets the option attribute values + final Object id = i.next(); + if (!isNullSelectionAllowed() && id != null + && id.equals(getNullSelectionItemId())) { + // Remove item if it's the null selection item but null + // selection is not allowed + continue; + } + final String key = itemIdMapper.key(id); + // add listener for each item, to cause repaint if an item changes + getCaptionChangeListener().addNotifierForItem(id); + target.startTag("so"); + paintItem(target, id); + if (isSelected(id) && keyIndex < selectedKeys.length) { + selectedKeys[keyIndex++] = key; + } + target.endTag("so"); + } + target.endTag("options"); + // == + + // Paint variables + target.addVariable(this, "selected", selectedKeys); + if (isNewItemsAllowed()) { + target.addVariable(this, "newitem", ""); + } + + } + + protected void paintItem(PaintTarget target, Object itemId) + throws PaintException { + final String key = itemIdMapper.key(itemId); + final String caption = getItemCaption(itemId); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + if (itemId != null && itemId.equals(getNullSelectionItemId())) { + target.addAttribute("nullselection", true); + } + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + } + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + // New option entered (and it is allowed) + if (isNewItemsAllowed()) { + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + } + } + + // Selection change + if (variables.containsKey("selected")) { + final String[] clientSideSelectedKeys = (String[]) variables + .get("selected"); + + // Multiselect mode + if (isMultiSelect()) { + + // TODO Optimize by adding repaintNotNeeded when applicable + + // Converts the key-array to id-set + final LinkedList<Object> acceptedSelections = new LinkedList<Object>(); + for (int i = 0; i < clientSideSelectedKeys.length; i++) { + final Object id = itemIdMapper + .get(clientSideSelectedKeys[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + markAsDirty(); + } else if (id != null && containsId(id)) { + acceptedSelections.add(id); + } + } + + if (!isNullSelectionAllowed() + && acceptedSelections.size() < 1) { + // empty selection not allowed, keep old value + markAsDirty(); + return; + } + + // Limits the deselection to the set of visible items + // (non-visible items can not be deselected) + Collection<?> visibleNotSelected = getVisibleItemIds(); + if (visibleNotSelected != null) { + visibleNotSelected = new HashSet<Object>( + visibleNotSelected); + // Don't remove those that will be added to preserve order + visibleNotSelected.removeAll(acceptedSelections); + + @SuppressWarnings("unchecked") + Set<Object> newsel = (Set<Object>) getValue(); + if (newsel == null) { + newsel = new LinkedHashSet<Object>(); + } else { + newsel = new LinkedHashSet<Object>(newsel); + } + newsel.removeAll(visibleNotSelected); + newsel.addAll(acceptedSelections); + setValue(newsel, true); + } + } else { + // Single select mode + if (!isNullSelectionAllowed() + && (clientSideSelectedKeys.length == 0 + || clientSideSelectedKeys[0] == null + || clientSideSelectedKeys[0] == getNullSelectionItemId())) { + markAsDirty(); + return; + } + if (clientSideSelectedKeys.length == 0) { + // Allows deselection only if the deselected item is + // visible + final Object current = getValue(); + final Collection<?> visible = getVisibleItemIds(); + if (visible != null && visible.contains(current)) { + setValue(null, true); + } + } else { + String clientSelectedKey = clientSideSelectedKeys[0]; + if ("null".equals(clientSelectedKey) + || itemIdMapper.containsKey(clientSelectedKey)) { + // Happens to work for nullselection + // (get ("null") -> null)) + final Object id = itemIdMapper.get(clientSelectedKey); + + if (!isNullSelectionAllowed() && id == null) { + markAsDirty(); + } else if (id != null + && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + } + } + } + + /** + * TODO refine doc Setter for new item handler that is called when user adds + * new item in newItemAllowed mode. + * + * @param newItemHandler + */ + public void setNewItemHandler(NewItemHandler newItemHandler) { + this.newItemHandler = newItemHandler; + } + + /** + * TODO refine doc + * + * @return + */ + public NewItemHandler getNewItemHandler() { + if (newItemHandler == null) { + newItemHandler = new DefaultNewItemHandler(); + } + return newItemHandler; + } + + public interface NewItemHandler extends Serializable { + void addNewItem(String newItemCaption); + } + + /** + * TODO refine doc + * + * This is a default class that handles adding new items that are typed by + * user to selects container. + * + * By extending this class one may implement some logic on new item addition + * like database inserts. + * + */ + public class DefaultNewItemHandler implements NewItemHandler { + @Override + public void addNewItem(String newItemCaption) { + // Checks for readonly + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Adds new option + if (addItem(newItemCaption) != null) { + + // Sets the caption property, if used + if (getItemCaptionPropertyId() != null) { + getContainerProperty(newItemCaption, + getItemCaptionPropertyId()) + .setValue(newItemCaption); + } + if (isMultiSelect()) { + Set values = new HashSet((Collection) getValue()); + values.add(newItemCaption); + setValue(values); + } else { + setValue(newItemCaption); + } + } + } + } + + /** + * Gets the visible item ids. In Select, this returns list of all item ids, + * but can be overriden in subclasses if they paint only part of the items + * to the terminal or null if no items is visible. + */ + public Collection<?> getVisibleItemIds() { + return getItemIds(); + } + + /* Property methods */ + + /** + * Returns the type of the property. <code>getValue</code> and + * <code>setValue</code> methods must be compatible with this type: one can + * safely cast <code>getValue</code> to given type and pass any variable + * assignable to this type as a parameter to <code>setValue</code>. + * + * @return the Type of the property. + */ + @Override + public Class<?> getType() { + if (isMultiSelect()) { + return Set.class; + } else { + return Object.class; + } + } + + /** + * Gets the selected item id or in multiselect mode a set of selected ids. + * + * @see com.vaadin.v7.ui.LegacyAbstractField#getValue() + */ + @Override + public Object getValue() { + final Object retValue = super.getValue(); + + if (isMultiSelect()) { + + // If the return value is not a set + if (retValue == null) { + return new HashSet<Object>(); + } + if (retValue instanceof Set) { + return Collections.unmodifiableSet((Set<?>) retValue); + } else if (retValue instanceof Collection) { + return new HashSet<Object>((Collection<?>) retValue); + } else { + final Set<Object> s = new HashSet<Object>(); + if (items.containsId(retValue)) { + s.add(retValue); + } + return s; + } + + } else { + return retValue; + } + } + + /** + * Sets the visible value of the property. + * + * <p> + * The value of the select is the selected item id. If the select is in + * multiselect-mode, the value is a set of selected item keys. In + * multiselect mode all collections of id:s can be assigned. + * </p> + * + * @param newValue + * the New selected item or collection of selected items. + * @see com.vaadin.v7.ui.LegacyAbstractField#setValue(java.lang.Object) + */ + @Override + public void setValue(Object newValue) throws Property.ReadOnlyException { + if (newValue == getNullSelectionItemId()) { + newValue = null; + } + + setValue(newValue, false); + } + + /** + * Sets the visible value of the property. + * + * <p> + * The value of the select is the selected item id. If the select is in + * multiselect-mode, the value is a set of selected item keys. In + * multiselect mode all collections of id:s can be assigned. + * </p> + * + * @since 7.5.7 + * @param newValue + * the New selected item or collection of selected items. + * @param repaintIsNotNeeded + * True if caller is sure that repaint is not needed. + * @param ignoreReadOnly + * True if read-only check should be omitted. + * @see com.vaadin.v7.ui.LegacyAbstractField#setValue(java.lang.Object, + * java.lang.Boolean) + */ + @Override + protected void setValue(Object newFieldValue, boolean repaintIsNotNeeded, + boolean ignoreReadOnly) + throws com.vaadin.data.Property.ReadOnlyException, + ConversionException, InvalidValueException { + if (isMultiSelect()) { + if (newFieldValue == null) { + super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded, + ignoreReadOnly); + } else if (Collection.class + .isAssignableFrom(newFieldValue.getClass())) { + super.setValue( + new LinkedHashSet<Object>( + (Collection<?>) newFieldValue), + repaintIsNotNeeded, ignoreReadOnly); + } + } else if (newFieldValue == null || items.containsId(newFieldValue)) { + super.setValue(newFieldValue, repaintIsNotNeeded, ignoreReadOnly); + } + } + + /* Container methods */ + + /** + * Gets the item from the container with given id. If the container does not + * contain the requested item, null is returned. + * + * @param itemId + * the item id. + * @return the item from the container. + */ + @Override + public Item getItem(Object itemId) { + return items.getItem(itemId); + } + + /** + * Gets the item Id collection from the container. + * + * @return the Collection of item ids. + */ + @Override + public Collection<?> getItemIds() { + return items.getItemIds(); + } + + /** + * Gets the property Id collection from the container. + * + * @return the Collection of property ids. + */ + @Override + public Collection<?> getContainerPropertyIds() { + return items.getContainerPropertyIds(); + } + + /** + * Gets the property type. + * + * @param propertyId + * the Id identifying the property. + * @see com.vaadin.data.Container#getType(java.lang.Object) + */ + @Override + public Class<?> getType(Object propertyId) { + return items.getType(propertyId); + } + + /* + * Gets the number of items in the container. + * + * @return the Number of items in the container. + * + * @see com.vaadin.data.Container#size() + */ + @Override + public int size() { + int size = items.size(); + assert size >= 0; + return size; + } + + /** + * Tests, if the collection contains an item with given id. + * + * @param itemId + * the Id the of item to be tested. + */ + @Override + public boolean containsId(Object itemId) { + if (itemId != null) { + return items.containsId(itemId); + } else { + return false; + } + } + + /** + * Gets the Property identified by the given itemId and propertyId from the + * Container + * + * @see com.vaadin.data.Container#getContainerProperty(Object, Object) + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + return items.getContainerProperty(itemId, propertyId); + } + + /** + * Adds the new property to all items. Adds a property with given id, type + * and default value to all items in the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object, + * java.lang.Class, java.lang.Object) + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + + final boolean retval = items.addContainerProperty(propertyId, type, + defaultValue); + if (retval && !(items instanceof Container.PropertySetChangeNotifier)) { + firePropertySetChange(); + } + return retval; + } + + /** + * Removes all items from the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#removeAllItems() + */ + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + + final boolean retval = items.removeAllItems(); + itemIdMapper.removeAll(); + if (retval) { + setValue(null); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + } + return retval; + } + + /** + * Creates a new item into container with container managed id. The id of + * the created new item is returned. The item can be fetched with getItem() + * method. if the creation fails, null is returned. + * + * @return the Id of the created item or null in case of failure. + * @see com.vaadin.data.Container#addItem() + */ + @Override + public Object addItem() throws UnsupportedOperationException { + + final Object retval = items.addItem(); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Create a new item into container. The created new item is returned and + * ready for setting property values. if the creation fails, null is + * returned. In case the container already contains the item, null is + * returned. + * + * This functionality is optional. If the function is unsupported, it always + * returns null. + * + * @param itemId + * the Identification of the item to be created. + * @return the Created item with the given id, or null in case of failure. + * @see com.vaadin.data.Container#addItem(java.lang.Object) + */ + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + + final Item retval = items.addItem(itemId); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Adds given items with given item ids to container. + * + * @since 7.2 + * @param itemId + * item identifiers to be added to underlying container + * @throws UnsupportedOperationException + * if the underlying container don't support adding items with + * identifiers + */ + public void addItems(Object... itemId) + throws UnsupportedOperationException { + for (Object id : itemId) { + addItem(id); + } + } + + /** + * Adds given items with given item ids to container. + * + * @since 7.2 + * @param itemIds + * item identifiers to be added to underlying container + * @throws UnsupportedOperationException + * if the underlying container don't support adding items with + * identifiers + */ + public void addItems(Collection<?> itemIds) + throws UnsupportedOperationException { + addItems(itemIds.toArray()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + unselect(itemId); + final boolean retval = items.removeItem(itemId); + itemIdMapper.remove(itemId); + if (retval && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Checks that the current selection is valid, i.e. the selected item ids + * exist in the container. Updates the selection if one or several selected + * item ids are no longer available in the container. + */ + @SuppressWarnings("unchecked") + public void sanitizeSelection() { + Object value = getValue(); + if (value == null) { + return; + } + + boolean changed = false; + + if (isMultiSelect()) { + Collection<Object> valueAsCollection = (Collection<Object>) value; + List<Object> newSelection = new ArrayList<Object>( + valueAsCollection.size()); + for (Object subValue : valueAsCollection) { + if (containsId(subValue)) { + newSelection.add(subValue); + } else { + changed = true; + } + } + if (changed) { + setValue(newSelection); + } + } else { + if (!containsId(value)) { + setValue(null); + } + } + + } + + /** + * Removes the property from all items. Removes a property with given id + * from all the items in the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object) + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + + final boolean retval = items.removeContainerProperty(propertyId); + if (retval && !(items instanceof Container.PropertySetChangeNotifier)) { + firePropertySetChange(); + } + return retval; + } + + /* Container.Viewer methods */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * As a side-effect the fields value (selection) is set to null due old + * selection not necessary exists in new Container. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + * + * @param newDataSource + * the new data source. + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + getCaptionChangeListener().clear(); + + if (items != newDataSource) { + + // Removes listeners from the old datasource + if (items != null) { + if (items instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) items) + .removeItemSetChangeListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .removePropertySetChangeListener(this); + } + } + + // Assigns new data source + items = newDataSource; + + // Clears itemIdMapper also + itemIdMapper.removeAll(); + + // Adds listeners + if (items != null) { + if (items instanceof Container.ItemSetChangeNotifier) { + ((Container.ItemSetChangeNotifier) items) + .addItemSetChangeListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .addPropertySetChangeListener(this); + } + } + + /* + * We expect changing the data source should also clean value. See + * #810, #4607, #5281 + */ + setValue(null); + + markAsDirty(); + + } + } + + /** + * Gets the viewing data-source container. + * + * @see com.vaadin.data.Container.Viewer#getContainerDataSource() + */ + @Override + public Container getContainerDataSource() { + return items; + } + + /* Select attributes */ + + /** + * Is the select in multiselect mode? In multiselect mode + * + * @return the Value of property multiSelect. + */ + public boolean isMultiSelect() { + return getState(false).multiSelect; + } + + /** + * Sets the multiselect mode. Setting multiselect mode false may lose + * selection information: if selected items set contains one or more + * selected items, only one of the selected items is kept as selected. + * + * Subclasses of AbstractSelect can choose not to support changing the + * multiselect mode, and may throw {@link UnsupportedOperationException}. + * + * @param multiSelect + * the New value of property multiSelect. + */ + public void setMultiSelect(boolean multiSelect) { + if (multiSelect && getNullSelectionItemId() != null) { + throw new IllegalStateException( + "Multiselect and NullSelectionItemId can not be set at the same time."); + } + if (multiSelect != getState(false).multiSelect) { + + // Selection before mode change + final Object oldValue = getValue(); + + getState().multiSelect = multiSelect; + + // Convert the value type + if (multiSelect) { + final Set<Object> s = new HashSet<Object>(); + if (oldValue != null) { + s.add(oldValue); + } + setValue(s); + } else { + final Set<?> s = (Set<?>) oldValue; + if (s == null || s.isEmpty()) { + setValue(null); + } else { + // Set the single select to contain only the first + // selected value in the multiselect + setValue(s.iterator().next()); + } + } + + markAsDirty(); + } + } + + /** + * Does the select allow adding new options by the user. If true, the new + * options can be added to the Container. The text entered by the user is + * used as id. Note that data-source must allow adding new items. + * + * @return True if additions are allowed. + */ + public boolean isNewItemsAllowed() { + return allowNewOptions; + } + + /** + * Enables or disables possibility to add new options by the user. + * + * @param allowNewOptions + * the New value of property allowNewOptions. + */ + public void setNewItemsAllowed(boolean allowNewOptions) { + + // Only handle change requests + if (this.allowNewOptions != allowNewOptions) { + + this.allowNewOptions = allowNewOptions; + + markAsDirty(); + } + } + + /** + * Override the caption of an item. Setting caption explicitly overrides id, + * item and index captions. + * + * @param itemId + * the id of the item to be recaptioned. + * @param caption + * the New caption. + */ + public void setItemCaption(Object itemId, String caption) { + if (itemId != null) { + itemCaptions.put(itemId, caption); + markAsDirty(); + } + } + + /** + * Gets the caption of an item. The caption is generated as specified by the + * item caption mode. See <code>setItemCaptionMode()</code> for more + * details. + * + * @param itemId + * the id of the item to be queried. + * @return the caption for specified item. + */ + public String getItemCaption(Object itemId) { + + // Null items can not be found + if (itemId == null) { + return null; + } + + String caption = null; + + switch (getItemCaptionMode()) { + + case ID: + caption = idToCaption(itemId); + break; + case ID_TOSTRING: + caption = itemId.toString(); + break; + case INDEX: + if (items instanceof Container.Indexed) { + caption = String + .valueOf(((Container.Indexed) items).indexOfId(itemId)); + } else { + caption = "ERROR: Container is not indexed"; + } + break; + + case ITEM: + final Item i = getItem(itemId); + if (i != null) { + caption = i.toString(); + } + break; + + case EXPLICIT: + caption = itemCaptions.get(itemId); + break; + + case EXPLICIT_DEFAULTS_ID: + caption = itemCaptions.get(itemId); + if (caption == null) { + caption = idToCaption(itemId); + } + break; + + case PROPERTY: + final Property<?> p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null) { + Object value = p.getValue(); + if (value != null) { + caption = value.toString(); + } + } + break; + } + + // All items must have some captions + return caption != null ? caption : ""; + } + + private String idToCaption(Object itemId) { + try { + LegacyConverter<String, Object> c = (LegacyConverter<String, Object>) LegacyConverterUtil + .getConverter(String.class, itemId.getClass(), + getSession()); + return LegacyConverterUtil.convertFromModel(itemId, String.class, c, + getLocale()); + } catch (Exception e) { + return itemId.toString(); + } + } + + /** + * Sets the icon for an item. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param icon + * the icon to use or null. + */ + public void setItemIcon(Object itemId, Resource icon) { + if (itemId != null) { + if (icon == null) { + itemIcons.remove(itemId); + } else { + itemIcons.put(itemId, icon); + } + markAsDirty(); + } + } + + /** + * Gets the item icon. + * + * @param itemId + * the id of the item to be assigned an icon. + * @return the icon for the item or null, if not specified. + */ + public Resource getItemIcon(Object itemId) { + final Resource explicit = itemIcons.get(itemId); + if (explicit != null) { + return explicit; + } + + if (getItemIconPropertyId() == null) { + return null; + } + + final Property<?> ip = getContainerProperty(itemId, + getItemIconPropertyId()); + if (ip == null) { + return null; + } + final Object icon = ip.getValue(); + if (icon instanceof Resource) { + return (Resource) icon; + } + + return null; + } + + /** + * Sets the item caption mode. + * + * See {@link ItemCaptionMode} for a description of the modes. + * <p> + * {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID} is the default mode. + * </p> + * + * @param mode + * the One of the modes listed above. + */ + public void setItemCaptionMode(ItemCaptionMode mode) { + if (mode != null) { + itemCaptionMode = mode; + markAsDirty(); + } + } + + /** + * Gets the item caption mode. + * + * <p> + * The mode can be one of the following ones: + * <ul> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items + * Id-objects <code>toString</code> is used as item caption. If caption is + * explicitly specified, it overrides the id-caption. + * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects + * <code>toString</code> is used as item caption.</li> + * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used + * as item caption. The index mode can only be used with the containers + * implementing <code>Container.Indexed</code> interface.</li> + * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be + * explicitly specified.</li> + * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read + * from property, that must be specified with + * <code>setItemCaptionPropertyId</code>.</li> + * </ul> + * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default + * mode. + * </p> + * + * @return the One of the modes listed above. + */ + public ItemCaptionMode getItemCaptionMode() { + return itemCaptionMode; + } + + /** + * Sets the item caption property. + * + * <p> + * Setting the id to a existing property implicitly sets the item caption + * mode to <code>ITEM_CAPTION_MODE_PROPERTY</code>. If the object is in + * <code>ITEM_CAPTION_MODE_PROPERTY</code> mode, setting caption property id + * null resets the item caption mode to + * <code>ITEM_CAPTION_EXPLICIT_DEFAULTS_ID</code>. + * </p> + * <p> + * Note that the type of the property used for caption must be String + * </p> + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @param propertyId + * the id of the property. + * + */ + public void setItemCaptionPropertyId(Object propertyId) { + if (propertyId != null) { + itemCaptionPropertyId = propertyId; + setItemCaptionMode(ITEM_CAPTION_MODE_PROPERTY); + markAsDirty(); + } else { + itemCaptionPropertyId = null; + if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) { + setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID); + } + markAsDirty(); + } + } + + /** + * Gets the item caption property. + * + * @return the Id of the property used as item caption source. + */ + public Object getItemCaptionPropertyId() { + return itemCaptionPropertyId; + } + + /** + * Sets the item icon property. + * + * <p> + * If the property id is set to a valid value, each item is given an icon + * got from the given property of the items. The type of the property must + * be assignable to Resource. + * </p> + * + * <p> + * Note : The icons set with <code>setItemIcon</code> function override the + * icons from the property. + * </p> + * + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @param propertyId + * the id of the property that specifies icons for items or null + * @throws IllegalArgumentException + * If the propertyId is not in the container or is not of a + * valid type + */ + public void setItemIconPropertyId(Object propertyId) + throws IllegalArgumentException { + if (propertyId == null) { + itemIconPropertyId = null; + } else if (!getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException( + "Property id not found in the container"); + } else if (Resource.class.isAssignableFrom(getType(propertyId))) { + itemIconPropertyId = propertyId; + } else { + throw new IllegalArgumentException( + "Property type must be assignable to Resource"); + } + markAsDirty(); + } + + /** + * Gets the item icon property. + * + * <p> + * If the property id is set to a valid value, each item is given an icon + * got from the given property of the items. The type of the property must + * be assignable to Icon. + * </p> + * + * <p> + * Note : The icons set with <code>setItemIcon</code> function override the + * icons from the property. + * </p> + * + * <p> + * Setting the property id to null disables this feature. The id is null by + * default + * </p> + * . + * + * @return the Id of the property containing the item icons. + */ + public Object getItemIconPropertyId() { + return itemIconPropertyId; + } + + /** + * Tests if an item is selected. + * + * <p> + * In single select mode testing selection status of the item identified by + * {@link #getNullSelectionItemId()} returns true if the value of the + * property is null. + * </p> + * + * @param itemId + * the Id the of the item to be tested. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public boolean isSelected(Object itemId) { + if (itemId == null) { + return false; + } + if (isMultiSelect()) { + return ((Set<?>) getValue()).contains(itemId); + } else { + final Object value = getValue(); + return itemId + .equals(value == null ? getNullSelectionItemId() : value); + } + } + + /** + * Selects an item. + * + * <p> + * In single select mode selecting item identified by + * {@link #getNullSelectionItemId()} sets the value of the property to null. + * </p> + * + * @param itemId + * the identifier of Item to be selected. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public void select(Object itemId) { + if (!isMultiSelect()) { + setValue(itemId); + } else if (!isSelected(itemId) && itemId != null + && items.containsId(itemId)) { + final Set<Object> s = new HashSet<Object>((Set<?>) getValue()); + s.add(itemId); + setValue(s); + } + } + + /** + * Unselects an item. + * + * @param itemId + * the identifier of the Item to be unselected. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public void unselect(Object itemId) { + if (isSelected(itemId)) { + if (isMultiSelect()) { + final Set<Object> s = new HashSet<Object>((Set<?>) getValue()); + s.remove(itemId); + setValue(s); + } else { + setValue(null); + } + } + } + + /** + * Notifies this listener that the Containers contents has changed. + * + * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + firePropertySetChange(); + } + + /** + * Adds a new Property set change listener for this Container. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void addPropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (propertySetEventListeners == null) { + propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>(); + } + propertySetEventListeners.add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addPropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Container.PropertySetChangeListener listener) { + addPropertySetChangeListener(listener); + } + + /** + * Removes a previously registered Property set change listener. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void removePropertySetChangeListener( + Container.PropertySetChangeListener listener) { + if (propertySetEventListeners != null) { + propertySetEventListeners.remove(listener); + if (propertySetEventListeners.isEmpty()) { + propertySetEventListeners = null; + } + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removePropertySetChangeListener(com.vaadin.data.Container.PropertySetChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Container.PropertySetChangeListener listener) { + removePropertySetChangeListener(listener); + } + + /** + * Adds an Item set change listener for the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void addItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (itemSetEventListeners == null) { + itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>(); + } + itemSetEventListeners.add(listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void addListener(Container.ItemSetChangeListener listener) { + addItemSetChangeListener(listener); + } + + /** + * Removes the Item set change listener from the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void removeItemSetChangeListener( + Container.ItemSetChangeListener listener) { + if (itemSetEventListeners != null) { + itemSetEventListeners.remove(listener); + if (itemSetEventListeners.isEmpty()) { + itemSetEventListeners = null; + } + } + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemSetChangeListener(com.vaadin.data.Container.ItemSetChangeListener)} + **/ + @Override + @Deprecated + public void removeListener(Container.ItemSetChangeListener listener) { + removeItemSetChangeListener(listener); + } + + @Override + public Collection<?> getListeners(Class<?> eventType) { + if (Container.ItemSetChangeEvent.class.isAssignableFrom(eventType)) { + if (itemSetEventListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(itemSetEventListeners); + } + } else if (Container.PropertySetChangeEvent.class + .isAssignableFrom(eventType)) { + if (propertySetEventListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections + .unmodifiableCollection(propertySetEventListeners); + } + } + + return super.getListeners(eventType); + } + + /** + * Lets the listener know a Containers Item set has changed. + * + * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + // Clears the item id mapping table + itemIdMapper.removeAll(); + + // Notify all listeners + fireItemSetChange(); + } + + /** + * Fires the property set change event. + */ + protected void firePropertySetChange() { + if (propertySetEventListeners != null + && !propertySetEventListeners.isEmpty()) { + final Container.PropertySetChangeEvent event = new PropertySetChangeEvent( + this); + final Object[] listeners = propertySetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.PropertySetChangeListener) listeners[i]) + .containerPropertySetChange(event); + } + } + markAsDirty(); + } + + /** + * Fires the item set change event. + */ + protected void fireItemSetChange() { + if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) { + final Container.ItemSetChangeEvent event = new ItemSetChangeEvent( + this); + final Object[] listeners = itemSetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.ItemSetChangeListener) listeners[i]) + .containerItemSetChange(event); + } + } + markAsDirty(); + } + + /** + * Implementation of item set change event. + */ + private static class ItemSetChangeEvent extends EventObject + implements Serializable, Container.ItemSetChangeEvent { + + private ItemSetChangeEvent(Container source) { + super(source); + } + + /** + * Gets the Property where the event occurred. + * + * @see com.vaadin.data.Container.ItemSetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return (Container) getSource(); + } + + } + + /** + * Implementation of property set change event. + */ + private static class PropertySetChangeEvent extends EventObject + implements Container.PropertySetChangeEvent, Serializable { + + private PropertySetChangeEvent(Container source) { + super(source); + } + + /** + * Retrieves the Container whose contents have been modified. + * + * @see com.vaadin.data.Container.PropertySetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return (Container) getSource(); + } + + } + + /** + * For multi-selectable fields, also an empty collection of values is + * considered to be an empty field. + * + * @see LegacyAbstractField#isEmpty(). + */ + @Override + public boolean isEmpty() { + if (!isMultiSelect()) { + return super.isEmpty(); + } else { + Object value = getValue(); + return super.isEmpty() || (value instanceof Collection + && ((Collection<?>) value).isEmpty()); + } + } + + /** + * Allow or disallow empty selection by the user. If the select is in + * single-select mode, you can make an item represent the empty selection by + * calling <code>setNullSelectionItemId()</code>. This way you can for + * instance set an icon and caption for the null selection item. + * + * @param nullSelectionAllowed + * whether or not to allow empty selection + * @see #setNullSelectionItemId(Object) + * @see #isNullSelectionAllowed() + */ + public void setNullSelectionAllowed(boolean nullSelectionAllowed) { + if (nullSelectionAllowed != this.nullSelectionAllowed) { + this.nullSelectionAllowed = nullSelectionAllowed; + markAsDirty(); + } + } + + /** + * Checks if null empty selection is allowed by the user. + * + * @return whether or not empty selection is allowed + * @see #setNullSelectionAllowed(boolean) + */ + public boolean isNullSelectionAllowed() { + return nullSelectionAllowed; + } + + /** + * Returns the item id that represents null value of this select in single + * select mode. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + * + * @return the Object Null value item id. + * @see #setNullSelectionItemId(Object) + * @see #isSelected(Object) + * @see #select(Object) + */ + public Object getNullSelectionItemId() { + return nullSelectionItemId; + } + + /** + * Sets the item id that represents null value of this select. + * + * <p> + * Data interface does not support nulls as item ids. Selecting the item + * identified by this id is the same as selecting no items at all. This + * setting only affects the single select mode. + * </p> + * + * @param nullSelectionItemId + * the nullSelectionItemId to set. + * @see #getNullSelectionItemId() + * @see #isSelected(Object) + * @see #select(Object) + */ + public void setNullSelectionItemId(Object nullSelectionItemId) { + if (nullSelectionItemId != null && isMultiSelect()) { + throw new IllegalStateException( + "Multiselect and NullSelectionItemId can not be set at the same time."); + } + this.nullSelectionItemId = nullSelectionItemId; + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.v7.ui.LegacyAbstractField#attach() + */ + @Override + public void attach() { + super.attach(); + } + + /** + * Detaches the component from application. + * + * @see com.vaadin.ui.AbstractComponent#detach() + */ + @Override + public void detach() { + getCaptionChangeListener().clear(); + super.detach(); + } + + // Caption change listener + protected CaptionChangeListener getCaptionChangeListener() { + if (captionChangeListener == null) { + captionChangeListener = new CaptionChangeListener(); + } + return captionChangeListener; + } + + /** + * This is a listener helper for Item and Property changes that should cause + * a repaint. It should be attached to all items that are displayed, and the + * default implementation does this in paintContent(). Especially + * "lazyloading" components should take care to add and remove listeners as + * appropriate. Call addNotifierForItem() for each painted item (and + * remember to clear). + * + * NOTE: singleton, use getCaptionChangeListener(). + * + */ + protected class CaptionChangeListener implements + Item.PropertySetChangeListener, Property.ValueChangeListener { + + // TODO clean this up - type is either Item.PropertySetChangeNotifier or + // Property.ValueChangeNotifier + HashSet<Object> captionChangeNotifiers = new HashSet<Object>(); + + public void addNotifierForItem(Object itemId) { + switch (getItemCaptionMode()) { + case ITEM: + final Item i = getItem(itemId); + if (i == null) { + return; + } + if (i instanceof Item.PropertySetChangeNotifier) { + ((Item.PropertySetChangeNotifier) i) + .addPropertySetChangeListener( + getCaptionChangeListener()); + captionChangeNotifiers.add(i); + } + Collection<?> pids = i.getItemPropertyIds(); + if (pids != null) { + for (Iterator<?> it = pids.iterator(); it.hasNext();) { + Property<?> p = i.getItemProperty(it.next()); + if (p != null + && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addValueChangeListener( + getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + } + + } + break; + case PROPERTY: + final Property<?> p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addValueChangeListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + break; + + } + if (getItemIconPropertyId() != null) { + final Property p = getContainerProperty(itemId, + getItemIconPropertyId()); + if (p != null && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addValueChangeListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + } + } + + public void clear() { + for (Iterator<Object> it = captionChangeNotifiers.iterator(); it + .hasNext();) { + Object notifier = it.next(); + if (notifier instanceof Item.PropertySetChangeNotifier) { + ((Item.PropertySetChangeNotifier) notifier) + .removePropertySetChangeListener( + getCaptionChangeListener()); + } else { + ((Property.ValueChangeNotifier) notifier) + .removeValueChangeListener( + getCaptionChangeListener()); + } + } + captionChangeNotifiers.clear(); + } + + @Override + public void valueChange( + com.vaadin.data.Property.ValueChangeEvent event) { + markAsDirty(); + } + + @Override + public void itemPropertySetChange( + com.vaadin.data.Item.PropertySetChangeEvent event) { + markAsDirty(); + } + + } + + /** + * Criterion which accepts a drop only if the drop target is (one of) the + * given Item identifier(s). Criterion can be used only on a drop targets + * that extends AbstractSelect like {@link Table} and {@link Tree}. The + * target and identifiers of valid Items are given in constructor. + * + * @since 6.3 + */ + public static class TargetItemIs extends AbstractItemSetCriterion { + + /** + * @param select + * the select implementation that is used as a drop target + * @param itemId + * the identifier(s) that are valid drop locations + */ + public TargetItemIs(AbstractSelect select, Object... itemId) { + super(select, itemId); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + if (dropTargetData.getTarget() != select) { + return false; + } + return itemIds.contains(dropTargetData.getItemIdOver()); + } + + } + + /** + * Abstract helper class to implement item id based criterion. + * + * Note, inner class used not to open itemIdMapper for public access. + * + * @since 6.3 + * + */ + private static abstract class AbstractItemSetCriterion + extends ClientSideCriterion { + protected final Collection<Object> itemIds = new HashSet<Object>(); + protected AbstractSelect select; + + public AbstractItemSetCriterion(AbstractSelect select, + Object... itemId) { + if (itemIds == null || select == null) { + throw new IllegalArgumentException( + "Accepted item identifiers must be accepted."); + } + Collections.addAll(itemIds, itemId); + this.select = select; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + String[] keys = new String[itemIds.size()]; + int i = 0; + for (Object itemId : itemIds) { + String key = select.itemIdMapper.key(itemId); + keys[i++] = key; + } + target.addAttribute("keys", keys); + target.addAttribute("s", select); + } + + } + + /** + * This criterion accepts a only a {@link Transferable} that contains given + * Item (practically its identifier) from a specific AbstractSelect. + * + * @since 6.3 + */ + public static class AcceptItem extends AbstractItemSetCriterion { + + /** + * @param select + * the select from which the item id's are checked + * @param itemId + * the item identifier(s) of the select that are accepted + */ + public AcceptItem(AbstractSelect select, Object... itemId) { + super(select, itemId); + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + DataBoundTransferable transferable = (DataBoundTransferable) dragEvent + .getTransferable(); + if (transferable.getSourceComponent() != select) { + return false; + } + return itemIds.contains(transferable.getItemId()); + } + + /** + * A simple accept criterion which ensures that {@link Transferable} + * contains an {@link Item} (or actually its identifier). In other words + * the criterion check that drag is coming from a {@link Container} like + * {@link Tree} or {@link Table}. + */ + public static final ClientSideCriterion ALL = new ContainsDataFlavor( + "itemId"); + + } + + /** + * TargetDetails implementation for subclasses of {@link AbstractSelect} + * that implement {@link DropTarget}. + * + * @since 6.3 + */ + public class AbstractSelectTargetDetails extends TargetDetailsImpl { + + /** + * The item id over which the drag event happened. + */ + protected Object idOver; + + /** + * Constructor that automatically converts itemIdOver key to + * corresponding item Id + * + */ + protected AbstractSelectTargetDetails( + Map<String, Object> rawVariables) { + super(rawVariables, (DropTarget) AbstractSelect.this); + // eagar fetch itemid, mapper may be emptied + String keyover = (String) getData("itemIdOver"); + if (keyover != null) { + idOver = itemIdMapper.get(keyover); + } + } + + /** + * If the drag operation is currently over an {@link Item}, this method + * returns the identifier of that {@link Item}. + * + */ + public Object getItemIdOver() { + return idOver; + } + + /** + * Returns a detailed vertical location where the drop happened on Item. + */ + public VerticalDropLocation getDropLocation() { + String detail = (String) getData("detail"); + if (detail == null) { + return null; + } + return VerticalDropLocation.valueOf(detail); + } + + } + + /** + * An accept criterion to accept drops only on a specific vertical location + * of an item. + * <p> + * This accept criterion is currently usable in Tree and Table + * implementations. + */ + public static class VerticalLocationIs extends TargetDetailIs { + public static VerticalLocationIs TOP = new VerticalLocationIs( + VerticalDropLocation.TOP); + public static VerticalLocationIs BOTTOM = new VerticalLocationIs( + VerticalDropLocation.BOTTOM); + public static VerticalLocationIs MIDDLE = new VerticalLocationIs( + VerticalDropLocation.MIDDLE); + + private VerticalLocationIs(VerticalDropLocation l) { + super("detail", l.name()); + } + } + + /** + * Implement this interface and pass it to Tree.setItemDescriptionGenerator + * or Table.setItemDescriptionGenerator to generate mouse over descriptions + * ("tooltips") for the rows and cells in Table or for the items in Tree. + */ + public interface ItemDescriptionGenerator extends Serializable { + + /** + * Called by Table when a cell (and row) is painted or a item is painted + * in Tree + * + * @param source + * The source of the generator, the Tree or Table the + * generator is attached to + * @param itemId + * The itemId of the painted cell + * @param propertyId + * The propertyId of the cell, null when getting row + * description + * @return The description or "tooltip" of the item. + */ + public String generateDescription(Component source, Object itemId, + Object propertyId); + } + + @Override + public void readDesign(Element design, DesignContext context) { + // handle default attributes + super.readDesign(design, context); + // handle children specifying selectable items (<option>) + readItems(design, context); + } + + protected void readItems(Element design, DesignContext context) { + Set<String> selected = new HashSet<String>(); + for (Element child : design.children()) { + readItem(child, selected, context); + } + if (!selected.isEmpty()) { + if (isMultiSelect()) { + setValue(selected, false, true); + } else if (selected.size() == 1) { + setValue(selected.iterator().next(), false, true); + } else { + throw new DesignException( + "Multiple values selected for a single select component"); + } + } + } + + /** + * Reads an Item from a design and inserts it into the data source. + * Hierarchical select components should override this method to recursively + * recursively read any child items as well. + * + * @since 7.5.0 + * @param child + * a child element representing the item + * @param selected + * A set accumulating selected items. If the item that is read is + * marked as selected, its item id should be added to this set. + * @param context + * the DesignContext instance used in parsing + * @return the item id of the new item + * + * @throws DesignException + * if the tag name of the {@code child} element is not + * {@code option}. + */ + protected Object readItem(Element child, Set<String> selected, + DesignContext context) { + if (!"option".equals(child.tagName())) { + throw new DesignException("Unrecognized child element in " + + getClass().getSimpleName() + ": " + child.tagName()); + } + + String itemId; + String caption = DesignFormatter.decodeFromTextNode(child.html()); + if (child.hasAttr("item-id")) { + itemId = child.attr("item-id"); + addItem(itemId); + setItemCaption(itemId, caption); + } else { + addItem(itemId = caption); + } + + if (child.hasAttr("icon")) { + setItemIcon(itemId, DesignAttributeHandler.readAttribute("icon", + child.attributes(), Resource.class)); + } + + if (child.hasAttr("selected")) { + selected.add(itemId); + } + + return itemId; + } + + @Override + public void writeDesign(Element design, DesignContext context) { + // Write default attributes + super.writeDesign(design, context); + + // Write options if warranted + if (context.shouldWriteData(this)) { + writeItems(design, context); + } + } + + /** + * Writes the data source items to a design. Hierarchical select components + * should override this method to only write the root items. + * + * @since 7.5.0 + * @param design + * the element into which to insert the items + * @param context + * the DesignContext instance used in writing + */ + protected void writeItems(Element design, DesignContext context) { + for (Object itemId : getItemIds()) { + writeItem(design, itemId, context); + } + } + + /** + * Writes a data source Item to a design. Hierarchical select components + * should override this method to recursively write any child items as well. + * + * @since 7.5.0 + * @param design + * the element into which to insert the item + * @param itemId + * the id of the item to write + * @param context + * the DesignContext instance used in writing + * @return + */ + protected Element writeItem(Element design, Object itemId, + DesignContext context) { + Element element = design.appendElement("option"); + + String caption = getItemCaption(itemId); + if (caption != null && !caption.equals(itemId.toString())) { + element.html(DesignFormatter.encodeForTextNode(caption)); + element.attr("item-id", itemId.toString()); + } else { + element.html(DesignFormatter.encodeForTextNode(itemId.toString())); + } + + Resource icon = getItemIcon(itemId); + if (icon != null) { + DesignAttributeHandler.writeAttribute("icon", element.attributes(), + icon, null, Resource.class); + } + + if (isSelected(itemId)) { + element.attr("selected", ""); + } + + return element; + } + + @Override + protected AbstractSelectState getState() { + return (AbstractSelectState) super.getState(); + } + + @Override + protected AbstractSelectState getState(boolean markAsDirty) { + return (AbstractSelectState) super.getState(markAsDirty); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/Calendar.java b/compatibility-server/src/main/java/com/vaadin/ui/Calendar.java new file mode 100644 index 0000000000..1a2c7ef716 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/Calendar.java @@ -0,0 +1,2029 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.lang.reflect.Method; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.EventListener; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jsoup.nodes.Attributes; +import org.jsoup.nodes.Element; + +import com.vaadin.data.Container; +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.CalendarState; +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventClickHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.RangeSelectEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.RangeSelectHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClickHandler; +import com.vaadin.ui.components.calendar.CalendarDateRange; +import com.vaadin.ui.components.calendar.CalendarTargetDetails; +import com.vaadin.ui.components.calendar.ContainerEventProvider; +import com.vaadin.ui.components.calendar.event.BasicEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEditableEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeListener; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; +import com.vaadin.ui.components.calendar.handler.BasicBackwardHandler; +import com.vaadin.ui.components.calendar.handler.BasicDateClickHandler; +import com.vaadin.ui.components.calendar.handler.BasicEventMoveHandler; +import com.vaadin.ui.components.calendar.handler.BasicEventResizeHandler; +import com.vaadin.ui.components.calendar.handler.BasicForwardHandler; +import com.vaadin.ui.components.calendar.handler.BasicWeekClickHandler; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; + +/** + * <p> + * Vaadin Calendar is for visualizing events in a calendar. Calendar events can + * be visualized in the variable length view depending on the start and end + * dates. + * </p> + * + * <li>You can set the viewable date range with the {@link #setStartDate(Date)} + * and {@link #setEndDate(Date)} methods. Calendar has a default date range of + * one week</li> + * + * <li>Calendar has two kind of views: monthly and weekly view</li> + * + * <li>If date range is seven days or shorter, the weekly view is used.</li> + * + * <li>Calendar queries its events by using a + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider}. By default, a + * {@link com.vaadin.addon.calendar.event.BasicEventProvider BasicEventProvider} + * is used.</li> + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class Calendar extends AbstractComponent + implements CalendarComponentEvents.NavigationNotifier, + CalendarComponentEvents.EventMoveNotifier, + CalendarComponentEvents.RangeSelectNotifier, + CalendarComponentEvents.EventResizeNotifier, + CalendarEventProvider.EventSetChangeListener, DropTarget, + CalendarEditableEventProvider, Action.Container, LegacyComponent { + + /** + * Calendar can use either 12 hours clock or 24 hours clock. + */ + public enum TimeFormat { + + Format12H(), Format24H(); + } + + /** Defines currently active format for time. 12H/24H. */ + protected TimeFormat currentTimeFormat; + + /** Internal calendar data source. */ + protected java.util.Calendar currentCalendar = java.util.Calendar + .getInstance(); + + /** Defines the component's active time zone. */ + protected TimeZone timezone; + + /** Defines the calendar's date range starting point. */ + protected Date startDate = null; + + /** Defines the calendar's date range ending point. */ + protected Date endDate = null; + + /** Event provider. */ + private CalendarEventProvider calendarEventProvider; + + /** + * Internal buffer for the events that are retrieved from the event + * provider. + */ + protected List<CalendarEvent> events; + + /** Date format that will be used in the UIDL for dates. */ + protected DateFormat df_date = new SimpleDateFormat("yyyy-MM-dd"); + + /** Time format that will be used in the UIDL for time. */ + protected DateFormat df_time = new SimpleDateFormat("HH:mm:ss"); + + /** Date format that will be used in the UIDL for both date and time. */ + protected DateFormat df_date_time = new SimpleDateFormat( + DateConstants.CLIENT_DATE_FORMAT + "-" + + DateConstants.CLIENT_TIME_FORMAT); + + /** + * Week view's scroll position. Client sends updates to this value so that + * scroll position wont reset all the time. + */ + private int scrollTop = 0; + + /** Caption format for the weekly view */ + private String weeklyCaptionFormat = null; + + /** Map from event ids to event handlers */ + private final Map<String, EventListener> handlers; + + /** + * Drop Handler for Vaadin DD. By default null. + */ + private DropHandler dropHandler; + + /** + * First day to show for a week + */ + private int firstDay = 1; + + /** + * Last day to show for a week + */ + private int lastDay = 7; + + /** + * First hour to show for a day + */ + private int firstHour = 0; + + /** + * Last hour to show for a day + */ + private int lastHour = 23; + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * + */ + private CalendarServerRpcImpl rpc = new CalendarServerRpcImpl(); + + private Integer customFirstDayOfWeek; + + /** + * Returns the logger for the calendar + */ + protected Logger getLogger() { + return Logger.getLogger(Calendar.class.getName()); + } + + /** + * Construct a Vaadin Calendar with a BasicEventProvider and no caption. + * Default date range is one week. + */ + public Calendar() { + this(null, new BasicEventProvider()); + } + + /** + * Construct a Vaadin Calendar with a BasicEventProvider and the provided + * caption. Default date range is one week. + * + * @param caption + */ + public Calendar(String caption) { + this(caption, new BasicEventProvider()); + } + + /** + * <p> + * Construct a Vaadin Calendar with event provider. Event provider is + * obligatory, because calendar component will query active events through + * it. + * </p> + * + * <p> + * By default, Vaadin Calendar will show dates from the start of the current + * week to the end of the current week. Use {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)} to change this. + * </p> + * + * @param eventProvider + * Event provider, cannot be null. + */ + public Calendar(CalendarEventProvider eventProvider) { + this(null, eventProvider); + } + + /** + * <p> + * Construct a Vaadin Calendar with event provider and a caption. Event + * provider is obligatory, because calendar component will query active + * events through it. + * </p> + * + * <p> + * By default, Vaadin Calendar will show dates from the start of the current + * week to the end of the current week. Use {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)} to change this. + * </p> + * + * @param eventProvider + * Event provider, cannot be null. + */ + // this is the constructor every other constructor calls + public Calendar(String caption, CalendarEventProvider eventProvider) { + registerRpc(rpc); + setCaption(caption); + handlers = new HashMap<String, EventListener>(); + setDefaultHandlers(); + currentCalendar.setTime(new Date()); + setEventProvider(eventProvider); + getState().firstDayOfWeek = firstDay; + getState().lastVisibleDayOfWeek = lastDay; + getState().firstHourOfDay = firstHour; + getState().lastHourOfDay = lastHour; + setTimeFormat(null); + + } + + @Override + public CalendarState getState() { + return (CalendarState) super.getState(); + } + + @Override + protected CalendarState getState(boolean markAsDirty) { + return (CalendarState) super.getState(markAsDirty); + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + + initCalendarWithLocale(); + + getState().format24H = TimeFormat.Format24H == getTimeFormat(); + setupDaysAndActions(); + setupCalendarEvents(); + rpc.scroll(scrollTop); + } + + /** + * Set all the wanted default handlers here. This is always called after + * constructing this object. All other events have default handlers except + * range and event click. + */ + protected void setDefaultHandlers() { + setHandler(new BasicBackwardHandler()); + setHandler(new BasicForwardHandler()); + setHandler(new BasicWeekClickHandler()); + setHandler(new BasicDateClickHandler()); + setHandler(new BasicEventMoveHandler()); + setHandler(new BasicEventResizeHandler()); + } + + /** + * Gets the calendar's start date. + * + * @return First visible date. + */ + public Date getStartDate() { + if (startDate == null) { + currentCalendar.set(java.util.Calendar.MILLISECOND, 0); + currentCalendar.set(java.util.Calendar.SECOND, 0); + currentCalendar.set(java.util.Calendar.MINUTE, 0); + currentCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0); + currentCalendar.set(java.util.Calendar.DAY_OF_WEEK, + currentCalendar.getFirstDayOfWeek()); + return currentCalendar.getTime(); + } + return startDate; + } + + /** + * Sets start date for the calendar. This and {@link #setEndDate(Date)} + * control the range of dates visible on the component. The default range is + * one week. + * + * @param date + * First visible date to show. + */ + public void setStartDate(Date date) { + if (!date.equals(startDate)) { + startDate = date; + markAsDirty(); + } + } + + /** + * Gets the calendar's end date. + * + * @return Last visible date. + */ + public Date getEndDate() { + if (endDate == null) { + currentCalendar.set(java.util.Calendar.MILLISECOND, 0); + currentCalendar.set(java.util.Calendar.SECOND, 59); + currentCalendar.set(java.util.Calendar.MINUTE, 59); + currentCalendar.set(java.util.Calendar.HOUR_OF_DAY, 23); + currentCalendar.set(java.util.Calendar.DAY_OF_WEEK, + currentCalendar.getFirstDayOfWeek() + 6); + return currentCalendar.getTime(); + } + return endDate; + } + + /** + * Sets end date for the calendar. Starting from startDate, only six weeks + * will be shown if duration to endDate is longer than six weeks. + * + * This and {@link #setStartDate(Date)} control the range of dates visible + * on the component. The default range is one week. + * + * @param date + * Last visible date to show. + */ + public void setEndDate(Date date) { + if (startDate != null && startDate.after(date)) { + startDate = (Date) date.clone(); + markAsDirty(); + } else if (!date.equals(endDate)) { + endDate = date; + markAsDirty(); + } + } + + /** + * Sets the locale to be used in the Calendar component. + * + * @see com.vaadin.ui.AbstractComponent#setLocale(java.util.Locale) + */ + @Override + public void setLocale(Locale newLocale) { + super.setLocale(newLocale); + initCalendarWithLocale(); + } + + /** + * Initialize the java calendar instance with the current locale and + * timezone. + */ + private void initCalendarWithLocale() { + if (timezone != null) { + currentCalendar = java.util.Calendar.getInstance(timezone, + getLocale()); + + } else { + currentCalendar = java.util.Calendar.getInstance(getLocale()); + } + + if (customFirstDayOfWeek != null) { + currentCalendar.setFirstDayOfWeek(customFirstDayOfWeek); + } + } + + private void setupCalendarEvents() { + int durationInDays = (int) (((endDate.getTime()) - startDate.getTime()) + / DateConstants.DAYINMILLIS); + durationInDays++; + if (durationInDays > 60) { + throw new RuntimeException( + "Daterange is too big (max 60) = " + durationInDays); + } + + Date firstDateToShow = expandStartDate(startDate, durationInDays > 7); + Date lastDateToShow = expandEndDate(endDate, durationInDays > 7); + + currentCalendar.setTime(firstDateToShow); + events = getEventProvider().getEvents(firstDateToShow, lastDateToShow); + + List<CalendarState.Event> calendarStateEvents = new ArrayList<CalendarState.Event>(); + if (events != null) { + for (int i = 0; i < events.size(); i++) { + CalendarEvent e = events.get(i); + CalendarState.Event event = new CalendarState.Event(); + event.index = i; + event.caption = e.getCaption() == null ? "" : e.getCaption(); + event.dateFrom = df_date.format(e.getStart()); + event.dateTo = df_date.format(e.getEnd()); + event.timeFrom = df_time.format(e.getStart()); + event.timeTo = df_time.format(e.getEnd()); + event.description = e.getDescription() == null ? "" + : e.getDescription(); + event.styleName = e.getStyleName() == null ? "" + : e.getStyleName(); + event.allDay = e.isAllDay(); + calendarStateEvents.add(event); + } + } + getState().events = calendarStateEvents; + } + + private void setupDaysAndActions() { + // Make sure we have a up-to-date locale + initCalendarWithLocale(); + + CalendarState state = getState(); + + state.firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + + // If only one is null, throw exception + // If both are null, set defaults + if (startDate == null ^ endDate == null) { + String message = "Schedule cannot be painted without a proper date range.\n"; + if (startDate == null) { + throw new IllegalStateException(message + + "You must set a start date using setStartDate(Date)."); + + } else { + throw new IllegalStateException(message + + "You must set an end date using setEndDate(Date)."); + } + + } else if (startDate == null && endDate == null) { + // set defaults + startDate = getStartDate(); + endDate = getEndDate(); + } + + int durationInDays = (int) (((endDate.getTime()) - startDate.getTime()) + / DateConstants.DAYINMILLIS); + durationInDays++; + if (durationInDays > 60) { + throw new RuntimeException( + "Daterange is too big (max 60) = " + durationInDays); + } + + state.dayNames = getDayNamesShort(); + state.monthNames = getMonthNamesShort(); + + // Use same timezone in all dates this component handles. + // Show "now"-marker in browser within given timezone. + Date now = new Date(); + currentCalendar.setTime(now); + now = currentCalendar.getTime(); + + // Reset time zones for custom date formats + df_date.setTimeZone(currentCalendar.getTimeZone()); + df_time.setTimeZone(currentCalendar.getTimeZone()); + + state.now = (df_date.format(now) + " " + df_time.format(now)); + + Date firstDateToShow = expandStartDate(startDate, durationInDays > 7); + Date lastDateToShow = expandEndDate(endDate, durationInDays > 7); + + currentCalendar.setTime(firstDateToShow); + + DateFormat weeklyCaptionFormatter = getWeeklyCaptionFormatter(); + weeklyCaptionFormatter.setTimeZone(currentCalendar.getTimeZone()); + + Map<CalendarDateRange, Set<Action>> actionMap = new HashMap<CalendarDateRange, Set<Action>>(); + + List<CalendarState.Day> days = new ArrayList<CalendarState.Day>(); + + // Send all dates to client from server. This + // approach was taken because gwt doesn't + // support date localization properly. + while (currentCalendar.getTime().compareTo(lastDateToShow) < 1) { + final Date date = currentCalendar.getTime(); + final CalendarState.Day day = new CalendarState.Day(); + day.date = df_date.format(date); + day.localizedDateFormat = weeklyCaptionFormatter.format(date); + day.dayOfWeek = getDowByLocale(currentCalendar); + day.week = getWeek(currentCalendar); + day.yearOfWeek = getYearOfWeek(currentCalendar); + + days.add(day); + + // Get actions for a specific date + if (actionHandlers != null) { + for (Action.Handler actionHandler : actionHandlers) { + + // Create calendar which omits time + GregorianCalendar cal = new GregorianCalendar(getTimeZone(), + getLocale()); + cal.clear(); + cal.set(currentCalendar.get(java.util.Calendar.YEAR), + currentCalendar.get(java.util.Calendar.MONTH), + currentCalendar.get(java.util.Calendar.DATE)); + + // Get day start and end times + Date start = cal.getTime(); + cal.add(java.util.Calendar.DATE, 1); + cal.add(java.util.Calendar.SECOND, -1); + Date end = cal.getTime(); + + boolean monthView = (durationInDays > 7); + + /** + * If in day or week view add actions for each half-an-hour. + * If in month view add actions for each day + */ + if (monthView) { + setActionsForDay(actionMap, start, end, actionHandler); + } else { + setActionsForEachHalfHour(actionMap, start, end, + actionHandler); + } + + } + } + + currentCalendar.add(java.util.Calendar.DATE, 1); + } + state.days = days; + state.actions = createActionsList(actionMap); + } + + private int getWeek(java.util.Calendar calendar) { + return calendar.get(java.util.Calendar.WEEK_OF_YEAR); + } + + private int getYearOfWeek(java.util.Calendar calendar) { + // Would use calendar.getWeekYear() but it's only available since 1.7. + int week = getWeek(calendar); + int month = calendar.get(java.util.Calendar.MONTH); + int year = calendar.get(java.util.Calendar.YEAR); + + if (week == 1 && month == java.util.Calendar.DECEMBER) { + return year + 1; + } + + return year; + } + + private void setActionsForEachHalfHour( + Map<CalendarDateRange, Set<Action>> actionMap, Date start, Date end, + Action.Handler actionHandler) { + GregorianCalendar cal = new GregorianCalendar(getTimeZone(), + getLocale()); + cal.setTime(start); + while (cal.getTime().before(end)) { + Date s = cal.getTime(); + cal.add(java.util.Calendar.MINUTE, 30); + Date e = cal.getTime(); + CalendarDateRange range = new CalendarDateRange(s, e, + getTimeZone()); + Action[] actions = actionHandler.getActions(range, this); + if (actions != null) { + Set<Action> actionSet = new LinkedHashSet<Action>( + Arrays.asList(actions)); + actionMap.put(range, actionSet); + } + } + } + + private void setActionsForDay(Map<CalendarDateRange, Set<Action>> actionMap, + Date start, Date end, Action.Handler actionHandler) { + CalendarDateRange range = new CalendarDateRange(start, end, + getTimeZone()); + Action[] actions = actionHandler.getActions(range, this); + if (actions != null) { + Set<Action> actionSet = new LinkedHashSet<Action>( + Arrays.asList(actions)); + actionMap.put(range, actionSet); + } + } + + private List<CalendarState.Action> createActionsList( + Map<CalendarDateRange, Set<Action>> actionMap) { + if (actionMap.isEmpty()) { + return null; + } + + List<CalendarState.Action> calendarActions = new ArrayList<CalendarState.Action>(); + + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + + for (Entry<CalendarDateRange, Set<Action>> entry : actionMap + .entrySet()) { + CalendarDateRange range = entry.getKey(); + Set<Action> actions = entry.getValue(); + for (Action action : actions) { + String key = actionMapper.key(action); + CalendarState.Action calendarAction = new CalendarState.Action(); + calendarAction.actionKey = key; + calendarAction.caption = action.getCaption(); + setResource(key, action.getIcon()); + calendarAction.iconKey = key; + calendarAction.startDate = formatter.format(range.getStart()); + calendarAction.endDate = formatter.format(range.getEnd()); + calendarActions.add(calendarAction); + } + } + + return calendarActions; + } + + /** + * Gets currently active time format. Value is either TimeFormat.Format12H + * or TimeFormat.Format24H. + * + * @return TimeFormat Format for the time. + */ + public TimeFormat getTimeFormat() { + if (currentTimeFormat == null) { + SimpleDateFormat f; + if (getLocale() == null) { + f = (SimpleDateFormat) SimpleDateFormat + .getTimeInstance(SimpleDateFormat.SHORT); + } else { + f = (SimpleDateFormat) SimpleDateFormat + .getTimeInstance(SimpleDateFormat.SHORT, getLocale()); + } + String p = f.toPattern(); + if (p.indexOf("HH") != -1 || p.indexOf("H") != -1) { + return TimeFormat.Format24H; + } + return TimeFormat.Format12H; + } + return currentTimeFormat; + } + + /** + * Example: <code>setTimeFormat(TimeFormat.Format12H);</code></br> + * Set to null, if you want the format being defined by the locale. + * + * @param format + * Set 12h or 24h format. Default is defined by the locale. + */ + public void setTimeFormat(TimeFormat format) { + currentTimeFormat = format; + markAsDirty(); + } + + /** + * Returns a time zone that is currently used by this component. + * + * @return Component's Time zone + */ + public TimeZone getTimeZone() { + if (timezone == null) { + return currentCalendar.getTimeZone(); + } + return timezone; + } + + /** + * Set time zone that this component will use. Null value sets the default + * time zone. + * + * @param zone + * Time zone to use + */ + public void setTimeZone(TimeZone zone) { + timezone = zone; + if (!currentCalendar.getTimeZone().equals(zone)) { + if (zone == null) { + zone = TimeZone.getDefault(); + } + currentCalendar.setTimeZone(zone); + df_date_time.setTimeZone(zone); + markAsDirty(); + } + } + + /** + * Get the internally used Calendar instance. This is the currently used + * instance of {@link java.util.Calendar} but is bound to change during the + * lifetime of the component. + * + * @return the currently used java calendar + */ + public java.util.Calendar getInternalCalendar() { + return currentCalendar; + } + + /** + * <p> + * This method restricts the weekdays that are shown. This affects both the + * monthly and the weekly view. The general contract is that <b>firstDay < + * lastDay</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param firstDay + * the first day of the week to show, between 1 and 7 + */ + public void setFirstVisibleDayOfWeek(int firstDay) { + if (this.firstDay != firstDay && firstDay >= 1 && firstDay <= 7 + && getLastVisibleDayOfWeek() >= firstDay) { + this.firstDay = firstDay; + getState().firstVisibleDayOfWeek = firstDay; + } + } + + /** + * Get the first visible day of the week. Returns the weekdays as integers + * represented by {@link java.util.Calendar#DAY_OF_WEEK} + * + * @return An integer representing the week day according to + * {@link java.util.Calendar#DAY_OF_WEEK} + */ + public int getFirstVisibleDayOfWeek() { + return firstDay; + } + + /** + * <p> + * This method restricts the weekdays that are shown. This affects both the + * monthly and the weekly view. The general contract is that <b>firstDay < + * lastDay</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param lastDay + * the first day of the week to show, between 1 and 7 + */ + public void setLastVisibleDayOfWeek(int lastDay) { + if (this.lastDay != lastDay && lastDay >= 1 && lastDay <= 7 + && getFirstVisibleDayOfWeek() <= lastDay) { + this.lastDay = lastDay; + getState().lastVisibleDayOfWeek = lastDay; + } + } + + /** + * Get the last visible day of the week. Returns the weekdays as integers + * represented by {@link java.util.Calendar#DAY_OF_WEEK} + * + * @return An integer representing the week day according to + * {@link java.util.Calendar#DAY_OF_WEEK} + */ + public int getLastVisibleDayOfWeek() { + return lastDay; + } + + /** + * <p> + * This method restricts the hours that are shown per day. This affects the + * weekly view. The general contract is that <b>firstHour < lastHour</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param firstHour + * the first hour of the day to show, between 0 and 23 + */ + public void setFirstVisibleHourOfDay(int firstHour) { + if (this.firstHour != firstHour && firstHour >= 0 && firstHour <= 23 + && firstHour <= getLastVisibleHourOfDay()) { + this.firstHour = firstHour; + getState().firstHourOfDay = firstHour; + } + } + + /** + * Returns the first visible hour in the week view. Returns the hour using a + * 24h time format + * + */ + public int getFirstVisibleHourOfDay() { + return firstHour; + } + + /** + * <p> + * This method restricts the hours that are shown per day. This affects the + * weekly view. The general contract is that <b>firstHour < lastHour</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param lastHour + * the first hour of the day to show, between 0 and 23 + */ + public void setLastVisibleHourOfDay(int lastHour) { + if (this.lastHour != lastHour && lastHour >= 0 && lastHour <= 23 + && lastHour >= getFirstVisibleHourOfDay()) { + this.lastHour = lastHour; + getState().lastHourOfDay = lastHour; + } + } + + /** + * Returns the last visible hour in the week view. Returns the hour using a + * 24h time format + * + */ + public int getLastVisibleHourOfDay() { + return lastHour; + } + + /** + * Gets the date caption format for the weekly view. + * + * @return The pattern used in caption of dates in weekly view. + */ + public String getWeeklyCaptionFormat() { + return weeklyCaptionFormat; + } + + /** + * Sets custom date format for the weekly view. This is the caption of the + * date. Format could be like "mmm MM/dd". + * + * @param dateFormatPattern + * The date caption pattern. + */ + public void setWeeklyCaptionFormat(String dateFormatPattern) { + if ((weeklyCaptionFormat == null && dateFormatPattern != null) + || (weeklyCaptionFormat != null + && !weeklyCaptionFormat.equals(dateFormatPattern))) { + weeklyCaptionFormat = dateFormatPattern; + markAsDirty(); + } + } + + private DateFormat getWeeklyCaptionFormatter() { + if (weeklyCaptionFormat != null) { + return new SimpleDateFormat(weeklyCaptionFormat, getLocale()); + } else { + return SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT, + getLocale()); + } + } + + /** + * Get the day of week by the given calendar and its locale + * + * @param calendar + * The calendar to use + * @return + */ + private static int getDowByLocale(java.util.Calendar calendar) { + int fow = calendar.get(java.util.Calendar.DAY_OF_WEEK); + + // monday first + if (calendar.getFirstDayOfWeek() == java.util.Calendar.MONDAY) { + fow = (fow == java.util.Calendar.SUNDAY) ? 7 : fow - 1; + } + + return fow; + } + + /** + * Is the user allowed to trigger events which alters the events + * + * @return true if the client is allowed to send changes to server + * @see #isEventClickAllowed() + */ + protected boolean isClientChangeAllowed() { + return !isReadOnly(); + } + + /** + * Is the user allowed to trigger click events. Returns {@code true} by + * default. Subclass can override this method to disallow firing event + * clicks got from the client side. + * + * @return true if the client is allowed to click events + * @see #isClientChangeAllowed() + * @deprecated As of 7.4, override {@link #fireEventClick(Integer)} instead. + */ + @Deprecated + protected boolean isEventClickAllowed() { + return true; + } + + /** + * Fires an event when the user selecing moving forward/backward in the + * calendar. + * + * @param forward + * True if the calendar moved forward else backward is assumed. + */ + protected void fireNavigationEvent(boolean forward) { + if (forward) { + fireEvent(new ForwardEvent(this)); + } else { + fireEvent(new BackwardEvent(this)); + } + } + + /** + * Fires an event move event to all server side move listerners + * + * @param index + * The index of the event in the events list + * @param newFromDatetime + * The changed from date time + */ + protected void fireEventMove(int index, Date newFromDatetime) { + MoveEvent event = new MoveEvent(this, events.get(index), + newFromDatetime); + + if (calendarEventProvider instanceof EventMoveHandler) { + // Notify event provider if it is an event move handler + ((EventMoveHandler) calendarEventProvider).eventMove(event); + } + + // Notify event move handler attached by using the + // setHandler(EventMoveHandler) method + fireEvent(event); + } + + /** + * Fires event when a week was clicked in the calendar. + * + * @param week + * The week that was clicked + * @param year + * The year of the week + */ + protected void fireWeekClick(int week, int year) { + fireEvent(new WeekClick(this, week, year)); + } + + /** + * Fires event when a date was clicked in the calendar. Uses an existing + * event from the event cache. + * + * @param index + * The index of the event in the event cache. + */ + protected void fireEventClick(Integer index) { + fireEvent(new EventClick(this, events.get(index))); + } + + /** + * Fires event when a date was clicked in the calendar. Creates a new event + * for the date and passes it to the listener. + * + * @param date + * The date and time that was clicked + */ + protected void fireDateClick(Date date) { + fireEvent(new DateClickEvent(this, date)); + } + + /** + * Fires an event range selected event. The event is fired when a user + * highlights an area in the calendar. The highlighted areas start and end + * dates are returned as arguments. + * + * @param from + * The start date and time of the highlighted area + * @param to + * The end date and time of the highlighted area + * @param monthlyMode + * Is the calendar in monthly mode + */ + protected void fireRangeSelect(Date from, Date to, boolean monthlyMode) { + fireEvent(new RangeSelectEvent(this, from, to, monthlyMode)); + } + + /** + * Fires an event resize event. The event is fired when a user resizes the + * event in the calendar causing the time range of the event to increase or + * decrease. The new start and end times are returned as arguments to this + * method. + * + * @param index + * The index of the event in the event cache + * @param startTime + * The new start date and time of the event + * @param endTime + * The new end date and time of the event + */ + protected void fireEventResize(int index, Date startTime, Date endTime) { + EventResize event = new EventResize(this, events.get(index), startTime, + endTime); + + if (calendarEventProvider instanceof EventResizeHandler) { + // Notify event provider if it is an event resize handler + ((EventResizeHandler) calendarEventProvider).eventResize(event); + } + + // Notify event resize handler attached by using the + // setHandler(EventMoveHandler) method + fireEvent(event); + } + + /** + * Localized display names for week days starting from sunday. Returned + * array's length is always 7. + * + * @return Array of localized weekday names. + */ + protected String[] getDayNamesShort() { + DateFormatSymbols s = new DateFormatSymbols(getLocale()); + return Arrays.copyOfRange(s.getWeekdays(), 1, 8); + } + + /** + * Localized display names for months starting from January. Returned + * array's length is always 12. + * + * @return Array of localized month names. + */ + protected String[] getMonthNamesShort() { + DateFormatSymbols s = new DateFormatSymbols(getLocale()); + return Arrays.copyOf(s.getShortMonths(), 12); + } + + /** + * Gets a date that is first day in the week that target given date belongs + * to. + * + * @param date + * Target date + * @return Date that is first date in same week that given date is. + */ + protected Date getFirstDateForWeek(Date date) { + int firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + currentCalendar.setTime(date); + while (firstDayOfWeek != currentCalendar + .get(java.util.Calendar.DAY_OF_WEEK)) { + currentCalendar.add(java.util.Calendar.DATE, -1); + } + return currentCalendar.getTime(); + } + + /** + * Gets a date that is last day in the week that target given date belongs + * to. + * + * @param date + * Target date + * @return Date that is last date in same week that given date is. + */ + protected Date getLastDateForWeek(Date date) { + currentCalendar.setTime(date); + currentCalendar.add(java.util.Calendar.DATE, 1); + int firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + // Roll to weeks last day using firstdayofweek. Roll until FDofW is + // found and then roll back one day. + while (firstDayOfWeek != currentCalendar + .get(java.util.Calendar.DAY_OF_WEEK)) { + currentCalendar.add(java.util.Calendar.DATE, 1); + } + currentCalendar.add(java.util.Calendar.DATE, -1); + return currentCalendar.getTime(); + } + + /** + * Calculates the end time of the day using the given calendar and date + * + * @param date + * @param calendar + * the calendar instance to be used in the calculation. The given + * instance is unchanged in this operation. + * @return the given date, with time set to the end of the day + */ + private static Date getEndOfDay(java.util.Calendar calendar, Date date) { + java.util.Calendar calendarClone = (java.util.Calendar) calendar + .clone(); + + calendarClone.setTime(date); + calendarClone.set(java.util.Calendar.MILLISECOND, + calendarClone.getActualMaximum(java.util.Calendar.MILLISECOND)); + calendarClone.set(java.util.Calendar.SECOND, + calendarClone.getActualMaximum(java.util.Calendar.SECOND)); + calendarClone.set(java.util.Calendar.MINUTE, + calendarClone.getActualMaximum(java.util.Calendar.MINUTE)); + calendarClone.set(java.util.Calendar.HOUR, + calendarClone.getActualMaximum(java.util.Calendar.HOUR)); + calendarClone.set(java.util.Calendar.HOUR_OF_DAY, + calendarClone.getActualMaximum(java.util.Calendar.HOUR_OF_DAY)); + + return calendarClone.getTime(); + } + + /** + * Calculates the end time of the day using the given calendar and date + * + * @param date + * @param calendar + * the calendar instance to be used in the calculation. The given + * instance is unchanged in this operation. + * @return the given date, with time set to the end of the day + */ + private static Date getStartOfDay(java.util.Calendar calendar, Date date) { + java.util.Calendar calendarClone = (java.util.Calendar) calendar + .clone(); + + calendarClone.setTime(date); + calendarClone.set(java.util.Calendar.MILLISECOND, 0); + calendarClone.set(java.util.Calendar.SECOND, 0); + calendarClone.set(java.util.Calendar.MINUTE, 0); + calendarClone.set(java.util.Calendar.HOUR, 0); + calendarClone.set(java.util.Calendar.HOUR_OF_DAY, 0); + + return calendarClone.getTime(); + } + + /** + * Finds the first day of the week and returns a day representing the start + * of that day + * + * @param start + * The actual date + * @param expandToFullWeek + * Should the returned date be moved to the start of the week + * @return If expandToFullWeek is set then it returns the first day of the + * week, else it returns a clone of the actual date with the time + * set to the start of the day + */ + protected Date expandStartDate(Date start, boolean expandToFullWeek) { + // If the duration is more than week, use monthly view and get startweek + // and endweek. Example if views daterange is from tuesday to next weeks + // wednesday->expand to monday to nextweeks sunday. If firstdayofweek = + // monday + if (expandToFullWeek) { + start = getFirstDateForWeek(start); + + } else { + start = (Date) start.clone(); + } + + // Always expand to the start of the first day to the end of the last + // day + start = getStartOfDay(currentCalendar, start); + + return start; + } + + /** + * Finds the last day of the week and returns a day representing the end of + * that day + * + * @param end + * The actual date + * @param expandToFullWeek + * Should the returned date be moved to the end of the week + * @return If expandToFullWeek is set then it returns the last day of the + * week, else it returns a clone of the actual date with the time + * set to the end of the day + */ + protected Date expandEndDate(Date end, boolean expandToFullWeek) { + // If the duration is more than week, use monthly view and get startweek + // and endweek. Example if views daterange is from tuesday to next weeks + // wednesday->expand to monday to nextweeks sunday. If firstdayofweek = + // monday + if (expandToFullWeek) { + end = getLastDateForWeek(end); + + } else { + end = (Date) end.clone(); + } + + // Always expand to the start of the first day to the end of the last + // day + end = getEndOfDay(currentCalendar, end); + + return end; + } + + /** + * Set the {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} to be used with this calendar. The EventProvider + * is used to query for events to show, and must be non-null. By default a + * {@link com.vaadin.addon.calendar.event.BasicEventProvider + * BasicEventProvider} is used. + * + * @param calendarEventProvider + * the calendarEventProvider to set. Cannot be null. + */ + public void setEventProvider(CalendarEventProvider calendarEventProvider) { + if (calendarEventProvider == null) { + throw new IllegalArgumentException( + "Calendar event provider cannot be null"); + } + + // remove old listener + if (getEventProvider() instanceof EventSetChangeNotifier) { + ((EventSetChangeNotifier) getEventProvider()) + .removeEventSetChangeListener(this); + } + + this.calendarEventProvider = calendarEventProvider; + + // add new listener + if (calendarEventProvider instanceof EventSetChangeNotifier) { + ((EventSetChangeNotifier) calendarEventProvider) + .addEventSetChangeListener(this); + } + } + + /** + * @return the {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} currently used + */ + public CalendarEventProvider getEventProvider() { + return calendarEventProvider; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.ui.CalendarEvents.EventChangeListener# + * eventChange (com.vaadin.addon.calendar.ui.CalendarEvents.EventChange) + */ + @Override + public void eventSetChange(EventSetChangeEvent changeEvent) { + // sanity check + if (calendarEventProvider == changeEvent.getProvider()) { + markAsDirty(); + } + } + + /** + * Set the handler for the given type information. Mirrors + * {@link #addListener(String, Class, Object, Method) addListener} from + * AbstractComponent + * + * @param eventId + * A unique id for the event. Usually one of + * {@link CalendarEventId} + * @param eventType + * The class of the event, most likely a subclass of + * {@link CalendarComponentEvent} + * @param listener + * A listener that listens to the given event + * @param listenerMethod + * The method on the lister to call when the event is triggered + */ + protected void setHandler(String eventId, Class<?> eventType, + EventListener listener, Method listenerMethod) { + if (handlers.get(eventId) != null) { + removeListener(eventId, eventType, handlers.get(eventId)); + handlers.remove(eventId); + } + + if (listener != null) { + addListener(eventId, eventType, listener, listenerMethod); + handlers.put(eventId, listener); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler) + */ + @Override + public void setHandler(ForwardHandler listener) { + setHandler(ForwardEvent.EVENT_ID, ForwardEvent.class, listener, + ForwardHandler.forwardMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler) + */ + @Override + public void setHandler(BackwardHandler listener) { + setHandler(BackwardEvent.EVENT_ID, BackwardEvent.class, listener, + BackwardHandler.backwardMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler) + */ + @Override + public void setHandler(DateClickHandler listener) { + setHandler(DateClickEvent.EVENT_ID, DateClickEvent.class, listener, + DateClickHandler.dateClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventClickHandler) + */ + @Override + public void setHandler(EventClickHandler listener) { + setHandler(EventClick.EVENT_ID, EventClick.class, listener, + EventClickHandler.eventClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler) + */ + @Override + public void setHandler(WeekClickHandler listener) { + setHandler(WeekClick.EVENT_ID, WeekClick.class, listener, + WeekClickHandler.weekClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * ) + */ + @Override + public void setHandler(EventResizeHandler listener) { + setHandler(EventResize.EVENT_ID, EventResize.class, listener, + EventResizeHandler.eventResizeMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectHandler + * ) + */ + @Override + public void setHandler(RangeSelectHandler listener) { + setHandler(RangeSelectEvent.EVENT_ID, RangeSelectEvent.class, listener, + RangeSelectHandler.rangeSelectMethod); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler) + */ + @Override + public void setHandler(EventMoveHandler listener) { + setHandler(MoveEvent.EVENT_ID, MoveEvent.class, listener, + EventMoveHandler.eventMoveMethod); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents. + * CalendarEventNotifier #getHandler(java.lang.String) + */ + @Override + public EventListener getHandler(String eventId) { + return handlers.get(eventId); + } + + /** + * Get the currently active drop handler + */ + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + /** + * Set the drop handler for the calendar See {@link DropHandler} for + * implementation details. + * + * @param dropHandler + * The drop handler to set + */ + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map) + */ + @Override + public TargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + Map<String, Object> serverVariables = new HashMap<String, Object>(); + + if (clientVariables.containsKey("dropSlotIndex")) { + int slotIndex = (Integer) clientVariables.get("dropSlotIndex"); + int dayIndex = (Integer) clientVariables.get("dropDayIndex"); + + currentCalendar.setTime(getStartOfDay(currentCalendar, startDate)); + currentCalendar.add(java.util.Calendar.DATE, dayIndex); + + // change this if slot length is modified + currentCalendar.add(java.util.Calendar.MINUTE, slotIndex * 30); + + serverVariables.put("dropTime", currentCalendar.getTime()); + + } else { + int dayIndex = (Integer) clientVariables.get("dropDayIndex"); + currentCalendar.setTime(expandStartDate(startDate, true)); + currentCalendar.add(java.util.Calendar.DATE, dayIndex); + serverVariables.put("dropDay", currentCalendar.getTime()); + } + serverVariables.put("mouseEvent", clientVariables.get("mouseEvent")); + + CalendarTargetDetails td = new CalendarTargetDetails(serverVariables, + this); + td.setHasDropTime(clientVariables.containsKey("dropSlotIndex")); + + return td; + } + + /** + * Sets a container as a data source for the events in the calendar. + * Equivalent for doing + * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code> + * + * Use this method if you are adding a container which uses the default + * property ids like {@link BeanItemContainer} for instance. If you are + * using custom properties instead use + * {@link Calendar#setContainerDataSource(com.vaadin.data.Container.Indexed, Object, Object, Object, Object, Object)} + * + * Please note that the container must be sorted by date! + * + * @param container + * The container to use as a datasource + */ + public void setContainerDataSource(Container.Indexed container) { + ContainerEventProvider provider = new ContainerEventProvider(container); + provider.addEventSetChangeListener( + new CalendarEventProvider.EventSetChangeListener() { + @Override + public void eventSetChange( + EventSetChangeEvent changeEvent) { + // Repaint if events change + markAsDirty(); + } + }); + provider.addEventChangeListener(new EventChangeListener() { + @Override + public void eventChange(EventChangeEvent changeEvent) { + // Repaint if event changes + markAsDirty(); + } + }); + setEventProvider(provider); + } + + /** + * Sets a container as a data source for the events in the calendar. + * Equivalent for doing + * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code> + * + * Please note that the container must be sorted by date! + * + * @param container + * The container to use as a data source + * @param captionProperty + * The property that has the caption, null if no caption property + * is present + * @param descriptionProperty + * The property that has the description, null if no description + * property is present + * @param startDateProperty + * The property that has the starting date + * @param endDateProperty + * The property that has the ending date + * @param styleNameProperty + * The property that has the stylename, null if no stylname + * property is present + */ + public void setContainerDataSource(Container.Indexed container, + Object captionProperty, Object descriptionProperty, + Object startDateProperty, Object endDateProperty, + Object styleNameProperty) { + ContainerEventProvider provider = new ContainerEventProvider(container); + provider.setCaptionProperty(captionProperty); + provider.setDescriptionProperty(descriptionProperty); + provider.setStartDateProperty(startDateProperty); + provider.setEndDateProperty(endDateProperty); + provider.setStyleNameProperty(styleNameProperty); + provider.addEventSetChangeListener( + new CalendarEventProvider.EventSetChangeListener() { + @Override + public void eventSetChange( + EventSetChangeEvent changeEvent) { + // Repaint if events change + markAsDirty(); + } + }); + provider.addEventChangeListener(new EventChangeListener() { + @Override + public void eventChange(EventChangeEvent changeEvent) { + // Repaint if event changes + markAsDirty(); + } + }); + setEventProvider(provider); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + return getEventProvider().getEvents(startDate, endDate); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + if (getEventProvider() instanceof CalendarEditableEventProvider) { + CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider(); + provider.addEvent(event); + markAsDirty(); + } else { + throw new UnsupportedOperationException( + "Event provider does not support adding events"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + if (getEventProvider() instanceof CalendarEditableEventProvider) { + CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider(); + provider.removeEvent(event); + markAsDirty(); + } else { + throw new UnsupportedOperationException( + "Event provider does not support removing events"); + } + } + + /** + * Adds an action handler to the calender that handles event produced by the + * context menu. + * + * <p> + * The {@link Handler#getActions(Object, Object)} parameters depend on what + * view the Calendar is in: + * <ul> + * <li>If the Calendar is in <i>Day or Week View</i> then the target + * parameter will be a {@link CalendarDateRange} with a range of + * half-an-hour. The {@link Handler#getActions(Object, Object)} method will + * be called once per half-hour slot.</li> + * <li>If the Calendar is in <i>Month View</i> then the target parameter + * will be a {@link CalendarDateRange} with a range of one day. The + * {@link Handler#getActions(Object, Object)} will be called once for each + * day. + * </ul> + * The Dates passed into the {@link CalendarDateRange} are in the same + * timezone as the calendar is. + * </p> + * + * <p> + * The {@link Handler#handleAction(Action, Object, Object)} parameters + * depend on what the context menu is called upon: + * <ul> + * <li>If the context menu is called upon an event then the target parameter + * is the event, i.e. instanceof {@link CalendarEvent}</li> + * <li>If the context menu is called upon an empty slot then the target is a + * {@link Date} representing that slot + * </ul> + * </p> + */ + @Override + public void addActionHandler(Handler actionHandler) { + if (actionHandler != null) { + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper<Action>(); + } + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + markAsDirty(); + } + } + } + + /** + * Is the calendar in a mode where all days of the month is shown + * + * @return Returns true if calendar is in monthly mode and false if it is in + * weekly mode + */ + public boolean isMonthlyMode() { + CalendarState state = getState(false); + if (state.days != null) { + return state.days.size() > 7; + } else { + // Default mode + return true; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.Action.Container#removeActionHandler(com.vaadin.event + * .Action.Handler) + */ + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + actionHandlers.remove(actionHandler); + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + markAsDirty(); + } + } + + private class CalendarServerRpcImpl implements CalendarServerRpc { + + @Override + public void eventMove(int eventIndex, String newDate) { + if (!isClientChangeAllowed()) { + return; + } + if (newDate != null) { + try { + Date d = df_date_time.parse(newDate); + if (eventIndex >= 0 && eventIndex < events.size() + && events.get(eventIndex) != null) { + fireEventMove(eventIndex, d); + } + } catch (ParseException e) { + getLogger().log(Level.WARNING, e.getMessage()); + } + } + } + + @Override + public void rangeSelect(String range) { + if (!isClientChangeAllowed()) { + return; + } + + if (range != null && range.length() > 14 && range.contains("TO")) { + String[] dates = range.split("TO"); + try { + Date d1 = df_date.parse(dates[0]); + Date d2 = df_date.parse(dates[1]); + + fireRangeSelect(d1, d2, true); + + } catch (ParseException e) { + // NOP + } + } else if (range != null && range.length() > 12 + && range.contains(":")) { + String[] dates = range.split(":"); + if (dates.length == 3) { + try { + Date d = df_date.parse(dates[0]); + currentCalendar.setTime(d); + int startMinutes = Integer.parseInt(dates[1]); + int endMinutes = Integer.parseInt(dates[2]); + currentCalendar.add(java.util.Calendar.MINUTE, + startMinutes); + Date start = currentCalendar.getTime(); + currentCalendar.add(java.util.Calendar.MINUTE, + endMinutes - startMinutes); + Date end = currentCalendar.getTime(); + fireRangeSelect(start, end, false); + } catch (ParseException e) { + // NOP + } catch (NumberFormatException e) { + // NOP + } + } + } + } + + @Override + public void forward() { + fireEvent(new ForwardEvent(Calendar.this)); + } + + @Override + public void backward() { + fireEvent(new BackwardEvent(Calendar.this)); + } + + @Override + public void dateClick(String date) { + if (date != null && date.length() > 6) { + try { + Date d = df_date.parse(date); + fireDateClick(d); + } catch (ParseException e) { + } + } + } + + @Override + public void weekClick(String event) { + if (event.length() > 0 && event.contains("w")) { + String[] splitted = event.split("w"); + if (splitted.length == 2) { + try { + int yr = Integer.parseInt(splitted[0]); + int week = Integer.parseInt(splitted[1]); + fireWeekClick(week, yr); + } catch (NumberFormatException e) { + // NOP + } + } + } + } + + @Override + public void eventClick(int eventIndex) { + if (!isEventClickAllowed()) { + return; + } + if (eventIndex >= 0 && eventIndex < events.size() + && events.get(eventIndex) != null) { + fireEventClick(eventIndex); + } + } + + @Override + public void eventResize(int eventIndex, String newStartDate, + String newEndDate) { + if (!isClientChangeAllowed()) { + return; + } + if (newStartDate != null && !"".equals(newStartDate) + && newEndDate != null && !"".equals(newEndDate)) { + try { + Date newStartTime = df_date_time.parse(newStartDate); + Date newEndTime = df_date_time.parse(newEndDate); + + fireEventResize(eventIndex, newStartTime, newEndTime); + } catch (ParseException e) { + // NOOP + } + } + } + + @Override + public void scroll(int scrollPosition) { + scrollTop = scrollPosition; + markAsDirty(); + } + + @Override + public void actionOnEmptyCell(String actionKey, String startDate, + String endDate) { + Action action = actionMapper.get(actionKey); + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + try { + Date start = formatter.parse(startDate); + for (Action.Handler ah : actionHandlers) { + ah.handleAction(action, Calendar.this, start); + } + + } catch (ParseException e) { + getLogger().log(Level.WARNING, + "Could not parse action date string"); + } + + } + + @Override + public void actionOnEvent(String actionKey, String startDate, + String endDate, int eventIndex) { + Action action = actionMapper.get(actionKey); + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + for (Action.Handler ah : actionHandlers) { + ah.handleAction(action, Calendar.this, events.get(eventIndex)); + } + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.server.VariableOwner#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + /* + * Only defined to fulfill the LegacyComponent interface used for + * calendar drag & drop. No implementation required. + */ + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.LegacyComponent#paintContent(com.vaadin.server.PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + } + + /** + * Sets whether the event captions are rendered as HTML. + * <p> + * If set to true, the captions are rendered in the browser as HTML and the + * developer is responsible for ensuring no harmful HTML is used. If set to + * false, the caption is rendered in the browser as plain text. + * <p> + * The default is false, i.e. to render that caption as plain text. + * + * @param captionAsHtml + * true if the captions are rendered as HTML, false if rendered + * as plain text + */ + public void setEventCaptionAsHtml(boolean eventCaptionAsHtml) { + getState().eventCaptionAsHtml = eventCaptionAsHtml; + } + + /** + * Checks whether event captions are rendered as HTML + * <p> + * The default is false, i.e. to render that caption as plain text. + * + * @return true if the captions are rendered as HTML, false if rendered as + * plain text + */ + public boolean isEventCaptionAsHtml() { + return getState(false).eventCaptionAsHtml; + } + + @Override + public void readDesign(Element design, DesignContext designContext) { + super.readDesign(design, designContext); + + Attributes attr = design.attributes(); + if (design.hasAttr("time-format")) { + setTimeFormat(TimeFormat.valueOf( + "Format" + design.attr("time-format").toUpperCase())); + } + + if (design.hasAttr("start-date")) { + setStartDate(DesignAttributeHandler.readAttribute("start-date", + attr, Date.class)); + } + if (design.hasAttr("end-date")) { + setEndDate(DesignAttributeHandler.readAttribute("end-date", attr, + Date.class)); + } + }; + + @Override + public void writeDesign(Element design, DesignContext designContext) { + super.writeDesign(design, designContext); + + if (currentTimeFormat != null) { + design.attr("time-format", + (currentTimeFormat == TimeFormat.Format12H ? "12h" + : "24h")); + } + if (startDate != null) { + design.attr("start-date", df_date.format(getStartDate())); + } + if (endDate != null) { + design.attr("end-date", df_date.format(getEndDate())); + } + if (!getTimeZone().equals(TimeZone.getDefault())) { + design.attr("time-zone", getTimeZone().getID()); + } + } + + @Override + protected Collection<String> getCustomAttributes() { + Collection<String> customAttributes = super.getCustomAttributes(); + customAttributes.add("time-format"); + customAttributes.add("start-date"); + customAttributes.add("end-date"); + return customAttributes; + } + + /** + * Allow setting first day of week independent of Locale. Set to null if you + * want first day of week being defined by the locale + * + * @since 7.6 + * @param dayOfWeek + * any of java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY + * or null to revert to default first day of week by locale + */ + public void setFirstDayOfWeek(Integer dayOfWeek) { + int minimalSupported = java.util.Calendar.SUNDAY; + int maximalSupported = java.util.Calendar.SATURDAY; + if (dayOfWeek != null && (dayOfWeek < minimalSupported + || dayOfWeek > maximalSupported)) { + throw new IllegalArgumentException(String.format( + "Day of week must be between %s and %s. Actually received: %s", + minimalSupported, maximalSupported, dayOfWeek)); + } + customFirstDayOfWeek = dayOfWeek; + markAsDirty(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/ColorPicker.java b/compatibility-server/src/main/java/com/vaadin/ui/ColorPicker.java new file mode 100644 index 0000000000..67002373d0 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/ColorPicker.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2016 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.ui; + +import com.vaadin.shared.ui.colorpicker.Color; + +/** + * A class that defines default (button-like) implementation for a color picker + * component. + * + * @since 7.0.0 + * + * @see ColorPickerArea + * + */ +public class ColorPicker extends AbstractColorPicker { + + /** + * Instantiates a new color picker. + */ + public ColorPicker() { + super(); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * caption of the color select popup + */ + public ColorPicker(String popupCaption) { + super(popupCaption); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * caption of the color select popup + * @param initialColor + * the initial color + */ + public ColorPicker(String popupCaption, Color initialColor) { + super(popupCaption, initialColor); + setDefaultCaptionEnabled(true); + } + + @Override + protected void setDefaultStyles() { + setPrimaryStyleName(STYLENAME_BUTTON); + addStyleName(STYLENAME_DEFAULT); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/ColorPickerArea.java b/compatibility-server/src/main/java/com/vaadin/ui/ColorPickerArea.java new file mode 100644 index 0000000000..c4f3971259 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/ColorPickerArea.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2016 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.ui; + +import com.vaadin.shared.ui.colorpicker.Color; + +/** + * A class that defines area-like implementation for a color picker component. + * + * @since 7.0.0 + * + * @see ColorPicker + * + */ +public class ColorPickerArea extends AbstractColorPicker { + + /** + * Instantiates a new color picker. + */ + public ColorPickerArea() { + super(); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * caption of the color select popup + */ + public ColorPickerArea(String popupCaption) { + super(popupCaption); + } + + /** + * Instantiates a new color picker. + * + * @param popupCaption + * caption of the color select popup + * @param initialColor + * the initial color + */ + public ColorPickerArea(String popupCaption, Color initialColor) { + super(popupCaption, initialColor); + setDefaultCaptionEnabled(false); + } + + @Override + protected void setDefaultStyles() { + // state already has correct default + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + + if ("".equals(getState().height)) { + getState().height = "30px"; + } + if ("".equals(getState().width)) { + getState().width = "30px"; + } + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/ComboBox.java b/compatibility-server/src/main/java/com/vaadin/ui/ComboBox.java new file mode 100644 index 0000000000..a78823d9a3 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/ComboBox.java @@ -0,0 +1,925 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.event.FieldEvents; +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.Resource; +import com.vaadin.shared.ui.combobox.ComboBoxServerRpc; +import com.vaadin.shared.ui.combobox.ComboBoxState; +import com.vaadin.shared.ui.combobox.FilteringMode; + +/** + * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's + * turned of by default to avoid mistakes. Items are filtered based on user + * input, and loaded dynamically ("lazy-loading") from the server. You can turn + * on newItemsAllowed and change filtering mode (and also turn it off), but you + * can not turn on multi-select mode. + * + */ +@SuppressWarnings("serial") +public class ComboBox extends AbstractSelect + implements AbstractSelect.Filtering, FieldEvents.BlurNotifier, + FieldEvents.FocusNotifier { + + /** + * ItemStyleGenerator can be used to add custom styles to combo box items + * shown in the popup. The CSS class name that will be added to the item + * style names is <tt>v-filterselect-item-[style name]</tt>. + * + * @since 7.5.6 + * @see ComboBox#setItemStyleGenerator(ItemStyleGenerator) + */ + public interface ItemStyleGenerator extends Serializable { + + /** + * Called by ComboBox when an item is painted. + * + * @param source + * the source combo box + * @param itemId + * The itemId of the item to be painted. Can be + * <code>null</code> if null selection is allowed. + * @return The style name to add to this item. (the CSS class name will + * be v-filterselect-item-[style name] + */ + public String getStyle(ComboBox source, Object itemId); + } + + private ComboBoxServerRpc rpc = new ComboBoxServerRpc() { + @Override + public void createNewItem(String itemValue) { + if (isNewItemsAllowed()) { + // New option entered (and it is allowed) + if (itemValue != null && itemValue.length() > 0) { + getNewItemHandler().addNewItem(itemValue); + // rebuild list + filterstring = null; + prevfilterstring = null; + } + } + } + + @Override + public void setSelectedItem(String item) { + if (item == null) { + setValue(null, true); + } else { + final Object id = itemIdMapper.get(item); + if (id != null && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + + @Override + public void requestPage(String filter, int page) { + filterstring = filter; + if (filterstring != null) { + filterstring = filterstring.toLowerCase(getLocale()); + } + currentPage = page; + + // TODO this should trigger a data-only update instead of a full + // repaint + requestRepaint(); + } + }; + + FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl( + this) { + @Override + protected void fireEvent(Component.Event event) { + ComboBox.this.fireEvent(event); + } + }; + + // Current page when the user is 'paging' trough options + private int currentPage = -1; + + private String filterstring; + private String prevfilterstring; + + /** + * Number of options that pass the filter, excluding the null item if any. + */ + private int filteredSize; + + /** + * Cache of filtered options, used only by the in-memory filtering system. + */ + private List<Object> filteredOptions; + + /** + * Flag to indicate that request repaint is called by filter request only + */ + private boolean optionRequest; + + /** + * True while painting to suppress item set change notifications that could + * be caused by temporary filtering. + */ + private boolean isPainting; + + /** + * Flag to indicate whether to scroll the selected item visible (select the + * page on which it is) when opening the popup or not. Only applies to + * single select mode. + * + * This requires finding the index of the item, which can be expensive in + * many large lazy loading containers. + */ + private boolean scrollToSelectedItem = true; + + private ItemStyleGenerator itemStyleGenerator = null; + + public ComboBox() { + init(); + } + + public ComboBox(String caption, Collection<?> options) { + super(caption, options); + init(); + } + + public ComboBox(String caption, Container dataSource) { + super(caption, dataSource); + init(); + } + + public ComboBox(String caption) { + super(caption); + init(); + } + + /** + * Initialize the ComboBox with default settings and register client to + * server RPC implementation. + */ + private void init() { + registerRpc(rpc); + registerRpc(focusBlurRpc); + + setNewItemsAllowed(false); + setImmediate(true); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return getState(false).inputPrompt; + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the + * select would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + * the desired input prompt, or null to disable + */ + public void setInputPrompt(String inputPrompt) { + getState().inputPrompt = inputPrompt; + } + + private boolean isFilteringNeeded() { + return filterstring != null && filterstring.length() > 0 + && getFilteringMode() != FilteringMode.OFF; + } + + /** + * A class representing an item in a ComboBox for server to client + * communication. This class is for internal use only and subject to change. + * + * @since + */ + private static class ComboBoxItem implements Serializable { + String key = ""; + String caption = ""; + String style = null; + Resource icon = null; + + // constructor for a null item + public ComboBoxItem() { + } + + public ComboBoxItem(String key, String caption, String style, + Resource icon) { + this.key = key; + this.caption = caption; + this.style = style; + this.icon = icon; + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + isPainting = true; + try { + // clear caption change listeners + getCaptionChangeListener().clear(); + + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + boolean needNullSelectOption = false; + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + needNullSelectOption = (getNullSelectionItemId() == null); + if (!needNullSelectOption) { + target.addAttribute("nullselectitem", true); + } + } + + // Constructs selected keys array + String[] selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + + // Paints the options and create array of selected id keys + int keyIndex = 0; + + if (currentPage < 0) { + optionRequest = false; + currentPage = 0; + filterstring = ""; + } + + boolean nullFilteredOut = isFilteringNeeded(); + // null option is needed and not filtered out, even if not on + // current page + boolean nullOptionVisible = needNullSelectOption + && !nullFilteredOut; + + // first try if using container filters is possible + List<?> options = getOptionsWithFilter(nullOptionVisible); + if (null == options) { + // not able to use container filters, perform explicit in-memory + // filtering + options = getFilteredOptions(); + filteredSize = options.size(); + options = sanitetizeList(options, nullOptionVisible); + } + + final boolean paintNullSelection = needNullSelectOption + && currentPage == 0 && !nullFilteredOut; + + List<ComboBoxItem> items = new ArrayList<ComboBoxItem>(); + + if (paintNullSelection) { + ComboBoxItem item = new ComboBoxItem(); + item.style = getItemStyle(null); + items.add(item); + } + + final Iterator<?> i = options.iterator(); + // Paints the available selection options from data source + + while (i.hasNext()) { + + final Object id = i.next(); + + if (!isNullSelectionAllowed() && id != null + && id.equals(getNullSelectionItemId()) + && !isSelected(id)) { + continue; + } + + // Gets the option attribute values + final String key = itemIdMapper.key(id); + final String caption = getItemCaption(id); + final Resource icon = getItemIcon(id); + + getCaptionChangeListener().addNotifierForItem(id); + + // Prepare to paint the option + ComboBoxItem item = new ComboBoxItem(key, caption, + getItemStyle(id), icon); + items.add(item); + if (keyIndex < selectedKeys.length && isSelected(id)) { + // at most one item can be selected at a time + selectedKeys[keyIndex++] = key; + } + } + + // paint the items + target.startTag("options"); + for (ComboBoxItem item : items) { + target.startTag("so"); + if (item.icon != null) { + target.addAttribute("icon", item.icon); + } + target.addAttribute("caption", item.caption); + target.addAttribute("key", item.key); + if (item.style != null) { + target.addAttribute("style", item.style); + } + + target.endTag("so"); + } + target.endTag("options"); + + target.addAttribute("totalitems", + size() + (needNullSelectOption ? 1 : 0)); + if (filteredSize > 0 || nullOptionVisible) { + target.addAttribute("totalMatches", + filteredSize + (nullOptionVisible ? 1 : 0)); + } + + // Paint variables + target.addVariable(this, "selected", selectedKeys); + if (getValue() != null && selectedKeys[0] == null) { + // not always available, e.g. scrollToSelectedIndex=false + // Give the caption for selected item still, not to make it look + // like there is no selection at all + target.addAttribute("selectedCaption", + getItemCaption(getValue())); + } + if (isNewItemsAllowed()) { + target.addVariable(this, "newitem", ""); + } + + target.addVariable(this, "filter", filterstring); + target.addVariable(this, "page", currentPage); + + currentPage = -1; // current page is always set by client + + optionRequest = true; + } finally { + isPainting = false; + } + + } + + private String getItemStyle(Object itemId) throws PaintException { + if (itemStyleGenerator != null) { + return itemStyleGenerator.getStyle(this, itemId); + } + return null; + } + + /** + * Sets whether it is possible to input text into the field or whether the + * field area of the component is just used to show what is selected. By + * disabling text input, the comboBox will work in the same way as a + * {@link NativeSelect} + * + * @see #isTextInputAllowed() + * + * @param textInputAllowed + * true to allow entering text, false to just show the current + * selection + */ + public void setTextInputAllowed(boolean textInputAllowed) { + getState().textInputAllowed = textInputAllowed; + } + + /** + * Returns true if the user can enter text into the field to either filter + * the selections or enter a new value if {@link #isNewItemsAllowed()} + * returns true. If text input is disabled, the comboBox will work in the + * same way as a {@link NativeSelect} + * + * @return + */ + public boolean isTextInputAllowed() { + return getState(false).textInputAllowed; + } + + @Override + protected ComboBoxState getState() { + return (ComboBoxState) super.getState(); + } + + @Override + protected ComboBoxState getState(boolean markAsDirty) { + return (ComboBoxState) super.getState(markAsDirty); + } + + /** + * Returns the filtered options for the current page using a container + * filter. + * + * As a size effect, {@link #filteredSize} is set to the total number of + * items passing the filter. + * + * The current container must be {@link Filterable} and {@link Indexed}, and + * the filtering mode must be suitable for container filtering (tested with + * {@link #canUseContainerFilter()}). + * + * Use {@link #getFilteredOptions()} and + * {@link #sanitetizeList(List, boolean)} if this is not the case. + * + * @param needNullSelectOption + * @return filtered list of options (may be empty) or null if cannot use + * container filters + */ + protected List<?> getOptionsWithFilter(boolean needNullSelectOption) { + Container container = getContainerDataSource(); + + if (getPageLength() == 0 && !isFilteringNeeded()) { + // no paging or filtering: return all items + filteredSize = container.size(); + assert filteredSize >= 0; + return new ArrayList<Object>(container.getItemIds()); + } + + if (!(container instanceof Filterable) + || !(container instanceof Indexed) + || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) { + return null; + } + + Filterable filterable = (Filterable) container; + + Filter filter = buildFilter(filterstring, getFilteringMode()); + + // adding and removing filters leads to extraneous item set + // change events from the underlying container, but the ComboBox does + // not process or propagate them based on the flag filteringContainer + if (filter != null) { + filterable.addContainerFilter(filter); + } + + // try-finally to ensure that the filter is removed from container even + // if a exception is thrown... + try { + Indexed indexed = (Indexed) container; + + int indexToEnsureInView = -1; + + // if not an option request (item list when user changes page), go + // to page with the selected item after filtering if accepted by + // filter + Object selection = getValue(); + if (isScrollToSelectedItem() && !optionRequest + && selection != null) { + // ensure proper page + indexToEnsureInView = indexed.indexOfId(selection); + } + + filteredSize = container.size(); + assert filteredSize >= 0; + currentPage = adjustCurrentPage(currentPage, needNullSelectOption, + indexToEnsureInView, filteredSize); + int first = getFirstItemIndexOnCurrentPage(needNullSelectOption, + filteredSize); + int last = getLastItemIndexOnCurrentPage(needNullSelectOption, + filteredSize, first); + + // Compute the number of items to fetch from the indexes given or + // based on the filtered size of the container + int lastItemToFetch = Math.min(last, filteredSize - 1); + int nrOfItemsToFetch = (lastItemToFetch + 1) - first; + + List<?> options = indexed.getItemIds(first, nrOfItemsToFetch); + + return options; + } finally { + // to the outside, filtering should not be visible + if (filter != null) { + filterable.removeContainerFilter(filter); + } + } + } + + /** + * Constructs a filter instance to use when using a Filterable container in + * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode. + * + * Note that the client side implementation expects the filter string to + * apply to the item caption string it sees, so changing the behavior of + * this method can cause problems. + * + * @param filterString + * @param filteringMode + * @return + */ + protected Filter buildFilter(String filterString, + FilteringMode filteringMode) { + Filter filter = null; + + if (null != filterString && !"".equals(filterString)) { + switch (filteringMode) { + case OFF: + break; + case STARTSWITH: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, true); + break; + case CONTAINS: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, false); + break; + } + } + return filter; + } + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + if (!isPainting) { + super.containerItemSetChange(event); + } + } + + /** + * Makes correct sublist of given list of options. + * + * If paint is not an option request (affected by page or filter change), + * page will be the one where possible selection exists. + * + * Detects proper first and last item in list to return right page of + * options. Also, if the current page is beyond the end of the list, it will + * be adjusted. + * + * @param options + * @param needNullSelectOption + * flag to indicate if nullselect option needs to be taken into + * consideration + */ + private List<?> sanitetizeList(List<?> options, + boolean needNullSelectOption) { + + if (getPageLength() != 0 && options.size() > getPageLength()) { + + int indexToEnsureInView = -1; + + // if not an option request (item list when user changes page), go + // to page with the selected item after filtering if accepted by + // filter + Object selection = getValue(); + if (isScrollToSelectedItem() && !optionRequest + && selection != null) { + // ensure proper page + indexToEnsureInView = options.indexOf(selection); + } + + int size = options.size(); + currentPage = adjustCurrentPage(currentPage, needNullSelectOption, + indexToEnsureInView, size); + int first = getFirstItemIndexOnCurrentPage(needNullSelectOption, + size); + int last = getLastItemIndexOnCurrentPage(needNullSelectOption, size, + first); + return options.subList(first, last + 1); + } else { + return options; + } + } + + /** + * Returns the index of the first item on the current page. The index is to + * the underlying (possibly filtered) contents. The null item, if any, does + * not have an index but takes up a slot on the first page. + * + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param size + * number of items after filtering (not including the null item, + * if any) + * @return first item to show on the UI (index to the filtered list of + * options, not taking the null item into consideration if any) + */ + private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption, + int size) { + // Not all options are visible, find out which ones are on the + // current "page". + int first = currentPage * getPageLength(); + if (needNullSelectOption && currentPage > 0) { + first--; + } + return first; + } + + /** + * Returns the index of the last item on the current page. The index is to + * the underlying (possibly filtered) contents. If needNullSelectOption is + * true, the null item takes up the first slot on the first page, + * effectively reducing the first page size by one. + * + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param size + * number of items after filtering (not including the null item, + * if any) + * @param first + * index in the filtered view of the first item of the page + * @return index in the filtered view of the last item on the page + */ + private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption, + int size, int first) { + // page length usable for non-null items + int effectivePageLength = getPageLength() + - (needNullSelectOption && (currentPage == 0) ? 1 : 0); + return Math.min(size - 1, first + effectivePageLength - 1); + } + + /** + * Adjusts the index of the current page if necessary: make sure the current + * page is not after the end of the contents, and optionally go to the page + * containg a specific item. There are no side effects but the adjusted page + * index is returned. + * + * @param page + * page number to use as the starting point + * @param needNullSelectOption + * true if a null option should be shown before any other options + * (takes up the first slot on the first page, not counted in + * index) + * @param indexToEnsureInView + * index of an item that should be included on the page (in the + * data set, not counting the null item if any), -1 for none + * @param size + * number of items after filtering (not including the null item, + * if any) + */ + private int adjustCurrentPage(int page, boolean needNullSelectOption, + int indexToEnsureInView, int size) { + if (indexToEnsureInView != -1) { + int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0)) + / getPageLength(); + page = newPage; + } + // adjust the current page if beyond the end of the list + if (page * getPageLength() > size) { + page = (size + (needNullSelectOption ? 1 : 0)) / getPageLength(); + } + return page; + } + + /** + * Filters the options in memory and returns the full filtered list. + * + * This can be less efficient than using container filters, so use + * {@link #getOptionsWithFilter(boolean)} if possible (filterable container + * and suitable item caption mode etc.). + * + * @return + */ + protected List<?> getFilteredOptions() { + if (!isFilteringNeeded()) { + prevfilterstring = null; + filteredOptions = new LinkedList<Object>(getItemIds()); + return filteredOptions; + } + + if (filterstring.equals(prevfilterstring)) { + return filteredOptions; + } + + Collection<?> items; + if (prevfilterstring != null + && filterstring.startsWith(prevfilterstring)) { + items = filteredOptions; + } else { + items = getItemIds(); + } + prevfilterstring = filterstring; + + filteredOptions = new LinkedList<Object>(); + for (final Iterator<?> it = items.iterator(); it.hasNext();) { + final Object itemId = it.next(); + String caption = getItemCaption(itemId); + if (caption == null || caption.equals("")) { + continue; + } else { + caption = caption.toLowerCase(getLocale()); + } + switch (getFilteringMode()) { + case CONTAINS: + if (caption.indexOf(filterstring) > -1) { + filteredOptions.add(itemId); + } + break; + case STARTSWITH: + default: + if (caption.startsWith(filterstring)) { + filteredOptions.add(itemId); + } + break; + } + } + + return filteredOptions; + } + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Not calling super.changeVariables due the history of select + // component hierarchy + + // all the client to server requests are now handled by RPC + } + + @Override + public void setFilteringMode(FilteringMode filteringMode) { + getState().filteringMode = filteringMode; + } + + @Override + public FilteringMode getFilteringMode() { + return getState(false).filteringMode; + } + + @Override + public void addBlurListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeBlurListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addFocusListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeFocusListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * ComboBox does not support multi select mode. + * + * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean) + * @throws UnsupportedOperationException + * if trying to activate multiselect mode + */ + @Deprecated + @Override + public void setMultiSelect(boolean multiSelect) { + if (multiSelect) { + throw new UnsupportedOperationException( + "Multiselect not supported"); + } + } + + /** + * ComboBox does not support multi select mode. + * + * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * + * @see com.vaadin.ui.AbstractSelect#isMultiSelect() + * + * @return false + */ + @Deprecated + @Override + public boolean isMultiSelect() { + return false; + } + + /** + * Returns the page length of the suggestion popup. + * + * @return the pageLength + */ + public int getPageLength() { + return getState(false).pageLength; + } + + /** + * Returns the suggestion pop-up's width as a CSS string. + * + * @see #setPopupWidth + * @since 7.7 + */ + public String getPopupWidth() { + return getState(false).suggestionPopupWidth; + } + + /** + * Sets the page length for the suggestion popup. Setting the page length to + * 0 will disable suggestion popup paging (all items visible). + * + * @param pageLength + * the pageLength to set + */ + public void setPageLength(int pageLength) { + getState().pageLength = pageLength; + } + + /** + * Sets the suggestion pop-up's width as a CSS string. By using relative + * units (e.g. "50%") it's possible to set the popup's width relative to the + * ComboBox itself. + * + * @see #getPopupWidth() + * @since 7.7 + * @param width + * the width + */ + public void setPopupWidth(String width) { + getState().suggestionPopupWidth = width; + } + + /** + * Sets whether to scroll the selected item visible (directly open the page + * on which it is) when opening the combo box popup or not. Only applies to + * single select mode. + * + * This requires finding the index of the item, which can be expensive in + * many large lazy loading containers. + * + * @param scrollToSelectedItem + * true to find the page with the selected item when opening the + * selection popup + */ + public void setScrollToSelectedItem(boolean scrollToSelectedItem) { + this.scrollToSelectedItem = scrollToSelectedItem; + } + + /** + * Returns true if the select should find the page with the selected item + * when opening the popup (single select combo box only). + * + * @see #setScrollToSelectedItem(boolean) + * + * @return true if the page with the selected item will be shown when + * opening the popup + */ + public boolean isScrollToSelectedItem() { + return scrollToSelectedItem; + } + + /** + * Sets the item style generator that is used to produce custom styles for + * showing items in the popup. The CSS class name that will be added to the + * item style names is <tt>v-filterselect-item-[style name]</tt>. + * + * @param itemStyleGenerator + * the item style generator to set, or <code>null</code> to not + * use any custom item styles + * @since 7.5.6 + */ + public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) { + this.itemStyleGenerator = itemStyleGenerator; + markAsDirty(); + } + + /** + * Gets the currently used item style generator. + * + * @return the itemStyleGenerator the currently used item style generator, + * or <code>null</code> if no generator is used + * @since 7.5.6 + */ + public ItemStyleGenerator getItemStyleGenerator() { + return itemStyleGenerator; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/Tree.java b/compatibility-server/src/main/java/com/vaadin/ui/Tree.java new file mode 100644 index 0000000000..5da499f94e --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/Tree.java @@ -0,0 +1,1984 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.StringTokenizer; + +import org.jsoup.nodes.Element; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ContextClickEvent; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.Resource; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.tree.TreeConstants; +import com.vaadin.shared.ui.tree.TreeServerRpc; +import com.vaadin.shared.ui.tree.TreeState; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; +import com.vaadin.util.ReflectTools; + +/** + * Tree component. A Tree can be used to select an item (or multiple items) from + * a hierarchical set of items. + * + * @author Vaadin Ltd. + * @since 3.0 + */ +@SuppressWarnings({ "serial", "deprecation" }) +public class Tree extends AbstractSelect implements Container.Hierarchical, + Action.Container, ItemClickNotifier, DragSource, DropTarget { + + /** + * ContextClickEvent for the Tree Component. + * + * @since 7.6 + */ + public static class TreeContextClickEvent extends ContextClickEvent { + + private final Object itemId; + + public TreeContextClickEvent(Tree source, Object itemId, + MouseEventDetails mouseEventDetails) { + super(source, mouseEventDetails); + this.itemId = itemId; + } + + @Override + public Tree getComponent() { + return (Tree) super.getComponent(); + } + + /** + * Returns the item id of context clicked row. + * + * @return item id of clicked row; <code>null</code> if no row is + * present at the location + */ + public Object getItemId() { + return itemId; + } + } + + /* Private members */ + + private static final String NULL_ALT_EXCEPTION_MESSAGE = "Parameter 'altText' needs to be non null"; + + /** + * Item icons alt texts. + */ + private final HashMap<Object, String> itemIconAlts = new HashMap<Object, String>(); + + /** + * Set of expanded nodes. + */ + private HashSet<Object> expanded = new HashSet<Object>(); + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * Is the tree selectable on the client side. + */ + private boolean selectable = true; + + /** + * Flag to indicate sub-tree loading + */ + private boolean partialUpdate = false; + + /** + * Holds a itemId which was recently expanded + */ + private Object expandedItemId; + + /** + * a flag which indicates initial paint. After this flag set true partial + * updates are allowed. + */ + private boolean initialPaint = true; + + /** + * Item tooltip generator + */ + private ItemDescriptionGenerator itemDescriptionGenerator; + + /** + * Supported drag modes for Tree. + */ + public enum TreeDragMode { + /** + * When drag mode is NONE, dragging from Tree is not supported. Browsers + * may still support selecting text/icons from Tree which can initiate + * HTML 5 style drag and drop operation. + */ + NONE, + /** + * When drag mode is NODE, users can initiate drag from Tree nodes that + * represent {@link Item}s in from the backed {@link Container}. + */ + NODE + // , SUBTREE + } + + private TreeDragMode dragMode = TreeDragMode.NONE; + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + /* Tree constructors */ + + /** + * Creates a new empty tree. + */ + public Tree() { + this(null); + + registerRpc(new TreeServerRpc() { + @Override + public void contextClick(String rowKey, MouseEventDetails details) { + fireEvent(new TreeContextClickEvent(Tree.this, + itemIdMapper.get(rowKey), details)); + } + }); + } + + /** + * Creates a new empty tree with caption. + * + * @param caption + */ + public Tree(String caption) { + this(caption, new HierarchicalContainer()); + } + + /** + * Creates a new tree with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Tree(String caption, Container dataSource) { + super(caption, dataSource); + } + + @Override + public void setItemIcon(Object itemId, Resource icon) { + setItemIcon(itemId, icon, ""); + } + + /** + * Sets the icon for an item. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param icon + * the icon to use or null. + * + * @param altText + * the alternative text for the icon + */ + public void setItemIcon(Object itemId, Resource icon, String altText) { + if (itemId != null) { + super.setItemIcon(itemId, icon); + + if (icon == null) { + itemIconAlts.remove(itemId); + } else if (altText == null) { + throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE); + } else { + itemIconAlts.put(itemId, altText); + } + markAsDirty(); + } + } + + /** + * Set the alternate text for an item. + * + * Used when the item has an icon. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param altText + * the alternative text for the icon + */ + public void setItemIconAlternateText(Object itemId, String altText) { + if (itemId != null) { + if (altText == null) { + throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE); + } else { + itemIconAlts.put(itemId, altText); + } + } + } + + /** + * Return the alternate text of an icon in a tree item. + * + * @param itemId + * Object with the ID of the item + * @return String with the alternate text of the icon, or null when no icon + * was set + */ + public String getItemIconAlternateText(Object itemId) { + String storedAlt = itemIconAlts.get(itemId); + return storedAlt == null ? "" : storedAlt; + } + + /* Expanding and collapsing */ + + /** + * Check is an item is expanded + * + * @param itemId + * the item id. + * @return true iff the item is expanded. + */ + public boolean isExpanded(Object itemId) { + return expanded.contains(itemId); + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @return True iff the expand operation succeeded + */ + public boolean expandItem(Object itemId) { + boolean success = expandItem(itemId, true); + markAsDirty(); + return success; + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @param sendChildTree + * flag to indicate if client needs subtree or not (may be + * cached) + * @return True if the expand operation succeeded + */ + private boolean expandItem(Object itemId, boolean sendChildTree) { + + // Succeeds if the node is already expanded + if (isExpanded(itemId)) { + return true; + } + + // Nodes that can not have children are not expandable + if (!areChildrenAllowed(itemId)) { + return false; + } + + // Expands + expanded.add(itemId); + + expandedItemId = itemId; + if (initialPaint) { + markAsDirty(); + } else if (sendChildTree) { + requestPartialRepaint(); + } + fireExpandEvent(itemId); + + return true; + } + + @Override + public void markAsDirty() { + super.markAsDirty(); + partialUpdate = false; + } + + private void requestPartialRepaint() { + super.markAsDirty(); + partialUpdate = true; + } + + /** + * Expands the items recursively + * + * Expands all the children recursively starting from an item. Operation + * succeeds only if all expandable items are expanded. + * + * @param startItemId + * @return True iff the expand operation succeeded + */ + public boolean expandItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Expands recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !expandItem(id, false)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + markAsDirty(); + return result; + } + + /** + * Collapses an item. + * + * @param itemId + * the item id. + * @return True iff the collapse operation succeeded + */ + public boolean collapseItem(Object itemId) { + + // Succeeds if the node is already collapsed + if (!isExpanded(itemId)) { + return true; + } + + // Collapse + expanded.remove(itemId); + markAsDirty(); + fireCollapseEvent(itemId); + + return true; + } + + /** + * Collapses the items recursively. + * + * Collapse all the children recursively starting from an item. Operation + * succeeds only if all expandable items are collapsed. + * + * @param startItemId + * @return True iff the collapse operation succeeded + */ + public boolean collapseItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Collapse recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !collapseItem(id)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + + return result; + } + + /** + * Returns the current selectable state. Selectable determines if the a node + * can be selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @return the current selectable state. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Sets the selectable state. Selectable determines if the a node can be + * selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @param selectable + * The new selectable state. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + markAsDirty(); + } + } + + /** + * Sets the behavior of the multiselect mode + * + * @param mode + * The mode to set + */ + public void setMultiselectMode(MultiSelectMode mode) { + if (multiSelectMode != mode && mode != null) { + multiSelectMode = mode; + markAsDirty(); + } + } + + /** + * Returns the mode the multiselect is in. The mode controls how + * multiselection can be done. + * + * @return The mode + */ + public MultiSelectMode getMultiselectMode() { + return multiSelectMode; + } + + /* Component API */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + if (variables.containsKey("clickedKey")) { + String key = (String) variables.get("clickedKey"); + + Object id = itemIdMapper.get(key); + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("clickEvent")); + Item item = getItem(id); + if (item != null) { + fireEvent(new ItemClickEvent(this, item, id, null, details)); + } + } + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Collapses the nodes + if (variables.containsKey("collapse")) { + final String[] keys = (String[]) variables.get("collapse"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null && isExpanded(id)) { + expanded.remove(id); + if (expandedItemId == id) { + expandedItemId = null; + } + fireCollapseEvent(id); + } + } + } + + // Expands the nodes + if (variables.containsKey("expand")) { + boolean sendChildTree = false; + if (variables.containsKey("requestChildTree")) { + sendChildTree = true; + } + final String[] keys = (String[]) variables.get("expand"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null) { + expandItem(id, sendChildTree); + } + } + } + + // AbstractSelect cannot handle multiselection so we handle + // it ourself + if (variables.containsKey("selected") && isMultiSelect() + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Selections are handled by the select component + super.changeVariables(source, variables); + + // Actions + if (variables.containsKey("action")) { + final StringTokenizer st = new StringTokenizer( + (String) variables.get("action"), ","); + if (st.countTokens() == 2) { + final Object itemId = itemIdMapper.get(st.nextToken()); + final Action action = actionMapper.get(st.nextToken()); + if (action != null && (itemId == null || containsId(itemId)) + && actionHandlers != null) { + for (Handler ah : actionHandlers) { + ah.handleAction(action, this, itemId); + } + } + } + } + } + + /** + * Handles the selection + * + * @param variables + * The variables sent to the server from the client + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + + // Converts the key-array to id-set + final LinkedList<Object> s = new LinkedList<Object>(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + markAsDirty(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + markAsDirty(); + return; + } + + setValue(s, true); + } + + /** + * Paints any needed component-specific things to the given UIDL stream. + * + * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + initialPaint = false; + + if (partialUpdate) { + target.addAttribute("partialUpdate", true); + target.addAttribute("rootKey", itemIdMapper.key(expandedItemId)); + } else { + getCaptionChangeListener().clear(); + + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // Paint tree attributes + if (isSelectable()) { + target.addAttribute("selectmode", + (isMultiSelect() ? "multi" : "single")); + if (isMultiSelect()) { + target.addAttribute("multiselectmode", + multiSelectMode.toString()); + } + } else { + target.addAttribute("selectmode", "none"); + } + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + } + + if (dragMode != TreeDragMode.NONE) { + target.addAttribute("dragMode", dragMode.ordinal()); + } + + if (isHtmlContentAllowed()) { + target.addAttribute(TreeConstants.ATTRIBUTE_HTML_ALLOWED, true); + } + + } + + // Initialize variables + final Set<Action> actionSet = new LinkedHashSet<Action>(); + + // rendered selectedKeys + LinkedList<String> selectedKeys = new LinkedList<String>(); + + final LinkedList<String> expandedKeys = new LinkedList<String>(); + + // Iterates through hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + Collection<?> ids; + if (partialUpdate) { + ids = getChildren(expandedItemId); + } else { + ids = rootItemIds(); + } + + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + + /* + * Body actions - Actions which has the target null and can be invoked + * by right clicking on the Tree body + */ + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + + // Getting action for the null item, which in this case + // means the body item + final Action[] aa = ah.getActions(null, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("alb", keys.toArray()); + } + + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + + // Closes node + if (!iteratorStack.isEmpty()) { + target.endTag("node"); + } + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + // Starts the item / node + final boolean isNode = areChildrenAllowed(itemId); + if (isNode) { + target.startTag("node"); + } else { + target.startTag("leaf"); + } + + if (itemStyleGenerator != null) { + String stylename = itemStyleGenerator.getStyle(this, + itemId); + if (stylename != null) { + target.addAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE, + stylename); + } + } + + if (itemDescriptionGenerator != null) { + String description = itemDescriptionGenerator + .generateDescription(this, itemId, null); + if (description != null && !description.equals("")) { + target.addAttribute("descr", description); + } + } + + // Adds the attributes + target.addAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION, + getItemCaption(itemId)); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON, + getItemIcon(itemId)); + target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT, + getItemIconAlternateText(itemId)); + } + final String key = itemIdMapper.key(itemId); + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + selectedKeys.add(key); + } + if (areChildrenAllowed(itemId) && isExpanded(itemId)) { + target.addAttribute("expanded", true); + expandedKeys.add(key); + } + + // Add caption change listener + getCaptionChangeListener().addNotifierForItem(itemId); + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + final Iterator<Action.Handler> ahi = actionHandlers + .iterator(); + while (ahi.hasNext()) { + final Action[] aa = ahi.next().getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + // Adds the children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId) + && areChildrenAllowed(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } else { + if (isNode) { + target.endTag("node"); + } else { + target.endTag("leaf"); + } + } + } + } + + // Actions + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + final Iterator<Action> i = actionSet.iterator(); + while (i.hasNext()) { + final Action a = i.next(); + target.startTag("action"); + if (a.getCaption() != null) { + target.addAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION, + a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON, + a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + + if (partialUpdate) { + partialUpdate = false; + } else { + // Selected + target.addVariable(this, "selected", + selectedKeys.toArray(new String[selectedKeys.size()])); + + // Expand and collapse + target.addVariable(this, "expand", new String[] {}); + target.addVariable(this, "collapse", new String[] {}); + + // New items + target.addVariable(this, "newitem", new String[] {}); + + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + + } + } + + /* Container.Hierarchical API */ + + /** + * Tests if the Item with given ID can have any children. + * + * @see com.vaadin.data.Container.Hierarchical#areChildrenAllowed(Object) + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + return ((Container.Hierarchical) items).areChildrenAllowed(itemId); + } + + /** + * Gets the IDs of all Items that are children of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getChildren(Object) + */ + @Override + public Collection<?> getChildren(Object itemId) { + return ((Container.Hierarchical) items).getChildren(itemId); + } + + /** + * Gets the ID of the parent Item of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getParent(Object) + */ + @Override + public Object getParent(Object itemId) { + return ((Container.Hierarchical) items).getParent(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> has child Items. + * + * @see com.vaadin.data.Container.Hierarchical#hasChildren(Object) + */ + @Override + public boolean hasChildren(Object itemId) { + return ((Container.Hierarchical) items).hasChildren(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> is a root Item. + * + * @see com.vaadin.data.Container.Hierarchical#isRoot(Object) + */ + @Override + public boolean isRoot(Object itemId) { + return ((Container.Hierarchical) items).isRoot(itemId); + } + + /** + * Gets the IDs of all Items in the container that don't have a parent. + * + * @see com.vaadin.data.Container.Hierarchical#rootItemIds() + */ + @Override + public Collection<?> rootItemIds() { + return ((Container.Hierarchical) items).rootItemIds(); + } + + /** + * Sets the given Item's capability to have children. + * + * @see com.vaadin.data.Container.Hierarchical#setChildrenAllowed(Object, + * boolean) + */ + @Override + public boolean setChildrenAllowed(Object itemId, + boolean areChildrenAllowed) { + final boolean success = ((Container.Hierarchical) items) + .setChildrenAllowed(itemId, areChildrenAllowed); + if (success) { + markAsDirty(); + } + return success; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object , + * java.lang.Object) + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + final boolean success = ((Container.Hierarchical) items) + .setParent(itemId, newParentId); + if (success) { + markAsDirty(); + } + return success; + } + + /* Overriding select behavior */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + newDataSource = new HierarchicalContainer(); + } + + // Assure that the data source is ordered by making unordered + // containers ordered by wrapping them + if (Container.Hierarchical.class + .isAssignableFrom(newDataSource.getClass())) { + super.setContainerDataSource(newDataSource); + } else { + super.setContainerDataSource( + new ContainerHierarchicalWrapper(newDataSource)); + } + + /* + * Ensure previous expanded items are cleaned up if they don't exist in + * the new container + */ + if (expanded != null) { + /* + * We need to check that the expanded-field is not null since + * setContainerDataSource() is called from the parent constructor + * (AbstractSelect()) and at that time the expanded field is not yet + * initialized. + */ + cleanupExpandedItems(); + } + + } + + @Override + public void containerItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + super.containerItemSetChange(event); + if (getContainerDataSource() instanceof Filterable) { + boolean hasFilters = !((Filterable) getContainerDataSource()) + .getContainerFilters().isEmpty(); + if (!hasFilters) { + /* + * If Container is not filtered then the itemsetchange is caused + * by either adding or removing items to the container. To + * prevent a memory leak we should cleanup the expanded list + * from items which was removed. + * + * However, there will still be a leak if the container is + * filtered to show only a subset of the items in the tree and + * later unfiltered items are removed from the container. In + * that case references to the unfiltered item ids will remain + * in the expanded list until the Tree instance is removed and + * the list is destroyed, or the container data source is + * replaced/updated. To force the removal of the removed items + * the application developer needs to a) remove the container + * filters temporarly or b) re-apply the container datasource + * using setContainerDataSource(getContainerDataSource()) + */ + cleanupExpandedItems(); + } + } + + } + + /* Expand event and listener */ + + /** + * Event to fired when a node is expanded. ExapandEvent is fired when a node + * is to be expanded. it can me used to dynamically fill the sub-nodes of + * the node. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public static class ExpandEvent extends Component.Event { + + private final Object expandedItemId; + + /** + * New instance of options change event + * + * @param source + * the Source of the event. + * @param expandedItemId + */ + public ExpandEvent(Component source, Object expandedItemId) { + super(source); + this.expandedItemId = expandedItemId; + } + + /** + * Node where the event occurred. + * + * @return the Source of the event. + */ + public Object getItemId() { + return expandedItemId; + } + } + + /** + * Expand event listener. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public interface ExpandListener extends Serializable { + + public static final Method EXPAND_METHOD = ReflectTools.findMethod( + ExpandListener.class, "nodeExpand", ExpandEvent.class); + + /** + * A node has been expanded. + * + * @param event + * the Expand event. + */ + public void nodeExpand(ExpandEvent event); + } + + /** + * Adds the expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addExpandListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addExpandListener(ExpandListener)} + **/ + @Deprecated + public void addListener(ExpandListener listener) { + addExpandListener(listener); + } + + /** + * Removes the expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeExpandListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeExpandListener(ExpandListener)} + **/ + @Deprecated + public void removeListener(ExpandListener listener) { + removeExpandListener(listener); + } + + /** + * Emits the expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /* Collapse event */ + + /** + * Collapse event + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public static class CollapseEvent extends Component.Event { + + private final Object collapsedItemId; + + /** + * New instance of options change event. + * + * @param source + * the Source of the event. + * @param collapsedItemId + */ + public CollapseEvent(Component source, Object collapsedItemId) { + super(source); + this.collapsedItemId = collapsedItemId; + } + + /** + * Gets tge Collapsed Item id. + * + * @return the collapsed item id. + */ + public Object getItemId() { + return collapsedItemId; + } + } + + /** + * Collapse event listener. + * + * @author Vaadin Ltd. + * @since 3.0 + */ + public interface CollapseListener extends Serializable { + + public static final Method COLLAPSE_METHOD = ReflectTools.findMethod( + CollapseListener.class, "nodeCollapse", CollapseEvent.class); + + /** + * A node has been collapsed. + * + * @param event + * the Collapse event. + */ + public void nodeCollapse(CollapseEvent event); + } + + /** + * Adds the collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addCollapseListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addCollapseListener(CollapseListener)} + **/ + @Deprecated + public void addListener(CollapseListener listener) { + addCollapseListener(listener); + } + + /** + * Removes the collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeCollapseListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeCollapseListener(CollapseListener)} + **/ + @Deprecated + public void removeListener(CollapseListener listener) { + removeCollapseListener(listener); + } + + /** + * Emits collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /* Action container */ + + /** + * Adds an action handler. + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + @Override + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper<Action>(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + markAsDirty(); + } + } + } + + /** + * Removes an action handler. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + @Override + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + markAsDirty(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + markAsDirty(); + } + + /** + * Gets the visible item ids. + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + @Override + public Collection<?> getVisibleItemIds() { + + final LinkedList<Object> visible = new LinkedList<Object>(); + + // Iterates trough hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + final Collection<?> ids = rootItemIds(); + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + visible.add(itemId); + + // Adds children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } + } + } + + return visible; + } + + /** + * Tree does not support <code>setNullSelectionItemId</code>. + * + * @see com.vaadin.ui.AbstractSelect#setNullSelectionItemId(java.lang.Object) + */ + @Override + public void setNullSelectionItemId(Object nullSelectionItemId) + throws UnsupportedOperationException { + if (nullSelectionItemId != null) { + throw new UnsupportedOperationException(); + } + + } + + /** + * Adding new items is not supported. + * + * @throws UnsupportedOperationException + * if set to true. + * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean) + */ + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + private ItemStyleGenerator itemStyleGenerator; + + private DropHandler dropHandler; + + private boolean htmlContentAllowed; + + @Override + public void addItemClickListener(ItemClickListener listener) { + addListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemClickListener(ItemClickListener)} + **/ + @Override + @Deprecated + public void addListener(ItemClickListener listener) { + addItemClickListener(listener); + } + + @Override + public void removeItemClickListener(ItemClickListener listener) { + removeListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemClickListener(ItemClickListener)} + **/ + @Override + @Deprecated + public void removeListener(ItemClickListener listener) { + removeItemClickListener(listener); + } + + /** + * Sets the {@link ItemStyleGenerator} to be used with this tree. + * + * @param itemStyleGenerator + * item style generator or null to remove generator + */ + public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) { + if (this.itemStyleGenerator != itemStyleGenerator) { + this.itemStyleGenerator = itemStyleGenerator; + markAsDirty(); + } + } + + /** + * @return the current {@link ItemStyleGenerator} for this tree. Null if + * {@link ItemStyleGenerator} is not set. + */ + public ItemStyleGenerator getItemStyleGenerator() { + return itemStyleGenerator; + } + + /** + * ItemStyleGenerator can be used to add custom styles to tree items. The + * CSS class name that will be added to the item content is + * <tt>v-tree-node-[style name]</tt>. + */ + public interface ItemStyleGenerator extends Serializable { + + /** + * Called by Tree when an item is painted. + * + * @param source + * the source Tree + * @param itemId + * The itemId of the item to be painted + * @return The style name to add to this item. (the CSS class name will + * be v-tree-node-[style name] + */ + public abstract String getStyle(Tree source, Object itemId); + } + + // Overriden so javadoc comes from Container.Hierarchical + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return super.removeItem(itemId); + } + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /** + * A {@link TargetDetails} implementation with Tree specific api. + * + * @since 6.3 + */ + public class TreeTargetDetails extends AbstractSelectTargetDetails { + + TreeTargetDetails(Map<String, Object> rawVariables) { + super(rawVariables); + } + + @Override + public Tree getTarget() { + return (Tree) super.getTarget(); + } + + /** + * If the event is on a node that can not have children (see + * {@link Tree#areChildrenAllowed(Object)}), this method returns the + * parent item id of the target item (see {@link #getItemIdOver()} ). + * The identifier of the parent node is also returned if the cursor is + * on the top part of node. Else this method returns the same as + * {@link #getItemIdOver()}. + * <p> + * In other words this method returns the identifier of the "folder" + * into the drag operation is targeted. + * <p> + * If the method returns null, the current target is on a root node or + * on other undefined area over the tree component. + * <p> + * The default Tree implementation marks the targetted tree node with + * CSS classnames v-tree-node-dragfolder and + * v-tree-node-caption-dragfolder (for the caption element). + */ + public Object getItemIdInto() { + + Object itemIdOver = getItemIdOver(); + if (areChildrenAllowed(itemIdOver) + && getDropLocation() == VerticalDropLocation.MIDDLE) { + return itemIdOver; + } + return getParent(itemIdOver); + } + + /** + * If drop is targeted into "folder node" (see {@link #getItemIdInto()} + * ), this method returns the item id of the node after the drag was + * targeted. This method is useful when implementing drop into specific + * location (between specific nodes) in tree. + * + * @return the id of the item after the user targets the drop or null if + * "target" is a first item in node list (or the first in root + * node list) + */ + public Object getItemIdAfter() { + Object itemIdOver = getItemIdOver(); + Object itemIdInto2 = getItemIdInto(); + if (itemIdOver.equals(itemIdInto2)) { + return null; + } + VerticalDropLocation dropLocation = getDropLocation(); + if (VerticalDropLocation.TOP == dropLocation) { + // if on top of the caption area, add before + Collection<?> children; + Object itemIdInto = getItemIdInto(); + if (itemIdInto != null) { + // seek the previous from child list + children = getChildren(itemIdInto); + } else { + children = rootItemIds(); + } + Object ref = null; + for (Object object : children) { + if (object.equals(itemIdOver)) { + return ref; + } + ref = object; + } + } + return itemIdOver; + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map) + */ + @Override + public TreeTargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new TreeTargetDetails(clientVariables); + } + + /** + * Helper API for {@link TreeDropCriterion} + * + * @param itemId + * @return + */ + private String key(Object itemId) { + return itemIdMapper.key(itemId); + } + + /** + * Sets the drag mode that controls how Tree behaves as a {@link DragSource} + * . + * + * @param dragMode + */ + public void setDragMode(TreeDragMode dragMode) { + this.dragMode = dragMode; + markAsDirty(); + } + + /** + * @return the drag mode that controls how Tree behaves as a + * {@link DragSource}. + * + * @see TreeDragMode + */ + public TreeDragMode getDragMode() { + return dragMode; + } + + /** + * Concrete implementation of {@link DataBoundTransferable} for data + * transferred from a tree. + * + * @see {@link DataBoundTransferable}. + * + * @since 6.3 + */ + protected class TreeTransferable extends DataBoundTransferable { + + public TreeTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + } + + @Override + public Object getItemId() { + return getData("itemId"); + } + + @Override + public Object getPropertyId() { + return getItemCaptionPropertyId(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map) + */ + @Override + public Transferable getTransferable(Map<String, Object> payload) { + TreeTransferable transferable = new TreeTransferable(this, payload); + // updating drag source variables + Object object = payload.get("itemId"); + if (object != null) { + transferable.setData("itemId", itemIdMapper.get((String) object)); + } + + return transferable; + } + + /** + * Lazy loading accept criterion for Tree. Accepted target nodes are loaded + * from server once per drag and drop operation. Developer must override one + * method that decides accepted tree nodes for the whole Tree. + * + * <p> + * Initially pretty much no data is sent to client. On first required + * criterion check (per drag request) the client side data structure is + * initialized from server and no subsequent requests requests are needed + * during that drag and drop operation. + */ + public static abstract class TreeDropCriterion extends ServerSideCriterion { + + private Tree tree; + + private Set<Object> allowedItemIds; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.ServerSideCriterion#getIdentifier + * () + */ + @Override + protected String getIdentifier() { + return TreeDropCriterion.class.getCanonicalName(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#accepts(com.vaadin + * .event.dd.DragAndDropEvent) + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + tree = (Tree) dragEvent.getTargetDetails().getTarget(); + allowedItemIds = getAllowedItemIds(dragEvent, tree); + + return allowedItemIds.contains(dropTargetData.getItemIdOver()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#paintResponse( + * com.vaadin.server.PaintTarget) + */ + @Override + public void paintResponse(PaintTarget target) throws PaintException { + /* + * send allowed nodes to client so subsequent requests can be + * avoided + */ + Object[] array = allowedItemIds.toArray(); + for (int i = 0; i < array.length; i++) { + String key = tree.key(array[i]); + array[i] = key; + } + target.addAttribute("allowedIds", array); + } + + protected abstract Set<Object> getAllowedItemIds( + DragAndDropEvent dragEvent, Tree tree); + + } + + /** + * A criterion that accepts {@link Transferable} only directly on a tree + * node that can have children. + * <p> + * Class is singleton, use {@link TargetItemAllowsChildren#get()} to get the + * instance. + * + * @see Tree#setChildrenAllowed(Object, boolean) + * + * @since 6.3 + */ + public static class TargetItemAllowsChildren extends TargetDetailIs { + + private static TargetItemAllowsChildren instance = new TargetItemAllowsChildren(); + + public static TargetItemAllowsChildren get() { + return instance; + } + + private TargetItemAllowsChildren() { + super("itemIdOverIsNode", Boolean.TRUE); + } + + /* + * Uses enhanced server side check + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + // must be over tree node and in the middle of it (not top or + // bottom + // part) + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + Object itemIdOver = eventDetails.getItemIdOver(); + if (!eventDetails.getTarget().areChildrenAllowed(itemIdOver)) { + return false; + } + // return true if directly over + return eventDetails + .getDropLocation() == VerticalDropLocation.MIDDLE; + } catch (Exception e) { + return false; + } + } + + } + + /** + * An accept criterion that checks the parent node (or parent hierarchy) for + * the item identifier given in constructor. If the parent is found, content + * is accepted. Criterion can be used to accepts drags on a specific sub + * tree only. + * <p> + * The root items is also consider to be valid target. + */ + public class TargetInSubtree extends ClientSideCriterion { + + private Object rootId; + private int depthToCheck = -1; + + /** + * Constructs a criteria that accepts the drag if the targeted Item is a + * descendant of Item identified by given id + * + * @param parentItemId + * the item identifier of the parent node + */ + public TargetInSubtree(Object parentItemId) { + rootId = parentItemId; + } + + /** + * Constructs a criteria that accepts drops within given level below the + * subtree root identified by given id. + * + * @param rootId + * the item identifier to be sought for + * @param depthToCheck + * the depth that tree is traversed upwards to seek for the + * parent, -1 means that the whole structure should be + * checked + */ + public TargetInSubtree(Object rootId, int depthToCheck) { + this.rootId = rootId; + this.depthToCheck = depthToCheck; + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + if (eventDetails.getItemIdOver() != null) { + Object itemId = eventDetails.getItemIdOver(); + int i = 0; + while (itemId != null + && (depthToCheck == -1 || i <= depthToCheck)) { + if (itemId.equals(rootId)) { + return true; + } + itemId = getParent(itemId); + i++; + } + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addAttribute("depth", depthToCheck); + target.addAttribute("key", key(rootId)); + } + } + + /** + * Set the item description generator which generates tooltips for the tree + * items + * + * @param generator + * The generator to use or null to disable + */ + public void setItemDescriptionGenerator( + ItemDescriptionGenerator generator) { + if (generator != itemDescriptionGenerator) { + itemDescriptionGenerator = generator; + markAsDirty(); + } + } + + /** + * Get the item description generator which generates tooltips for tree + * items + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + + private void cleanupExpandedItems() { + Set<Object> removedItemIds = new HashSet<Object>(); + for (Object expandedItemId : expanded) { + if (getItem(expandedItemId) == null) { + removedItemIds.add(expandedItemId); + if (this.expandedItemId == expandedItemId) { + this.expandedItemId = null; + } + } + } + expanded.removeAll(removedItemIds); + } + + /** + * Reads an Item from a design and inserts it into the data source. + * Recursively handles any children of the item as well. + * + * @since 7.5.0 + * @param node + * an element representing the item (tree node). + * @param selected + * A set accumulating selected items. If the item that is read is + * marked as selected, its item id should be added to this set. + * @param context + * the DesignContext instance used in parsing + * @return the item id of the new item + * + * @throws DesignException + * if the tag name of the {@code node} element is not + * {@code node}. + */ + @Override + protected String readItem(Element node, Set<String> selected, + DesignContext context) { + + if (!"node".equals(node.tagName())) { + throw new DesignException("Unrecognized child element in " + + getClass().getSimpleName() + ": " + node.tagName()); + } + + String itemId = node.attr("text"); + addItem(itemId); + if (node.hasAttr("icon")) { + Resource icon = DesignAttributeHandler.readAttribute("icon", + node.attributes(), Resource.class); + setItemIcon(itemId, icon); + } + if (node.hasAttr("selected")) { + selected.add(itemId); + } + + for (Element child : node.children()) { + String childItemId = readItem(child, selected, context); + setParent(childItemId, itemId); + } + return itemId; + } + + /** + * Recursively writes the root items and their children to a design. + * + * @since 7.5.0 + * @param design + * the element into which to insert the items + * @param context + * the DesignContext instance used in writing + */ + @Override + protected void writeItems(Element design, DesignContext context) { + for (Object itemId : rootItemIds()) { + writeItem(design, itemId, context); + } + } + + /** + * Recursively writes a data source Item and its children to a design. + * + * @since 7.5.0 + * @param design + * the element into which to insert the item + * @param itemId + * the id of the item to write + * @param context + * the DesignContext instance used in writing + * @return + */ + @Override + protected Element writeItem(Element design, Object itemId, + DesignContext context) { + Element element = design.appendElement("node"); + + element.attr("text", itemId.toString()); + + Resource icon = getItemIcon(itemId); + if (icon != null) { + DesignAttributeHandler.writeAttribute("icon", element.attributes(), + icon, null, Resource.class); + } + + if (isSelected(itemId)) { + element.attr("selected", ""); + } + + Collection<?> children = getChildren(itemId); + if (children != null) { + // Yeah... see #5864 + for (Object childItemId : children) { + writeItem(element, childItemId, context); + } + } + + return element; + } + + /** + * Sets whether html is allowed in the item captions. If set to + * <code>true</code>, the captions are passed to the browser as html and the + * developer is responsible for ensuring no harmful html is used. If set to + * <code>false</code>, the content is passed to the browser as plain text. + * The default setting is <code>false</code> + * + * @since 7.6 + * @param htmlContentAllowed + * <code>true</code> if the captions are used as html, + * <code>false</code> if used as plain text + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + markAsDirty(); + } + + /** + * Checks whether captions are interpreted as html or plain text. + * + * @since 7.6 + * @return <code>true</code> if the captions are displayed as html, + * <code>false</code> if displayed as plain text + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + @Override + protected TreeState getState() { + return (TreeState) super.getState(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/TwinColSelect.java b/compatibility-server/src/main/java/com/vaadin/ui/TwinColSelect.java new file mode 100644 index 0000000000..261d813ffa --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/TwinColSelect.java @@ -0,0 +1,169 @@ +/* + * Copyright 2000-2016 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.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.shared.ui.twincolselect.TwinColSelectConstants; +import com.vaadin.shared.ui.twincolselect.TwinColSelectState; + +/** + * Multiselect component with two lists: left side for available items and right + * side for selected items. + */ +@SuppressWarnings("serial") +public class TwinColSelect extends AbstractSelect { + + private int rows = 0; + + private String leftColumnCaption; + private String rightColumnCaption; + + /** + * + */ + public TwinColSelect() { + super(); + setMultiSelect(true); + } + + /** + * @param caption + */ + public TwinColSelect(String caption) { + super(caption); + setMultiSelect(true); + } + + /** + * @param caption + * @param dataSource + */ + public TwinColSelect(String caption, Container dataSource) { + super(caption, dataSource); + setMultiSelect(true); + } + + public int getRows() { + return rows; + } + + /** + * Sets the number of rows in the editor. If the number of rows is set to 0, + * the actual number of displayed rows is determined implicitly by the + * adapter. + * <p> + * If a height if set (using {@link #setHeight(String)} or + * {@link #setHeight(float, int)}) it overrides the number of rows. Leave + * the height undefined to use this method. This is the opposite of how + * {@link #setColumns(int)} work. + * + * + * @param rows + * the number of rows to set. + */ + public void setRows(int rows) { + if (rows < 0) { + rows = 0; + } + if (this.rows != rows) { + this.rows = rows; + markAsDirty(); + } + } + + /** + * @param caption + * @param options + */ + public TwinColSelect(String caption, Collection<?> options) { + super(caption, options); + setMultiSelect(true); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + // Adds the number of columns + // Adds the number of rows + if (rows != 0) { + target.addAttribute("rows", rows); + } + + // Right and left column captions and/or icons (if set) + String lc = getLeftColumnCaption(); + String rc = getRightColumnCaption(); + if (lc != null) { + target.addAttribute(TwinColSelectConstants.ATTRIBUTE_LEFT_CAPTION, + lc); + } + if (rc != null) { + target.addAttribute(TwinColSelectConstants.ATTRIBUTE_RIGHT_CAPTION, + rc); + } + + super.paintContent(target); + } + + /** + * Sets the text shown above the right column. + * + * @param caption + * The text to show + */ + public void setRightColumnCaption(String rightColumnCaption) { + this.rightColumnCaption = rightColumnCaption; + markAsDirty(); + } + + /** + * Returns the text shown above the right column. + * + * @return The text shown or null if not set. + */ + public String getRightColumnCaption() { + return rightColumnCaption; + } + + /** + * Sets the text shown above the left column. + * + * @param caption + * The text to show + */ + public void setLeftColumnCaption(String leftColumnCaption) { + this.leftColumnCaption = leftColumnCaption; + markAsDirty(); + } + + /** + * Returns the text shown above the left column. + * + * @return The text shown or null if not set. + */ + public String getLeftColumnCaption() { + return leftColumnCaption; + } + + @Override + protected TwinColSelectState getState() { + return (TwinColSelectState) super.getState(); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvent.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvent.java new file mode 100644 index 0000000000..f007f0cf34 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar; + +import com.vaadin.ui.Calendar; +import com.vaadin.ui.Component; + +/** + * All Calendar events extends this class. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +@SuppressWarnings("serial") +public class CalendarComponentEvent extends Component.Event { + + /** + * Set the source of the event + * + * @param source + * The source calendar + * + */ + public CalendarComponentEvent(Calendar source) { + super(source); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Event#getComponent() + */ + @Override + public Calendar getComponent() { + return (Calendar) super.getComponent(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvents.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvents.java new file mode 100644 index 0000000000..2c4fec95d4 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarComponentEvents.java @@ -0,0 +1,603 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.EventListener; + +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.ui.Calendar; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.util.ReflectTools; + +/** + * Interface for all Vaadin Calendar events. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarComponentEvents extends Serializable { + + /** + * Notifier interface for notifying listener of calendar events + */ + public interface CalendarEventNotifier extends Serializable { + /** + * Get the assigned event handler for the given eventId. + * + * @param eventId + * @return the assigned eventHandler, or null if no handler is assigned + */ + public EventListener getHandler(String eventId); + } + + /** + * Notifier interface for event drag & drops. + */ + public interface EventMoveNotifier extends CalendarEventNotifier { + + /** + * Set the EventMoveHandler. + * + * @param listener + * EventMoveHandler to be added + */ + public void setHandler(EventMoveHandler listener); + + } + + /** + * MoveEvent is sent when existing event is dragged to a new position. + */ + @SuppressWarnings("serial") + public class MoveEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTMOVE; + + /** Index for the moved Schedule.Event. */ + private CalendarEvent calendarEvent; + + /** New starting date for the moved Calendar.Event. */ + private Date newStart; + + /** + * MoveEvent needs the target event and new start date. + * + * @param source + * Calendar component. + * @param calendarEvent + * Target event. + * @param newStart + * Target event's new start date. + */ + public MoveEvent(Calendar source, CalendarEvent calendarEvent, + Date newStart) { + super(source); + + this.calendarEvent = calendarEvent; + this.newStart = newStart; + } + + /** + * Get target event. + * + * @return Target event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + /** + * Get new start date. + * + * @return New start date. + */ + public Date getNewStart() { + return newStart; + } + } + + /** + * Handler interface for when events are being dragged on the calendar + * + */ + public interface EventMoveHandler extends EventListener, Serializable { + + /** Trigger method for the MoveEvent. */ + public static final Method eventMoveMethod = ReflectTools.findMethod( + EventMoveHandler.class, "eventMove", MoveEvent.class); + + /** + * This method will be called when event has been moved to a new + * position. + * + * @param event + * MoveEvent containing specific information of the new + * position and target event. + */ + public void eventMove(MoveEvent event); + } + + /** + * Handler interface for day or time cell drag-marking with mouse. + */ + public interface RangeSelectNotifier + extends Serializable, CalendarEventNotifier { + + /** + * Set the RangeSelectHandler that listens for drag-marking. + * + * @param listener + * RangeSelectHandler to be added. + */ + public void setHandler(RangeSelectHandler listener); + } + + /** + * RangeSelectEvent is sent when day or time cells are drag-marked with + * mouse. + */ + @SuppressWarnings("serial") + public class RangeSelectEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.RANGESELECT; + + /** Calendar event's start date. */ + private Date start; + + /** Calendar event's end date. */ + private Date end; + + /** + * Defines the event's view mode. + */ + private boolean monthlyMode; + + /** + * RangeSelectEvent needs a start and end date. + * + * @param source + * Calendar component. + * @param start + * Start date. + * @param end + * End date. + * @param monthlyMode + * Calendar view mode. + */ + public RangeSelectEvent(Calendar source, Date start, Date end, + boolean monthlyMode) { + super(source); + this.start = start; + this.end = end; + this.monthlyMode = monthlyMode; + } + + /** + * Get start date. + * + * @return Start date. + */ + public Date getStart() { + return start; + } + + /** + * Get end date. + * + * @return End date. + */ + public Date getEnd() { + return end; + } + + /** + * Gets the event's view mode. Calendar can be be either in monthly or + * weekly mode, depending on the active date range. + * + * @deprecated User {@link Calendar#isMonthlyMode()} instead + * + * @return Returns true when monthly view is active. + */ + @Deprecated + public boolean isMonthlyMode() { + return monthlyMode; + } + } + + /** RangeSelectHandler handles RangeSelectEvent. */ + public interface RangeSelectHandler extends EventListener, Serializable { + + /** Trigger method for the RangeSelectEvent. */ + public static final Method rangeSelectMethod = ReflectTools.findMethod( + RangeSelectHandler.class, "rangeSelect", + RangeSelectEvent.class); + + /** + * This method will be called when day or time cells are drag-marked + * with mouse. + * + * @param event + * RangeSelectEvent that contains range start and end date. + */ + public void rangeSelect(RangeSelectEvent event); + } + + /** Notifier interface for navigation listening. */ + public interface NavigationNotifier extends Serializable { + /** + * Add a forward navigation listener. + * + * @param handler + * ForwardHandler to be added. + */ + public void setHandler(ForwardHandler handler); + + /** + * Add a backward navigation listener. + * + * @param handler + * BackwardHandler to be added. + */ + public void setHandler(BackwardHandler handler); + + /** + * Add a date click listener. + * + * @param handler + * DateClickHandler to be added. + */ + public void setHandler(DateClickHandler handler); + + /** + * Add a event click listener. + * + * @param handler + * EventClickHandler to be added. + */ + public void setHandler(EventClickHandler handler); + + /** + * Add a week click listener. + * + * @param handler + * WeekClickHandler to be added. + */ + public void setHandler(WeekClickHandler handler); + } + + /** + * ForwardEvent is sent when forward navigation button is clicked. + */ + @SuppressWarnings("serial") + public class ForwardEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.FORWARD; + + /** + * ForwardEvent needs only the source component. + * + * @param source + * Calendar component. + */ + public ForwardEvent(Calendar source) { + super(source); + } + } + + /** ForwardHandler handles ForwardEvent. */ + public interface ForwardHandler extends EventListener, Serializable { + + /** Trigger method for the ForwardEvent. */ + public static final Method forwardMethod = ReflectTools.findMethod( + ForwardHandler.class, "forward", ForwardEvent.class); + + /** + * This method will be called when date range is moved forward. + * + * @param event + * ForwardEvent + */ + public void forward(ForwardEvent event); + } + + /** + * BackwardEvent is sent when backward navigation button is clicked. + */ + @SuppressWarnings("serial") + public class BackwardEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.BACKWARD; + + /** + * BackwardEvent needs only the source source component. + * + * @param source + * Calendar component. + */ + public BackwardEvent(Calendar source) { + super(source); + } + } + + /** BackwardHandler handles BackwardEvent. */ + public interface BackwardHandler extends EventListener, Serializable { + + /** Trigger method for the BackwardEvent. */ + public static final Method backwardMethod = ReflectTools.findMethod( + BackwardHandler.class, "backward", BackwardEvent.class); + + /** + * This method will be called when date range is moved backwards. + * + * @param event + * BackwardEvent + */ + public void backward(BackwardEvent event); + } + + /** + * DateClickEvent is sent when a date is clicked. + */ + @SuppressWarnings("serial") + public class DateClickEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.DATECLICK; + + /** Date that was clicked. */ + private Date date; + + /** DateClickEvent needs the target date that was clicked. */ + public DateClickEvent(Calendar source, Date date) { + super(source); + this.date = date; + } + + /** + * Get clicked date. + * + * @return Clicked date. + */ + public Date getDate() { + return date; + } + } + + /** DateClickHandler handles DateClickEvent. */ + public interface DateClickHandler extends EventListener, Serializable { + + /** Trigger method for the DateClickEvent. */ + public static final Method dateClickMethod = ReflectTools.findMethod( + DateClickHandler.class, "dateClick", DateClickEvent.class); + + /** + * This method will be called when a date is clicked. + * + * @param event + * DateClickEvent containing the target date. + */ + public void dateClick(DateClickEvent event); + } + + /** + * EventClick is sent when an event is clicked. + */ + @SuppressWarnings("serial") + public class EventClick extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTCLICK; + + /** Clicked source event. */ + private CalendarEvent calendarEvent; + + /** Target source event is needed for the EventClick. */ + public EventClick(Calendar source, CalendarEvent calendarEvent) { + super(source); + this.calendarEvent = calendarEvent; + } + + /** + * Get the clicked event. + * + * @return Clicked event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + } + + /** EventClickHandler handles EventClick. */ + public interface EventClickHandler extends EventListener, Serializable { + + /** Trigger method for the EventClick. */ + public static final Method eventClickMethod = ReflectTools.findMethod( + EventClickHandler.class, "eventClick", EventClick.class); + + /** + * This method will be called when an event is clicked. + * + * @param event + * EventClick containing the target event. + */ + public void eventClick(EventClick event); + } + + /** + * WeekClick is sent when week is clicked. + */ + @SuppressWarnings("serial") + public class WeekClick extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.WEEKCLICK; + + /** Target week. */ + private int week; + + /** Target year. */ + private int year; + + /** + * WeekClick needs a target year and week. + * + * @param source + * Target source. + * @param week + * Target week. + * @param year + * Target year. + */ + public WeekClick(Calendar source, int week, int year) { + super(source); + this.week = week; + this.year = year; + } + + /** + * Get week as a integer. See {@link java.util.Calendar} for the allowed + * values. + * + * @return Week as a integer. + */ + public int getWeek() { + return week; + } + + /** + * Get year as a integer. See {@link java.util.Calendar} for the allowed + * values. + * + * @return Year as a integer + */ + public int getYear() { + return year; + } + } + + /** WeekClickHandler handles WeekClicks. */ + public interface WeekClickHandler extends EventListener, Serializable { + + /** Trigger method for the WeekClick. */ + public static final Method weekClickMethod = ReflectTools.findMethod( + WeekClickHandler.class, "weekClick", WeekClick.class); + + /** + * This method will be called when a week is clicked. + * + * @param event + * WeekClick containing the target week and year. + */ + public void weekClick(WeekClick event); + } + + /** + * EventResize is sent when an event is resized + */ + @SuppressWarnings("serial") + public class EventResize extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTRESIZE; + + private CalendarEvent calendarEvent; + + private Date startTime; + + private Date endTime; + + public EventResize(Calendar source, CalendarEvent calendarEvent, + Date startTime, Date endTime) { + super(source); + this.calendarEvent = calendarEvent; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Get target event. + * + * @return Target event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + /** + * @deprecated Use {@link #getNewStart()} instead + * + * @return the new start time + */ + @Deprecated + public Date getNewStartTime() { + return startTime; + } + + /** + * Returns the updated start date/time of the event + * + * @return The new date for the event + */ + public Date getNewStart() { + return startTime; + } + + /** + * @deprecated Use {@link #getNewEnd()} instead + * + * @return the new end time + */ + @Deprecated + public Date getNewEndTime() { + return endTime; + } + + /** + * Returns the updates end date/time of the event + * + * @return The new date for the event + */ + public Date getNewEnd() { + return endTime; + } + } + + /** + * Notifier interface for event resizing. + */ + public interface EventResizeNotifier extends Serializable { + + /** + * Set a EventResizeHandler. + * + * @param handler + * EventResizeHandler to be set + */ + public void setHandler(EventResizeHandler handler); + } + + /** + * Handler for EventResize event. + */ + public interface EventResizeHandler extends EventListener, Serializable { + + /** Trigger method for the EventResize. */ + public static final Method eventResizeMethod = ReflectTools.findMethod( + EventResizeHandler.class, "eventResize", EventResize.class); + + void eventResize(EventResize event); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarDateRange.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarDateRange.java new file mode 100644 index 0000000000..09d6c80a7f --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarDateRange.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar; + +import java.io.Serializable; +import java.util.Date; +import java.util.TimeZone; + +/** + * Class for representing a date range. + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +@SuppressWarnings("serial") +public class CalendarDateRange implements Serializable { + + private Date start; + + private Date end; + + private final transient TimeZone tz; + + /** + * Constructor + * + * @param start + * The start date and time of the date range + * @param end + * The end date and time of the date range + */ + public CalendarDateRange(Date start, Date end, TimeZone tz) { + super(); + this.start = start; + this.end = end; + this.tz = tz; + } + + /** + * Get the start date of the date range + * + * @return the start Date of the range + */ + public Date getStart() { + return start; + } + + /** + * Get the end date of the date range + * + * @return the end Date of the range + */ + public Date getEnd() { + return end; + } + + /** + * Is a date in the date range + * + * @param date + * The date to check + * @return true if the date range contains a date start and end of range + * inclusive; false otherwise + */ + public boolean inRange(Date date) { + if (date == null) { + return false; + } + + return date.compareTo(start) >= 0 && date.compareTo(end) <= 0; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "CalendarDateRange [start=" + start + ", end=" + end + "]"; + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarTargetDetails.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarTargetDetails.java new file mode 100644 index 0000000000..3b71ab5a00 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/CalendarTargetDetails.java @@ -0,0 +1,80 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar; + +import java.util.Date; +import java.util.Map; + +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.ui.Calendar; + +/** + * Drop details for {@link com.vaadin.ui.addon.calendar.ui.Calendar Calendar}. + * When something is dropped on the Calendar, this class contains the specific + * details of the drop point. Specifically, this class gives access to the date + * where the drop happened. If the Calendar was in weekly mode, the date also + * includes the start time of the slot. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class CalendarTargetDetails extends TargetDetailsImpl { + + private boolean hasDropTime; + + public CalendarTargetDetails(Map<String, Object> rawDropData, + DropTarget dropTarget) { + super(rawDropData, dropTarget); + } + + /** + * @return true if {@link #getDropTime()} will return a date object with the + * time set to the start of the time slot where the drop happened + */ + public boolean hasDropTime() { + return hasDropTime; + } + + /** + * Does the dropped item have a time associated with it + * + * @param hasDropTime + */ + public void setHasDropTime(boolean hasDropTime) { + this.hasDropTime = hasDropTime; + } + + /** + * @return the date where the drop happened + */ + public Date getDropTime() { + if (hasDropTime) { + return (Date) getData("dropTime"); + } else { + return (Date) getData("dropDay"); + } + } + + /** + * @return the {@link com.vaadin.ui.addon.calendar.ui.Calendar Calendar} + * instance which was the target of the drop + */ + public Calendar getTargetCalendar() { + return (Calendar) getTarget(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/ContainerEventProvider.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/ContainerEventProvider.java new file mode 100644 index 0000000000..b0a6aaa95a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/ContainerEventProvider.java @@ -0,0 +1,566 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar; + +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.event.BasicEvent; +import com.vaadin.ui.components.calendar.event.CalendarEditableEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeListener; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeNotifier; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier; + +/** + * A event provider which uses a {@link Container} as a datasource. Container + * used as data source. + * + * NOTE: The data source must be sorted by date! + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class ContainerEventProvider + implements CalendarEditableEventProvider, EventSetChangeNotifier, + EventChangeNotifier, EventMoveHandler, EventResizeHandler, + Container.ItemSetChangeListener, Property.ValueChangeListener { + + // Default property ids + public static final String CAPTION_PROPERTY = "caption"; + public static final String DESCRIPTION_PROPERTY = "description"; + public static final String STARTDATE_PROPERTY = "start"; + public static final String ENDDATE_PROPERTY = "end"; + public static final String STYLENAME_PROPERTY = "styleName"; + public static final String ALL_DAY_PROPERTY = "allDay"; + + /** + * Internal class to keep the container index which item this event + * represents + * + */ + private class ContainerCalendarEvent extends BasicEvent { + private final int index; + + public ContainerCalendarEvent(int containerIndex) { + super(); + index = containerIndex; + } + + public int getContainerIndex() { + return index; + } + } + + /** + * Listeners attached to the container + */ + private final List<EventSetChangeListener> eventSetChangeListeners = new LinkedList<CalendarEventProvider.EventSetChangeListener>(); + private final List<EventChangeListener> eventChangeListeners = new LinkedList<CalendarEvent.EventChangeListener>(); + + /** + * The event cache contains the events previously created by + * {@link #getEvents(Date, Date)} + */ + private final List<CalendarEvent> eventCache = new LinkedList<CalendarEvent>(); + + /** + * The container used as datasource + */ + private Indexed container; + + /** + * Container properties. Defaults based on using the {@link BasicEvent} + * helper class. + */ + private Object captionProperty = CAPTION_PROPERTY; + private Object descriptionProperty = DESCRIPTION_PROPERTY; + private Object startDateProperty = STARTDATE_PROPERTY; + private Object endDateProperty = ENDDATE_PROPERTY; + private Object styleNameProperty = STYLENAME_PROPERTY; + private Object allDayProperty = ALL_DAY_PROPERTY; + + /** + * Constructor + * + * @param container + * Container to use as a data source. + */ + public ContainerEventProvider(Container.Indexed container) { + this.container = container; + listenToContainerEvents(); + } + + /** + * Set the container data source + * + * @param container + * The container to use as datasource + * + */ + public void setContainerDataSource(Container.Indexed container) { + // Detach the previous container + detachContainerDataSource(); + + this.container = container; + listenToContainerEvents(); + } + + /** + * Returns the container used as data source + * + */ + public Container.Indexed getContainerDataSource() { + return container; + } + + /** + * Attaches listeners to the container so container events can be processed + */ + private void listenToContainerEvents() { + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container).addItemSetChangeListener(this); + } + if (container instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) container).addValueChangeListener(this); + } + } + + /** + * Removes listeners from the container so no events are processed + */ + private void ignoreContainerEvents() { + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(this); + } + if (container instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) container).removeValueChangeListener(this); + } + } + + /** + * Converts an event in the container to an {@link CalendarEvent} + * + * @param index + * The index of the item in the container to get the event for + * @return + */ + private CalendarEvent getEvent(int index) { + + // Check the event cache first + for (CalendarEvent e : eventCache) { + if (e instanceof ContainerCalendarEvent + && ((ContainerCalendarEvent) e) + .getContainerIndex() == index) { + return e; + } else if (container.getIdByIndex(index) == e) { + return e; + } + } + + final Object id = container.getIdByIndex(index); + Item item = container.getItem(id); + CalendarEvent event; + if (id instanceof CalendarEvent) { + /* + * If we are using the BeanItemContainer or another container which + * stores the objects as ids then just return the instances + */ + event = (CalendarEvent) id; + + } else { + /* + * Else we use the properties to create the event + */ + BasicEvent basicEvent = new ContainerCalendarEvent(index); + + // Set values from property values + if (captionProperty != null + && item.getItemPropertyIds().contains(captionProperty)) { + basicEvent.setCaption(String.valueOf( + item.getItemProperty(captionProperty).getValue())); + } + if (descriptionProperty != null && item.getItemPropertyIds() + .contains(descriptionProperty)) { + basicEvent.setDescription(String.valueOf( + item.getItemProperty(descriptionProperty).getValue())); + } + if (startDateProperty != null + && item.getItemPropertyIds().contains(startDateProperty)) { + basicEvent.setStart((Date) item + .getItemProperty(startDateProperty).getValue()); + } + if (endDateProperty != null + && item.getItemPropertyIds().contains(endDateProperty)) { + basicEvent.setEnd((Date) item.getItemProperty(endDateProperty) + .getValue()); + } + if (styleNameProperty != null + && item.getItemPropertyIds().contains(styleNameProperty)) { + basicEvent.setStyleName(String.valueOf( + item.getItemProperty(styleNameProperty).getValue())); + } + if (allDayProperty != null + && item.getItemPropertyIds().contains(allDayProperty)) { + basicEvent.setAllDay((Boolean) item + .getItemProperty(allDayProperty).getValue()); + } + event = basicEvent; + } + return event; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + eventCache.clear(); + int size = container.size(); + assert size >= 0; + + for (int i = 0; i < size; i++) { + Object id = container.getIdByIndex(i); + Item item = container.getItem(id); + boolean add = true; + if (startDate != null) { + Date eventEnd = (Date) item.getItemProperty(endDateProperty) + .getValue(); + if (eventEnd.compareTo(startDate) < 0) { + add = false; + } + } + if (add && endDate != null) { + Date eventStart = (Date) item.getItemProperty(startDateProperty) + .getValue(); + if (eventStart.compareTo(endDate) >= 0) { + break; // because container is sorted, all further events + // will be even later + } + } + if (add) { + eventCache.add(getEvent(i)); + } + } + return Collections.unmodifiableList(eventCache); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeNotifier + * #addListener(com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeListener) + */ + @Override + public void addEventSetChangeListener(EventSetChangeListener listener) { + if (!eventSetChangeListeners.contains(listener)) { + eventSetChangeListeners.add(listener); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeNotifier + * #removeListener(com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeListener) + */ + @Override + public void removeEventSetChangeListener(EventSetChangeListener listener) { + eventSetChangeListeners.remove(listener); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier# + * addListener + * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener) + */ + @Override + public void addEventChangeListener(EventChangeListener listener) { + if (eventChangeListeners.contains(listener)) { + eventChangeListeners.add(listener); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier# + * removeListener + * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener) + */ + @Override + public void removeEventChangeListener(EventChangeListener listener) { + eventChangeListeners.remove(listener); + } + + /** + * Get the property which provides the caption of the event + */ + public Object getCaptionProperty() { + return captionProperty; + } + + /** + * Set the property which provides the caption of the event + */ + public void setCaptionProperty(Object captionProperty) { + this.captionProperty = captionProperty; + } + + /** + * Get the property which provides the description of the event + */ + public Object getDescriptionProperty() { + return descriptionProperty; + } + + /** + * Set the property which provides the description of the event + */ + public void setDescriptionProperty(Object descriptionProperty) { + this.descriptionProperty = descriptionProperty; + } + + /** + * Get the property which provides the starting date and time of the event + */ + public Object getStartDateProperty() { + return startDateProperty; + } + + /** + * Set the property which provides the starting date and time of the event + */ + public void setStartDateProperty(Object startDateProperty) { + this.startDateProperty = startDateProperty; + } + + /** + * Get the property which provides the ending date and time of the event + */ + public Object getEndDateProperty() { + return endDateProperty; + } + + /** + * Set the property which provides the ending date and time of the event + */ + public void setEndDateProperty(Object endDateProperty) { + this.endDateProperty = endDateProperty; + } + + /** + * Get the property which provides the style name for the event + */ + public Object getStyleNameProperty() { + return styleNameProperty; + } + + /** + * Set the property which provides the style name for the event + */ + public void setStyleNameProperty(Object styleNameProperty) { + this.styleNameProperty = styleNameProperty; + } + + /** + * Set the all day property for the event + * + * @since 7.3.4 + */ + public void setAllDayProperty(Object allDayProperty) { + this.allDayProperty = allDayProperty; + } + + /** + * Get the all day property for the event + * + * @since 7.3.4 + */ + public Object getAllDayProperty() { + return allDayProperty; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange + * (com.vaadin.data.Container.ItemSetChangeEvent) + */ + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + if (event.getContainer() == container) { + // Trigger an eventset change event when the itemset changes + for (EventSetChangeListener listener : eventSetChangeListeners) { + listener.eventSetChange(new EventSetChangeEvent(this)); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Property.ValueChangeListener#valueChange(com.vaadin.data + * .Property.ValueChangeEvent) + */ + @Override + public void valueChange(ValueChangeEvent event) { + /* + * TODO Need to figure out how to get the item which triggered the the + * valuechange event and then trigger a EventChange event to the + * listeners + */ + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler + * #eventMove + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent) + */ + @Override + public void eventMove(MoveEvent event) { + CalendarEvent ce = event.getCalendarEvent(); + if (eventCache.contains(ce)) { + int index; + if (ce instanceof ContainerCalendarEvent) { + index = ((ContainerCalendarEvent) ce).getContainerIndex(); + } else { + index = container.indexOfId(ce); + } + + long eventLength = ce.getEnd().getTime() - ce.getStart().getTime(); + Date newEnd = new Date(event.getNewStart().getTime() + eventLength); + + ignoreContainerEvents(); + Item item = container.getItem(container.getIdByIndex(index)); + item.getItemProperty(startDateProperty) + .setValue(event.getNewStart()); + item.getItemProperty(endDateProperty).setValue(newEnd); + listenToContainerEvents(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * #eventResize + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize) + */ + @Override + public void eventResize(EventResize event) { + CalendarEvent ce = event.getCalendarEvent(); + if (eventCache.contains(ce)) { + int index; + if (ce instanceof ContainerCalendarEvent) { + index = ((ContainerCalendarEvent) ce).getContainerIndex(); + } else { + index = container.indexOfId(ce); + } + ignoreContainerEvents(); + Item item = container.getItem(container.getIdByIndex(index)); + item.getItemProperty(startDateProperty) + .setValue(event.getNewStart()); + item.getItemProperty(endDateProperty).setValue(event.getNewEnd()); + listenToContainerEvents(); + } + } + + /** + * If you are reusing the container which previously have been attached to + * this ContainerEventProvider call this method to remove this event + * providers container listeners before attaching it to an other + * ContainerEventProvider + */ + public void detachContainerDataSource() { + ignoreContainerEvents(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + Item item; + try { + item = container.addItem(event); + } catch (UnsupportedOperationException uop) { + // Thrown if container does not support adding items with custom + // ids. JPAContainer for example. + item = container.getItem(container.addItem()); + } + if (item != null) { + item.getItemProperty(getCaptionProperty()) + .setValue(event.getCaption()); + item.getItemProperty(getStartDateProperty()) + .setValue(event.getStart()); + item.getItemProperty(getEndDateProperty()).setValue(event.getEnd()); + item.getItemProperty(getStyleNameProperty()) + .setValue(event.getStyleName()); + item.getItemProperty(getDescriptionProperty()) + .setValue(event.getDescription()); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + container.removeItem(event); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEvent.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEvent.java new file mode 100644 index 0000000000..e2a580085a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEvent.java @@ -0,0 +1,265 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeNotifier; + +/** + * Simple implementation of {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent}. Has setters for all required fields and fires events when + * this event is changed. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEvent implements EditableCalendarEvent, EventChangeNotifier { + + private String caption; + private String description; + private Date end; + private Date start; + private String styleName; + private transient List<EventChangeListener> listeners = new ArrayList<EventChangeListener>(); + + private boolean isAllDay; + + /** + * Default constructor + */ + public BasicEvent() { + + } + + /** + * Constructor for creating an event with the same start and end date + * + * @param caption + * The caption for the event + * @param description + * The description for the event + * @param date + * The date the event occurred + */ + public BasicEvent(String caption, String description, Date date) { + this.caption = caption; + this.description = description; + start = date; + end = date; + } + + /** + * Constructor for creating an event with a start date and an end date. + * Start date should be before the end date + * + * @param caption + * The caption for the event + * @param description + * The description for the event + * @param startDate + * The start date of the event + * @param endDate + * The end date of the event + */ + public BasicEvent(String caption, String description, Date startDate, + Date endDate) { + this.caption = caption; + this.description = description; + start = startDate; + end = endDate; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getCaption() + */ + @Override + public String getCaption() { + return caption; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getDescription() + */ + @Override + public String getDescription() { + return description; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + */ + @Override + public Date getEnd() { + return end; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + */ + @Override + public Date getStart() { + return start; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + */ + @Override + public String getStyleName() { + return styleName; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#isAllDay() + */ + @Override + public boolean isAllDay() { + return isAllDay; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setCaption(java.lang + * .String) + */ + @Override + public void setCaption(String caption) { + this.caption = caption; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setDescription(java + * .lang.String) + */ + @Override + public void setDescription(String description) { + this.description = description; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setEnd(java.util. + * Date) + */ + @Override + public void setEnd(Date end) { + this.end = end; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setStart(java.util + * .Date) + */ + @Override + public void setStart(Date start) { + this.start = start; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setStyleName(java + * .lang.String) + */ + @Override + public void setStyleName(String styleName) { + this.styleName = styleName; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setAllDay(boolean) + */ + @Override + public void setAllDay(boolean isAllDay) { + this.isAllDay = isAllDay; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * ) + */ + @Override + public void addEventChangeListener(EventChangeListener listener) { + listeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier + * #removeListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * ) + */ + @Override + public void removeEventChangeListener(EventChangeListener listener) { + listeners.remove(listener); + } + + /** + * Fires an event change event to the listeners. Should be triggered when + * some property of the event changes. + */ + protected void fireEventChange() { + EventChangeEvent event = new EventChangeEvent(this); + + for (EventChangeListener listener : listeners) { + listener.eventChange(event); + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEventProvider.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEventProvider.java new file mode 100644 index 0000000000..fbf197d3eb --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/BasicEventProvider.java @@ -0,0 +1,177 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeEvent; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier; + +/** + * <p> + * Simple implementation of + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider}. Use {@link #addEvent(CalendarEvent)} and + * {@link #removeEvent(CalendarEvent)} to add / remove events. + * </p> + * + * <p> + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider.EventSetChangeNotifier + * EventSetChangeNotifier} and + * {@link com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener + * EventChangeListener} are also implemented, so the Calendar is notified when + * an event is added, changed or removed. + * </p> + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventProvider implements CalendarEditableEventProvider, + EventSetChangeNotifier, CalendarEvent.EventChangeListener { + + protected List<CalendarEvent> eventList = new ArrayList<CalendarEvent>(); + + private List<EventSetChangeListener> listeners = new ArrayList<EventSetChangeListener>(); + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + ArrayList<CalendarEvent> activeEvents = new ArrayList<CalendarEvent>(); + + for (CalendarEvent ev : eventList) { + long from = startDate.getTime(); + long to = endDate.getTime(); + + if (ev.getStart() != null && ev.getEnd() != null) { + long f = ev.getStart().getTime(); + long t = ev.getEnd().getTime(); + // Select only events that overlaps with startDate and + // endDate. + if ((f <= to && f >= from) || (t >= from && t <= to) + || (f <= from && t >= to)) { + activeEvents.add(ev); + } + } + } + + return activeEvents; + } + + /** + * Does this event provider container this event + * + * @param event + * The event to check for + * @return If this provider has the event then true is returned, else false + */ + public boolean containsEvent(BasicEvent event) { + return eventList.contains(event); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents. + * EventSetChangeNotifier #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents. + * EventSetChangeListener ) + */ + @Override + public void addEventSetChangeListener(EventSetChangeListener listener) { + listeners.add(listener); + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents. + * EventSetChangeNotifier #removeListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents. + * EventSetChangeListener ) + */ + @Override + public void removeEventSetChangeListener(EventSetChangeListener listener) { + listeners.remove(listener); + } + + /** + * Fires a eventsetchange event. The event is fired when either an event is + * added or removed to the event provider + */ + protected void fireEventSetChange() { + EventSetChangeEvent event = new EventSetChangeEvent(this); + + for (EventSetChangeListener listener : listeners) { + listener.eventSetChange(event); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * #eventChange + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChange) + */ + @Override + public void eventChange(EventChangeEvent changeEvent) { + // naive implementation + fireEventSetChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + eventList.add(event); + if (event instanceof BasicEvent) { + ((BasicEvent) event).addEventChangeListener(this); + } + fireEventSetChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + eventList.remove(event); + if (event instanceof BasicEvent) { + ((BasicEvent) event).removeEventChangeListener(this); + } + fireEventSetChange(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java new file mode 100644 index 0000000000..aaa76418a6 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +/** + * An event provider which allows adding and removing events + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarEditableEventProvider extends CalendarEventProvider { + + /** + * Adds an event to the event provider + * + * @param event + * The event to add + */ + void addEvent(CalendarEvent event); + + /** + * Removes an event from the event provider + * + * @param event + * The event + */ + void removeEvent(CalendarEvent event); +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEvent.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEvent.java new file mode 100644 index 0000000000..b4195cf0b1 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEvent.java @@ -0,0 +1,147 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +import java.io.Serializable; +import java.util.Date; + +/** + * <p> + * Event in the calendar. Customize your own event by implementing this + * interface. + * </p> + * + * <li>Start and end fields are mandatory.</li> + * + * <li>In "allDay" events longer than one day, starting and ending clock times + * are omitted in UI and only dates are shown.</li> + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +public interface CalendarEvent extends Serializable { + + /** + * Gets start date of event. + * + * @return Start date. + */ + public Date getStart(); + + /** + * Get end date of event. + * + * @return End date; + */ + public Date getEnd(); + + /** + * Gets caption of event. + * + * @return Caption text + */ + public String getCaption(); + + /** + * Gets description of event. Shown as a tooltip over the event. + * + * @return Description text. + */ + public String getDescription(); + + /** + * <p> + * Gets style name of event. In the client, style name will be set to the + * event's element class name and can be styled by CSS + * </p> + * Styling example:</br> + * <code>Java code: </br> + * event.setStyleName("color1"); + * </br></br> + * CSS:</br> + * .v-calendar-event-color1 {</br> + * background-color: #9effae;</br>}</code> + * + * @return Style name. + */ + public String getStyleName(); + + /** + * An all-day event typically does not occur at a specific time but targets + * a whole day or days. The rendering of all-day events differs from normal + * events. + * + * @return true if this event is an all-day event, false otherwise + */ + public boolean isAllDay(); + + /** + * Event to signal that an event has changed. + */ + @SuppressWarnings("serial") + public class EventChangeEvent implements Serializable { + + private CalendarEvent source; + + public EventChangeEvent(CalendarEvent source) { + this.source = source; + } + + /** + * @return the {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent} that has changed + */ + public CalendarEvent getCalendarEvent() { + return source; + } + } + + /** + * Listener for EventSetChange events. + */ + public interface EventChangeListener extends Serializable { + + /** + * Called when an Event has changed. + */ + public void eventChange(EventChangeEvent eventChangeEvent); + } + + /** + * Notifier interface for EventChange events. + */ + public interface EventChangeNotifier extends Serializable { + + /** + * Add a listener to listen for EventChangeEvents. These events are + * fired when a events properties are changed. + * + * @param listener + * The listener to add + */ + void addEventChangeListener(EventChangeListener listener); + + /** + * Remove a listener from the event provider. + * + * @param listener + * The listener to remove + */ + void removeEventChangeListener(EventChangeListener listener); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java new file mode 100644 index 0000000000..1d4fabed5a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java @@ -0,0 +1,112 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * Interface for querying events. The Vaadin Calendar always has a + * CalendarEventProvider set. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarEventProvider extends Serializable { + /** + * <p> + * Gets all available events in the target date range between startDate and + * endDate. The Vaadin Calendar queries the events from the range that is + * shown, which is not guaranteed to be the same as the date range that is + * set. + * </p> + * + * <p> + * For example, if you set the date range to be monday 22.2.2010 - wednesday + * 24.2.2010, the used Event Provider will be queried for events between + * monday 22.2.2010 00:00 and sunday 28.2.2010 23:59. Generally you can + * expect the date range to be expanded to whole days and whole weeks. + * </p> + * + * @param startDate + * Start date + * @param endDate + * End date + * @return List of events + */ + public List<CalendarEvent> getEvents(Date startDate, Date endDate); + + /** + * Event to signal that the set of events has changed and the calendar + * should refresh its view from the + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} . + * + */ + @SuppressWarnings("serial") + public class EventSetChangeEvent implements Serializable { + + private CalendarEventProvider source; + + public EventSetChangeEvent(CalendarEventProvider source) { + this.source = source; + } + + /** + * @return the + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} that has changed + */ + public CalendarEventProvider getProvider() { + return source; + } + } + + /** + * Listener for EventSetChange events. + */ + public interface EventSetChangeListener extends Serializable { + + /** + * Called when the set of Events has changed. + */ + public void eventSetChange(EventSetChangeEvent changeEvent); + } + + /** + * Notifier interface for EventSetChange events. + */ + public interface EventSetChangeNotifier extends Serializable { + + /** + * Add a listener for listening to when new events are adding or removed + * from the event provider. + * + * @param listener + * The listener to add + */ + void addEventSetChangeListener(EventSetChangeListener listener); + + /** + * Remove a listener which listens to {@link EventSetChangeEvent}-events + * + * @param listener + * The listener to remove + */ + void removeEventSetChangeListener(EventSetChangeListener listener); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java new file mode 100644 index 0000000000..5a44cae4ac --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java @@ -0,0 +1,91 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.event; + +import java.util.Date; + +/** + * <p> + * Extension to the basic {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent}. This interface provides setters (and thus editing + * capabilities) for all {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent} fields. For descriptions on the fields, refer to the extended + * interface. + * </p> + * + * <p> + * This interface is used by some of the basic Calendar event handlers in the + * <code>com.vaadin.addon.calendar.ui.handler</code> package to determine + * whether an event can be edited. + * </p> + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public interface EditableCalendarEvent extends CalendarEvent { + + /** + * Set the visible text in the calendar for the event. + * + * @param caption + * The text to show in the calendar + */ + void setCaption(String caption); + + /** + * Set the description of the event. This is shown in the calendar when + * hoovering over the event. + * + * @param description + * The text which describes the event + */ + void setDescription(String description); + + /** + * Set the end date of the event. Must be after the start date. + * + * @param end + * The end date to set + */ + void setEnd(Date end); + + /** + * Set the start date for the event. Must be before the end date + * + * @param start + * The start date of the event + */ + void setStart(Date start); + + /** + * Set the style name for the event used for styling the event cells + * + * @param styleName + * The stylename to use + * + */ + void setStyleName(String styleName); + + /** + * Does the event span the whole day. If so then set this to true + * + * @param isAllDay + * True if the event spans the whole day. In this case the start + * and end times are ignored. + */ + void setAllDay(boolean isAllDay); + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java new file mode 100644 index 0000000000..61f738fcd0 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java @@ -0,0 +1,96 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardHandler; + +/** + * Implements basic functionality needed to enable backwards navigation. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicBackwardHandler implements BackwardHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler# + * backward + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardEvent) + */ + @Override + public void backward(BackwardEvent event) { + Date start = event.getComponent().getStartDate(); + Date end = event.getComponent().getEndDate(); + + // calculate amount to move back + int durationInDays = (int) (((end.getTime()) - start.getTime()) + / DateConstants.DAYINMILLIS); + durationInDays++; + // for week view durationInDays = -7, for day view durationInDays = -1 + durationInDays = -durationInDays; + + // set new start and end times + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.setTime(start); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newStart = javaCalendar.getTime(); + + javaCalendar.setTime(end); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newEnd = javaCalendar.getTime(); + + if (start.equals(end)) { // day view + int firstDay = event.getComponent().getFirstVisibleDayOfWeek(); + int lastDay = event.getComponent().getLastVisibleDayOfWeek(); + int dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK); + + // we suppose that 7 >= lastDay >= firstDay >= 1 + while (!(firstDay <= dayOfWeek && dayOfWeek <= lastDay)) { + javaCalendar.add(java.util.Calendar.DATE, -1); + dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK); + } + + newStart = javaCalendar.getTime(); + newEnd = javaCalendar.getTime(); + } + + setDates(event, newStart, newEnd); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(BackwardEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java new file mode 100644 index 0000000000..0107e10e3d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickHandler; + +/** + * Implements basic functionality needed to switch to day view when a single day + * is clicked. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicDateClickHandler implements DateClickHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler + * #dateClick + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickEvent) + */ + @Override + public void dateClick(DateClickEvent event) { + Date clickedDate = event.getDate(); + + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.setTime(clickedDate); + + // as times are expanded, this is all that is needed to show one day + Date start = javaCalendar.getTime(); + Date end = javaCalendar.getTime(); + + setDates(event, start, end); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(DateClickEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java new file mode 100644 index 0000000000..60f0016312 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.EditableCalendarEvent; + +/** + * Implements basic functionality needed to enable moving events. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventMoveHandler implements EventMoveHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler + * #eventMove + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent) + */ + @Override + public void eventMove(MoveEvent event) { + CalendarEvent calendarEvent = event.getCalendarEvent(); + + if (calendarEvent instanceof EditableCalendarEvent) { + + EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent; + + Date newFromTime = event.getNewStart(); + + // Update event dates + long length = editableEvent.getEnd().getTime() + - editableEvent.getStart().getTime(); + setDates(editableEvent, newFromTime, + new Date(newFromTime.getTime() + length)); + } + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(EditableCalendarEvent event, Date start, Date end) { + event.setStart(start); + event.setEnd(end); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java new file mode 100644 index 0000000000..51e4bc1cbc --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.EditableCalendarEvent; + +/** + * Implements basic functionality needed to enable event resizing. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventResizeHandler implements EventResizeHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * #eventResize + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize) + */ + @Override + public void eventResize(EventResize event) { + CalendarEvent calendarEvent = event.getCalendarEvent(); + + if (calendarEvent instanceof EditableCalendarEvent) { + Date newStartTime = event.getNewStart(); + Date newEndTime = event.getNewEnd(); + + EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent; + + setDates(editableEvent, newStartTime, newEndTime); + } + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(EditableCalendarEvent event, Date start, Date end) { + event.setStart(start); + event.setEnd(end); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java new file mode 100644 index 0000000000..6cdc00bee2 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardHandler; + +/** + * Implements basic functionality needed to enable forward navigation. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicForwardHandler implements ForwardHandler { + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler# + * forward + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardEvent) + */ + @Override + public void forward(ForwardEvent event) { + Date start = event.getComponent().getStartDate(); + Date end = event.getComponent().getEndDate(); + + // calculate amount to move forward + int durationInDays = (int) (((end.getTime()) - start.getTime()) + / DateConstants.DAYINMILLIS); + // for week view durationInDays = 7, for day view durationInDays = 1 + durationInDays++; + + // set new start and end times + Calendar javaCalendar = Calendar.getInstance(); + javaCalendar.setTime(start); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newStart = javaCalendar.getTime(); + + javaCalendar.setTime(end); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newEnd = javaCalendar.getTime(); + + if (start.equals(end)) { // day view + int firstDay = event.getComponent().getFirstVisibleDayOfWeek(); + int lastDay = event.getComponent().getLastVisibleDayOfWeek(); + int dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK); + + // we suppose that 7 >= lastDay >= firstDay >= 1 + while (!(firstDay <= dayOfWeek && dayOfWeek <= lastDay)) { + javaCalendar.add(java.util.Calendar.DATE, 1); + dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK); + } + + newStart = javaCalendar.getTime(); + newEnd = javaCalendar.getTime(); + } + + setDates(event, newStart, newEnd); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(ForwardEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java new file mode 100644 index 0000000000..128c6abfbd --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2016 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.ui.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClickHandler; + +/** + * Implements basic functionality needed to change to week view when a week + * number is clicked. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicWeekClickHandler implements WeekClickHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler + * #weekClick + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClick) + */ + @Override + public void weekClick(WeekClick event) { + int week = event.getWeek(); + int year = event.getYear(); + + // set correct year and month + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.set(GregorianCalendar.YEAR, year); + javaCalendar.set(GregorianCalendar.WEEK_OF_YEAR, week); + + // starting at the beginning of the week + javaCalendar.set(GregorianCalendar.DAY_OF_WEEK, + javaCalendar.getFirstDayOfWeek()); + Date start = javaCalendar.getTime(); + + // ending at the end of the week + javaCalendar.add(GregorianCalendar.DATE, 6); + Date end = javaCalendar.getTime(); + + setDates(event, start, end); + + // times are automatically expanded, no need to worry about them + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(WeekClick event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeEvent.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeEvent.java new file mode 100644 index 0000000000..aa703deb19 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.ui.Component; +import com.vaadin.ui.Component.Event; + +/** + * The color changed event which is passed to the listeners when a color change + * occurs. + * + * @since 7.0.0 + */ +public class ColorChangeEvent extends Event { + private final Color color; + + public ColorChangeEvent(Component source, Color color) { + super(source); + + this.color = color; + } + + /** + * Returns the new color. + */ + public Color getColor() { + return color; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeListener.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeListener.java new file mode 100644 index 0000000000..b234dc3d5d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorChangeListener.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.io.Serializable; + +/** + * The listener interface for receiving colorChange events. The class that is + * interested in processing a {@link ColorChangeEvent} implements this + * interface, and the object created with that class is registered with a + * component using the component's <code>addColorChangeListener</code> method. + * When the colorChange event occurs, that object's appropriate method is + * invoked. + * + * @since 7.0.0 + * + * @see ColorChangeEvent + */ +public interface ColorChangeListener extends Serializable { + + /** + * Called when a new color has been selected. + * + * @param event + * An event containing information about the color change. + */ + void colorChanged(ColorChangeEvent event); + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGradient.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGradient.java new file mode 100644 index 0000000000..23748a967a --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGradient.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.lang.reflect.Method; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.shared.ui.colorpicker.ColorPickerGradientServerRpc; +import com.vaadin.shared.ui.colorpicker.ColorPickerGradientState; +import com.vaadin.ui.AbstractColorPicker.Coordinates2Color; +import com.vaadin.ui.AbstractComponent; + +/** + * A component that represents a color gradient within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerGradient extends AbstractComponent + implements ColorSelector { + + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + private ColorPickerGradientServerRpc rpc = new ColorPickerGradientServerRpc() { + + @Override + public void select(int cursorX, int cursorY) { + x = cursorX; + y = cursorY; + color = converter.calculate(x, y); + + fireColorChanged(color); + } + }; + + /** The converter. */ + private Coordinates2Color converter; + + /** The foreground color. */ + private Color color; + + /** The x-coordinate. */ + private int x = 0; + + /** The y-coordinate. */ + private int y = 0; + + private ColorPickerGradient() { + registerRpc(rpc); + // width and height must be set here instead of in theme, otherwise + // coordinate calculations fail + getState().width = "220px"; + getState().height = "220px"; + } + + /** + * Instantiates a new color picker gradient. + * + * @param id + * the id + * @param converter + * the converter + */ + public ColorPickerGradient(String id, Coordinates2Color converter) { + this(); + addStyleName(id); + this.converter = converter; + } + + @Override + public void setColor(Color c) { + color = c; + + int[] coords = converter.calculate(c); + x = coords[0]; + y = coords[1]; + + getState().cursorX = x; + getState().cursorY = y; + + } + + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + /** + * Sets the background color. + * + * @param color + * the new background color + */ + public void setBackgroundColor(Color color) { + getState().bgColor = color.getCSS(); + } + + @Override + public Color getColor() { + return color; + } + + /** + * Notifies the listeners that the color has changed + * + * @param color + * The color which it changed to + */ + public void fireColorChanged(Color color) { + fireEvent(new ColorChangeEvent(this, color)); + } + + @Override + protected ColorPickerGradientState getState() { + return (ColorPickerGradientState) super.getState(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java new file mode 100644 index 0000000000..9e5580c719 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java @@ -0,0 +1,258 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.awt.Point; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.shared.ui.colorpicker.ColorPickerGridServerRpc; +import com.vaadin.shared.ui.colorpicker.ColorPickerGridState; +import com.vaadin.ui.AbstractComponent; + +/** + * A component that represents a color selection grid within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerGrid extends AbstractComponent + implements ColorSelector { + + private static final String STYLENAME = "v-colorpicker-grid"; + + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + private ColorPickerGridServerRpc rpc = new ColorPickerGridServerRpc() { + + @Override + public void select(int x, int y) { + ColorPickerGrid.this.x = x; + ColorPickerGrid.this.y = y; + + fireColorChanged(colorGrid[y][x]); + } + + @Override + public void refresh() { + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + changedColors.put(new Point(row, col), colorGrid[row][col]); + } + } + sendChangedColors(); + markAsDirty(); + } + }; + + /** The x-coordinate. */ + private int x = 0; + + /** The y-coordinate. */ + private int y = 0; + + /** The rows. */ + private int rows; + + /** The columns. */ + private int columns; + + /** The color grid. */ + private Color[][] colorGrid = new Color[1][1]; + + /** The changed colors. */ + private final Map<Point, Color> changedColors = new HashMap<Point, Color>(); + + /** + * Instantiates a new color picker grid. + */ + public ColorPickerGrid() { + registerRpc(rpc); + setPrimaryStyleName(STYLENAME); + setColorGrid(new Color[1][1]); + setColor(Color.WHITE); + } + + /** + * Instantiates a new color picker grid. + * + * @param rows + * the rows + * @param columns + * the columns + */ + public ColorPickerGrid(int rows, int columns) { + registerRpc(rpc); + setPrimaryStyleName(STYLENAME); + setColorGrid(new Color[rows][columns]); + setColor(Color.WHITE); + } + + /** + * Instantiates a new color picker grid. + * + * @param colors + * the colors + */ + public ColorPickerGrid(Color[][] colors) { + registerRpc(rpc); + setPrimaryStyleName(STYLENAME); + setColorGrid(colors); + } + + private void setColumnCount(int columns) { + this.columns = columns; + getState().columnCount = columns; + } + + private void setRowCount(int rows) { + this.rows = rows; + getState().rowCount = rows; + } + + private void sendChangedColors() { + if (!changedColors.isEmpty()) { + String[] colors = new String[changedColors.size()]; + String[] XCoords = new String[changedColors.size()]; + String[] YCoords = new String[changedColors.size()]; + int counter = 0; + for (Point p : changedColors.keySet()) { + Color c = changedColors.get(p); + if (c == null) { + continue; + } + + String color = c.getCSS(); + + colors[counter] = color; + XCoords[counter] = String.valueOf((int) p.getX()); + YCoords[counter] = String.valueOf((int) p.getY()); + counter++; + } + getState().changedColor = colors; + getState().changedX = XCoords; + getState().changedY = YCoords; + + changedColors.clear(); + } + } + + /** + * Sets the color grid. + * + * @param colors + * the new color grid + */ + public void setColorGrid(Color[][] colors) { + setRowCount(colors.length); + setColumnCount(colors[0].length); + colorGrid = colors; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + changedColors.put(new Point(row, col), colorGrid[row][col]); + } + } + sendChangedColors(); + + markAsDirty(); + } + + /** + * Adds a color change listener + * + * @param listener + * The color change listener + */ + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + @Override + public Color getColor() { + return colorGrid[x][y]; + } + + /** + * Removes a color change listener + * + * @param listener + * The listener + */ + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + @Override + public void setColor(Color color) { + colorGrid[x][y] = color; + changedColors.put(new Point(x, y), color); + sendChangedColors(); + markAsDirty(); + } + + /** + * Sets the position. + * + * @param x + * the x + * @param y + * the y + */ + public void setPosition(int x, int y) { + if (x >= 0 && x < columns && y >= 0 && y < rows) { + this.x = x; + this.y = y; + } + } + + /** + * Gets the position. + * + * @return the position + */ + public int[] getPosition() { + return new int[] { x, y }; + } + + /** + * Notifies the listeners that a color change has occurred + * + * @param color + * The color which it changed to + */ + public void fireColorChanged(Color color) { + fireEvent(new ColorChangeEvent(this, color)); + } + + @Override + protected ColorPickerGridState getState() { + return (ColorPickerGridState) super.getState(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java new file mode 100644 index 0000000000..1173faf152 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java @@ -0,0 +1,217 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.ui.CustomComponent; + +/** + * A component that represents color selection history within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerHistory extends CustomComponent + implements ColorSelector, ColorChangeListener { + + private static final String STYLENAME = "v-colorpicker-history"; + + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + /** The rows. */ + private static final int rows = 4; + + /** The columns. */ + private static final int columns = 15; + + /** Temporary color history for when the component is detached. */ + private ArrayBlockingQueue<Color> tempHistory = new ArrayBlockingQueue<Color>( + rows * columns); + + /** The grid. */ + private final ColorPickerGrid grid; + + /** + * Instantiates a new color picker history. + */ + public ColorPickerHistory() { + setPrimaryStyleName(STYLENAME); + + grid = new ColorPickerGrid(rows, columns); + grid.setWidth("100%"); + grid.setPosition(0, 0); + grid.addColorChangeListener(this); + + setCompositionRoot(grid); + } + + @Override + public void attach() { + super.attach(); + createColorHistoryIfNecessary(); + } + + private void createColorHistoryIfNecessary() { + List<Color> tempColors = new ArrayList<Color>(tempHistory); + if (getSession().getAttribute("colorPickerHistory") == null) { + getSession().setAttribute("colorPickerHistory", + new ArrayBlockingQueue<Color>(rows * columns)); + } + for (Color color : tempColors) { + setColor(color); + } + tempHistory.clear(); + } + + @SuppressWarnings("unchecked") + private ArrayBlockingQueue<Color> getColorHistory() { + if (isAttached()) { + Object colorHistory = getSession() + .getAttribute("colorPickerHistory"); + if (colorHistory instanceof ArrayBlockingQueue<?>) { + return (ArrayBlockingQueue<Color>) colorHistory; + } + } + return tempHistory; + } + + @Override + public void setHeight(String height) { + super.setHeight(height); + grid.setHeight(height); + } + + @Override + public void setColor(Color color) { + + ArrayBlockingQueue<Color> colorHistory = getColorHistory(); + + // Check that the color does not already exist + boolean exists = false; + Iterator<Color> iter = colorHistory.iterator(); + while (iter.hasNext()) { + if (color.equals(iter.next())) { + exists = true; + break; + } + } + + // If the color does not exist then add it + if (!exists) { + if (!colorHistory.offer(color)) { + colorHistory.poll(); + colorHistory.offer(color); + } + } + + List<Color> colorList = new ArrayList<Color>(colorHistory); + + // Invert order of colors + Collections.reverse(colorList); + + // Move the selected color to the front of the list + Collections.swap(colorList, colorList.indexOf(color), 0); + + // Create 2d color map + Color[][] colors = new Color[rows][columns]; + iter = colorList.iterator(); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + if (iter.hasNext()) { + colors[row][col] = iter.next(); + } else { + colors[row][col] = Color.WHITE; + } + } + } + + grid.setColorGrid(colors); + grid.markAsDirty(); + } + + @Override + public Color getColor() { + return getColorHistory().peek(); + } + + /** + * Gets the history. + * + * @return the history + */ + public List<Color> getHistory() { + ArrayBlockingQueue<Color> colorHistory = getColorHistory(); + Color[] array = colorHistory.toArray(new Color[colorHistory.size()]); + return Collections.unmodifiableList(Arrays.asList(array)); + } + + /** + * Checks if the history contains given color. + * + * @param c + * the color + * + * @return true, if successful + */ + public boolean hasColor(Color c) { + return getColorHistory().contains(c); + } + + /** + * Adds a color change listener + * + * @param listener + * The listener + */ + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + /** + * Removes a color change listener + * + * @param listener + * The listener + */ + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + @Override + public void colorChanged(ColorChangeEvent event) { + fireEvent(new ColorChangeEvent(this, event.getColor())); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java new file mode 100644 index 0000000000..dbf3b18bf3 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java @@ -0,0 +1,759 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.shared.ui.MarginInfo; +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.ui.AbstractColorPicker.Coordinates2Color; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Component; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Layout; +import com.vaadin.ui.Slider; +import com.vaadin.ui.Slider.ValueOutOfBoundsException; +import com.vaadin.ui.TabSheet; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +/** + * A component that represents color selection popup within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerPopup extends Window + implements ClickListener, ColorChangeListener, ColorSelector { + + private static final String STYLENAME = "v-colorpicker-popup"; + + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + /** The tabs. */ + private final TabSheet tabs = new TabSheet(); + + private Component rgbTab; + + private Component hsvTab; + + private Component swatchesTab; + + /** The layout. */ + private final VerticalLayout layout; + + /** The ok button. */ + private final Button ok = new Button("OK"); + + /** The cancel button. */ + private final Button cancel = new Button("Cancel"); + + /** The resize button. */ + private final Button resize = new Button("show/hide history"); + + /** The selected color. */ + private Color selectedColor = Color.WHITE; + + /** The history. */ + private ColorPickerHistory history; + + /** The history container. */ + private Layout historyContainer; + + /** The rgb gradient. */ + private ColorPickerGradient rgbGradient; + + /** The hsv gradient. */ + private ColorPickerGradient hsvGradient; + + /** The red slider. */ + private Slider redSlider; + + /** The green slider. */ + private Slider greenSlider; + + /** The blue slider. */ + private Slider blueSlider; + + /** The hue slider. */ + private Slider hueSlider; + + /** The saturation slider. */ + private Slider saturationSlider; + + /** The value slider. */ + private Slider valueSlider; + + /** The preview on the rgb tab. */ + private ColorPickerPreview rgbPreview; + + /** The preview on the hsv tab. */ + private ColorPickerPreview hsvPreview; + + /** The preview on the swatches tab. */ + private ColorPickerPreview selPreview; + + /** The color select. */ + private ColorPickerSelect colorSelect; + + /** The selectors. */ + private final Set<ColorSelector> selectors = new HashSet<ColorSelector>(); + + /** + * Set true while the slider values are updated after colorChange. When + * true, valueChange reactions from the sliders are disabled, because + * otherwise the set color may become corrupted as it is repeatedly re-set + * in valueChangeListeners using values from sliders that may not have been + * updated yet. + */ + private boolean updatingColors = false; + + private ColorPickerPopup() { + // Set the layout + layout = new VerticalLayout(); + layout.setSpacing(false); + layout.setMargin(false); + layout.setWidth("100%"); + layout.setHeight(null); + + setContent(layout); + setStyleName(STYLENAME); + setResizable(false); + setImmediate(true); + // Create the history + history = new ColorPickerHistory(); + history.addColorChangeListener(this); + } + + /** + * Instantiates a new color picker popup. + */ + public ColorPickerPopup(Color initialColor) { + this(); + selectedColor = initialColor; + initContents(); + } + + private void initContents() { + // Create the preview on the rgb tab + rgbPreview = new ColorPickerPreview(selectedColor); + rgbPreview.setWidth("240px"); + rgbPreview.setHeight("20px"); + rgbPreview.addColorChangeListener(this); + selectors.add(rgbPreview); + + // Create the preview on the hsv tab + hsvPreview = new ColorPickerPreview(selectedColor); + hsvPreview.setWidth("240px"); + hsvPreview.setHeight("20px"); + hsvPreview.addColorChangeListener(this); + selectors.add(hsvPreview); + + // Create the preview on the swatches tab + selPreview = new ColorPickerPreview(selectedColor); + selPreview.setWidth("100%"); + selPreview.setHeight("20px"); + selPreview.addColorChangeListener(this); + selectors.add(selPreview); + + // Create the tabs + rgbTab = createRGBTab(selectedColor); + tabs.addTab(rgbTab, "RGB", null); + + hsvTab = createHSVTab(selectedColor); + tabs.addTab(hsvTab, "HSV", null); + + swatchesTab = createSelectTab(); + tabs.addTab(swatchesTab, "Swatches", null); + + // Add the tabs + tabs.setWidth("100%"); + + layout.addComponent(tabs); + + // Add the history + history.setWidth("97%"); + history.setHeight("22px"); + + // Create the default colors + List<Color> defaultColors = new ArrayList<Color>(); + defaultColors.add(Color.BLACK); + defaultColors.add(Color.WHITE); + + // Create the history + VerticalLayout innerContainer = new VerticalLayout(); + innerContainer.setWidth("100%"); + innerContainer.setHeight(null); + innerContainer.addComponent(history); + + VerticalLayout outerContainer = new VerticalLayout(); + outerContainer.setWidth("99%"); + outerContainer.setHeight("27px"); + outerContainer.addComponent(innerContainer); + historyContainer = outerContainer; + + layout.addComponent(historyContainer); + + // Add the resize button for the history + resize.addClickListener(this); + resize.setData(new Boolean(false)); + resize.setWidth("100%"); + resize.setHeight("10px"); + resize.setPrimaryStyleName("resize-button"); + layout.addComponent(resize); + + // Add the buttons + ok.setWidth("70px"); + ok.addClickListener(this); + + cancel.setWidth("70px"); + cancel.addClickListener(this); + + HorizontalLayout buttons = new HorizontalLayout(); + buttons.addComponent(ok); + buttons.addComponent(cancel); + buttons.setWidth("100%"); + buttons.setHeight("30px"); + buttons.setComponentAlignment(ok, Alignment.MIDDLE_CENTER); + buttons.setComponentAlignment(cancel, Alignment.MIDDLE_CENTER); + layout.addComponent(buttons); + } + + /** + * Creates the RGB tab. + * + * @return the component + */ + private Component createRGBTab(Color color) { + VerticalLayout rgbLayout = new VerticalLayout(); + rgbLayout.setMargin(new MarginInfo(false, false, true, false)); + rgbLayout.addComponent(rgbPreview); + rgbLayout.setStyleName("rgbtab"); + + // Add the RGB color gradient + rgbGradient = new ColorPickerGradient("rgb-gradient", RGBConverter); + rgbGradient.setColor(color); + rgbGradient.addColorChangeListener(this); + rgbLayout.addComponent(rgbGradient); + selectors.add(rgbGradient); + + // Add the RGB sliders + VerticalLayout sliders = new VerticalLayout(); + sliders.setStyleName("rgb-sliders"); + + redSlider = createRGBSlider("Red", "red"); + greenSlider = createRGBSlider("Green", "green"); + blueSlider = createRGBSlider("Blue", "blue"); + setRgbSliderValues(color); + + redSlider.addValueChangeListener(e -> { + double red = e.getValue(); + if (!updatingColors) { + Color newColor = new Color((int) red, selectedColor.getGreen(), + selectedColor.getBlue()); + setColor(newColor); + } + }); + + sliders.addComponent(redSlider); + + greenSlider.addValueChangeListener(e -> { + double green = e.getValue(); + if (!updatingColors) { + Color newColor = new Color(selectedColor.getRed(), (int) green, + selectedColor.getBlue()); + setColor(newColor); + } + }); + sliders.addComponent(greenSlider); + + blueSlider.addValueChangeListener(e -> { + double blue = e.getValue(); + if (!updatingColors) { + Color newColor = new Color(selectedColor.getRed(), + selectedColor.getGreen(), (int) blue); + setColor(newColor); + } + }); + sliders.addComponent(blueSlider); + + rgbLayout.addComponent(sliders); + + return rgbLayout; + } + + private Slider createRGBSlider(String caption, String styleName) { + Slider redSlider = new Slider(caption, 0, 255); + redSlider.setImmediate(true); + redSlider.setStyleName("rgb-slider"); + redSlider.setWidth("220px"); + redSlider.addStyleName(styleName); + return redSlider; + } + + /** + * Creates the hsv tab. + * + * @return the component + */ + private Component createHSVTab(Color color) { + VerticalLayout hsvLayout = new VerticalLayout(); + hsvLayout.setMargin(new MarginInfo(false, false, true, false)); + hsvLayout.addComponent(hsvPreview); + hsvLayout.setStyleName("hsvtab"); + + // Add the hsv gradient + hsvGradient = new ColorPickerGradient("hsv-gradient", HSVConverter); + hsvGradient.setColor(color); + hsvGradient.addColorChangeListener(this); + hsvLayout.addComponent(hsvGradient); + selectors.add(hsvGradient); + + VerticalLayout sliders = new VerticalLayout(); + sliders.setStyleName("hsv-sliders"); + + hueSlider = new Slider("Hue", 0, 360); + saturationSlider = new Slider("Saturation", 0, 100); + valueSlider = new Slider("Value", 0, 100); + + float[] hsv = color.getHSV(); + setHsvSliderValues(hsv); + + hueSlider.setStyleName("hsv-slider"); + hueSlider.addStyleName("hue-slider"); + hueSlider.setWidth("220px"); + hueSlider.setImmediate(true); + hueSlider.addValueChangeListener(event -> { + if (!updatingColors) { + float hue = (Float.parseFloat(event.getValue().toString())) + / 360f; + float saturation = (Float + .parseFloat(saturationSlider.getValue().toString())) + / 100f; + float value = (Float + .parseFloat(valueSlider.getValue().toString())) / 100f; + + // Set the color + Color newColor = new Color( + Color.HSVtoRGB(hue, saturation, value)); + setColor(newColor); + + /* + * Set the background color of the hue gradient. This has to be + * done here since in the conversion the base color information + * is lost when color is black/white + */ + Color bgColor = new Color(Color.HSVtoRGB(hue, 1f, 1f)); + hsvGradient.setBackgroundColor(bgColor); + } + }); + sliders.addComponent(hueSlider); + + saturationSlider.setStyleName("hsv-slider"); + saturationSlider.setWidth("220px"); + saturationSlider.setImmediate(true); + saturationSlider.addValueChangeListener(event -> { + if (!updatingColors) { + float hue = (Float.parseFloat(hueSlider.getValue().toString())) + / 360f; + float saturation = (Float + .parseFloat(event.getValue().toString())) / 100f; + float value = (Float + .parseFloat(valueSlider.getValue().toString())) / 100f; + Color newColor = new Color( + Color.HSVtoRGB(hue, saturation, value)); + setColor(newColor); + } + }); + sliders.addComponent(saturationSlider); + + valueSlider.setStyleName("hsv-slider"); + valueSlider.setWidth("220px"); + valueSlider.setImmediate(true); + valueSlider.addValueChangeListener(event -> { + if (!updatingColors) { + float hue = (Float.parseFloat(hueSlider.getValue().toString())) + / 360f; + float saturation = (Float + .parseFloat(saturationSlider.getValue().toString())) + / 100f; + float value = (Float.parseFloat(event.getValue().toString())) + / 100f; + + Color newColor = new Color( + Color.HSVtoRGB(hue, saturation, value)); + setColor(newColor); + } + }); + + sliders.addComponent(valueSlider); + hsvLayout.addComponent(sliders); + + return hsvLayout; + } + + /** + * Creates the select tab. + * + * @return the component + */ + private Component createSelectTab() { + VerticalLayout selLayout = new VerticalLayout(); + selLayout.setMargin(new MarginInfo(false, false, true, false)); + selLayout.addComponent(selPreview); + selLayout.addStyleName("seltab"); + + colorSelect = new ColorPickerSelect(); + colorSelect.addColorChangeListener(this); + selLayout.addComponent(colorSelect); + + return selLayout; + } + + @Override + public void buttonClick(ClickEvent event) { + // History resize was clicked + if (event.getButton() == resize) { + boolean state = (Boolean) resize.getData(); + + // minimize + if (state) { + historyContainer.setHeight("27px"); + history.setHeight("22px"); + + // maximize + } else { + historyContainer.setHeight("90px"); + history.setHeight("85px"); + } + + resize.setData(new Boolean(!state)); + } + + // Ok button was clicked + else if (event.getButton() == ok) { + history.setColor(getColor()); + fireColorChanged(); + close(); + } + + // Cancel button was clicked + else if (event.getButton() == cancel) { + close(); + } + + } + + /** + * Notifies the listeners that the color changed + */ + public void fireColorChanged() { + fireEvent(new ColorChangeEvent(this, getColor())); + } + + /** + * Gets the history. + * + * @return the history + */ + public ColorPickerHistory getHistory() { + return history; + } + + @Override + public void setColor(Color color) { + if (color == null) { + return; + } + + selectedColor = color; + + hsvGradient.setColor(selectedColor); + hsvPreview.setColor(selectedColor); + + rgbGradient.setColor(selectedColor); + rgbPreview.setColor(selectedColor); + + selPreview.setColor(selectedColor); + } + + @Override + public Color getColor() { + return selectedColor; + } + + /** + * Gets the color history. + * + * @return the color history + */ + public List<Color> getColorHistory() { + return Collections.unmodifiableList(history.getHistory()); + } + + @Override + public void colorChanged(ColorChangeEvent event) { + setColor(event.getColor()); + + updatingColors = true; + + setRgbSliderValues(selectedColor); + float[] hsv = selectedColor.getHSV(); + setHsvSliderValues(hsv); + + updatingColors = false; + + for (ColorSelector s : selectors) { + if (event.getSource() != s && s != this + && s.getColor() != selectedColor) { + s.setColor(selectedColor); + } + } + } + + private void setRgbSliderValues(Color color) { + try { + redSlider.setValue(((Integer) color.getRed()).doubleValue()); + blueSlider.setValue(((Integer) color.getBlue()).doubleValue()); + greenSlider.setValue(((Integer) color.getGreen()).doubleValue()); + } catch (ValueOutOfBoundsException e) { + getLogger().log(Level.WARNING, + "Unable to set RGB color value to " + color.getRed() + "," + + color.getGreen() + "," + color.getBlue(), + e); + } + } + + private void setHsvSliderValues(float[] hsv) { + try { + hueSlider.setValue(((Float) (hsv[0] * 360f)).doubleValue()); + saturationSlider.setValue(((Float) (hsv[1] * 100f)).doubleValue()); + valueSlider.setValue(((Float) (hsv[2] * 100f)).doubleValue()); + } catch (ValueOutOfBoundsException e) { + getLogger().log(Level.WARNING, "Unable to set HSV color value to " + + hsv[0] + "," + hsv[1] + "," + hsv[2], e); + } + } + + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + /** + * Checks the visibility of the given tab + * + * @param tab + * The tab to check + * @return true if tab is visible, false otherwise + */ + private boolean tabIsVisible(Component tab) { + Iterator<Component> tabIterator = tabs.getComponentIterator(); + while (tabIterator.hasNext()) { + if (tabIterator.next() == tab) { + return true; + } + } + return false; + } + + /** + * How many tabs are visible + * + * @return The number of tabs visible + */ + private int tabsNumVisible() { + Iterator<Component> tabIterator = tabs.getComponentIterator(); + int tabCounter = 0; + while (tabIterator.hasNext()) { + tabIterator.next(); + tabCounter++; + } + return tabCounter; + } + + /** + * Checks if tabs are needed and hides them if not + */ + private void checkIfTabsNeeded() { + tabs.hideTabs(tabsNumVisible() == 1); + } + + /** + * Set RGB tab visibility + * + * @param visible + * The visibility of the RGB tab + */ + public void setRGBTabVisible(boolean visible) { + if (visible && !tabIsVisible(rgbTab)) { + tabs.addTab(rgbTab, "RGB", null); + checkIfTabsNeeded(); + } else if (!visible && tabIsVisible(rgbTab)) { + tabs.removeComponent(rgbTab); + checkIfTabsNeeded(); + } + } + + /** + * Set HSV tab visibility + * + * @param visible + * The visibility of the HSV tab + */ + public void setHSVTabVisible(boolean visible) { + if (visible && !tabIsVisible(hsvTab)) { + tabs.addTab(hsvTab, "HSV", null); + checkIfTabsNeeded(); + } else if (!visible && tabIsVisible(hsvTab)) { + tabs.removeComponent(hsvTab); + checkIfTabsNeeded(); + } + } + + /** + * Set Swatches tab visibility + * + * @param visible + * The visibility of the Swatches tab + */ + public void setSwatchesTabVisible(boolean visible) { + if (visible && !tabIsVisible(swatchesTab)) { + tabs.addTab(swatchesTab, "Swatches", null); + checkIfTabsNeeded(); + } else if (!visible && tabIsVisible(swatchesTab)) { + tabs.removeComponent(swatchesTab); + checkIfTabsNeeded(); + } + } + + /** + * Set the History visibility + * + * @param visible + */ + public void setHistoryVisible(boolean visible) { + historyContainer.setVisible(visible); + resize.setVisible(visible); + } + + /** + * Set the preview visibility + * + * @param visible + */ + public void setPreviewVisible(boolean visible) { + hsvPreview.setVisible(visible); + rgbPreview.setVisible(visible); + selPreview.setVisible(visible); + } + + /** RGB color converter */ + private Coordinates2Color RGBConverter = new Coordinates2Color() { + + @Override + public Color calculate(int x, int y) { + float h = (x / 220f); + float s = 1f; + float v = 1f; + + if (y < 110) { + s = y / 110f; + } else if (y > 110) { + v = 1f - (y - 110f) / 110f; + } + + return new Color(Color.HSVtoRGB(h, s, v)); + } + + @Override + public int[] calculate(Color color) { + + float[] hsv = color.getHSV(); + + int x = Math.round(hsv[0] * 220f); + int y = 0; + + // lower half + if (hsv[1] == 1f) { + y = Math.round(110f - (hsv[1] + hsv[2]) * 110f); + } else { + y = Math.round(hsv[1] * 110f); + } + + return new int[] { x, y }; + } + }; + + /** HSV color converter */ + Coordinates2Color HSVConverter = new Coordinates2Color() { + @Override + public int[] calculate(Color color) { + + float[] hsv = color.getHSV(); + + // Calculate coordinates + int x = Math.round(hsv[2] * 220.0f); + int y = Math.round(220 - hsv[1] * 220.0f); + + // Create background color of clean color + Color bgColor = new Color(Color.HSVtoRGB(hsv[0], 1f, 1f)); + hsvGradient.setBackgroundColor(bgColor); + + return new int[] { x, y }; + } + + @Override + public Color calculate(int x, int y) { + float saturation = 1f - (y / 220.0f); + float value = (x / 220.0f); + float hue = Float.parseFloat(hueSlider.getValue().toString()) + / 360f; + + Color color = new Color(Color.HSVtoRGB(hue, saturation, value)); + return color; + } + }; + + private static Logger getLogger() { + return Logger.getLogger(ColorPickerPopup.class.getName()); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPreview.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPreview.java new file mode 100644 index 0000000000..dc133ce156 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerPreview.java @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.lang.reflect.Method; + +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.ui.Component; +import com.vaadin.ui.CssLayout; +import com.vaadin.v7.ui.LegacyTextField; + +/** + * A component that represents color selection preview within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerPreview extends CssLayout + implements ColorSelector, ValueChangeListener { + + private static final String STYLE_DARK_COLOR = "v-textfield-dark"; + private static final String STYLE_LIGHT_COLOR = "v-textfield-light"; + + private static final Method COLOR_CHANGE_METHOD; + static { + try { + COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod( + "colorChanged", new Class[] { ColorChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in ColorPicker"); + } + } + + /** The color. */ + private Color color; + + /** The field. */ + private final LegacyTextField field; + + /** The old value. */ + private String oldValue; + + private ColorPickerPreview() { + setStyleName("v-colorpicker-preview"); + setImmediate(true); + field = new LegacyTextField(); + field.setImmediate(true); + field.setSizeFull(); + field.setStyleName("v-colorpicker-preview-textfield"); + field.setData(this); + field.addValueChangeListener(this); + addComponent(field); + } + + /** + * Instantiates a new color picker preview. + */ + public ColorPickerPreview(Color color) { + this(); + setColor(color); + } + + @Override + public void setColor(Color color) { + this.color = color; + + // Unregister listener + field.removeValueChangeListener(this); + + String colorCSS = color.getCSS(); + field.setValue(colorCSS); + + if (field.isValid()) { + oldValue = colorCSS; + } else { + field.setValue(oldValue); + } + + // Re-register listener + field.addValueChangeListener(this); + + // Set the text color + field.removeStyleName(STYLE_DARK_COLOR); + field.removeStyleName(STYLE_LIGHT_COLOR); + if (this.color.getRed() + this.color.getGreen() + + this.color.getBlue() < 3 * 128) { + field.addStyleName(STYLE_DARK_COLOR); + } else { + field.addStyleName(STYLE_LIGHT_COLOR); + } + + markAsDirty(); + } + + @Override + public Color getColor() { + return color; + } + + @Override + public void addColorChangeListener(ColorChangeListener listener) { + addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); + } + + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + removeListener(ColorChangeEvent.class, listener); + } + + @Override + public void valueChange(ValueChangeEvent event) { + String value = (String) event.getProperty().getValue(); + try { + if (value != null) { + /* + * Description of supported formats see + * http://www.w3schools.com/cssref/css_colors_legal.asp + */ + if (value.length() == 7 && value.startsWith("#")) { + // CSS color format (e.g. #000000) + int red = Integer.parseInt(value.substring(1, 3), 16); + int green = Integer.parseInt(value.substring(3, 5), 16); + int blue = Integer.parseInt(value.substring(5, 7), 16); + color = new Color(red, green, blue); + + } else if (value.startsWith("rgb")) { + // RGB color format rgb/rgba(255,255,255,0.1) + String[] colors = value.substring(value.indexOf("(") + 1, + value.length() - 1).split(","); + + int red = Integer.parseInt(colors[0]); + int green = Integer.parseInt(colors[1]); + int blue = Integer.parseInt(colors[2]); + if (colors.length > 3) { + int alpha = (int) (Double.parseDouble(colors[3]) + * 255d); + color = new Color(red, green, blue, alpha); + } else { + color = new Color(red, green, blue); + } + + } else if (value.startsWith("hsl")) { + // HSL color format hsl/hsla(100,50%,50%,1.0) + String[] colors = value.substring(value.indexOf("(") + 1, + value.length() - 1).split(","); + + int hue = Integer.parseInt(colors[0]); + int saturation = Integer + .parseInt(colors[1].replace("%", "")); + int lightness = Integer + .parseInt(colors[2].replace("%", "")); + int rgb = Color.HSLtoRGB(hue, saturation, lightness); + + if (colors.length > 3) { + int alpha = (int) (Double.parseDouble(colors[3]) + * 255d); + color = new Color(rgb); + color.setAlpha(alpha); + } else { + color = new Color(rgb); + } + } + + oldValue = value; + fireEvent(new ColorChangeEvent((Component) field.getData(), + color)); + } + + } catch (NumberFormatException nfe) { + // Revert value + field.setValue(oldValue); + } + } + + /** + * Called when the component is refreshing + */ + @Override + protected String getCss(Component c) { + return "background: " + color.getCSS(); + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerSelect.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerSelect.java new file mode 100644 index 0000000000..ae3dee4069 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorPickerSelect.java @@ -0,0 +1,235 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.VerticalLayout; + +/** + * A component that represents color selection swatches within a color picker. + * + * @since 7.0.0 + */ +public class ColorPickerSelect extends CustomComponent + implements ColorSelector, ValueChangeListener { + + /** The range. */ + private final ComboBox range; + + /** The grid. */ + private final ColorPickerGrid grid; + + /** + * The Enum ColorRangePropertyId. + */ + private enum ColorRangePropertyId { + ALL("All colors"), RED("Red colors"), GREEN("Green colors"), BLUE( + "Blue colors"); + + /** The caption. */ + private String caption; + + /** + * Instantiates a new color range property id. + * + * @param caption + * the caption + */ + ColorRangePropertyId(String caption) { + this.caption = caption; + } + + @Override + public String toString() { + return caption; + } + } + + /** + * Instantiates a new color picker select. + * + * @param rows + * the rows + * @param columns + * the columns + */ + public ColorPickerSelect() { + + VerticalLayout layout = new VerticalLayout(); + setCompositionRoot(layout); + + setStyleName("colorselect"); + setWidth("100%"); + + range = new ComboBox(); + range.setImmediate(true); + range.setImmediate(true); + range.setNullSelectionAllowed(false); + range.setNewItemsAllowed(false); + range.setWidth("100%"); + range.addValueChangeListener(this); + + for (ColorRangePropertyId id : ColorRangePropertyId.values()) { + range.addItem(id); + } + range.select(ColorRangePropertyId.ALL); + + layout.addComponent(range); + + grid = new ColorPickerGrid(createAllColors(14, 10)); + grid.setWidth("100%"); + + layout.addComponent(grid); + } + + /** + * Creates the all colors. + * + * @param rows + * the rows + * @param columns + * the columns + * + * @return the color[][] + */ + private Color[][] createAllColors(int rows, int columns) { + Color[][] colors = new Color[rows][columns]; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + + // Create the color grid by varying the saturation and value + if (row < (rows - 1)) { + // Calculate new hue value + float hue = ((float) col / (float) columns); + float saturation = 1f; + float value = 1f; + + // For the upper half use value=1 and variable + // saturation + if (row < (rows / 2)) { + saturation = ((row + 1f) / (rows / 2f)); + } else { + value = 1f - ((row - (rows / 2f)) / (rows / 2f)); + } + + colors[row][col] = new Color( + Color.HSVtoRGB(hue, saturation, value)); + } + + // The last row should have the black&white gradient + else { + float hue = 0f; + float saturation = 0f; + float value = 1f - ((float) col / (float) columns); + + colors[row][col] = new Color( + Color.HSVtoRGB(hue, saturation, value)); + } + } + } + + return colors; + } + + /** + * Creates the color. + * + * @param color + * the color + * @param rows + * the rows + * @param columns + * the columns + * + * @return the color[][] + */ + private Color[][] createColors(Color color, int rows, int columns) { + Color[][] colors = new Color[rows][columns]; + + float[] hsv = color.getHSV(); + + float hue = hsv[0]; + float saturation = 1f; + float value = 1f; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < columns; col++) { + + int index = row * columns + col; + saturation = 1f; + value = 1f; + + if (index <= ((rows * columns) / 2)) { + saturation = index + / (((float) rows * (float) columns) / 2f); + } else { + index -= ((rows * columns) / 2); + value = 1f + - index / (((float) rows * (float) columns) / 2f); + } + + colors[row][col] = new Color( + Color.HSVtoRGB(hue, saturation, value)); + } + } + + return colors; + } + + @Override + public Color getColor() { + return grid.getColor(); + } + + @Override + public void setColor(Color color) { + grid.getColor(); + } + + @Override + public void addColorChangeListener(ColorChangeListener listener) { + grid.addColorChangeListener(listener); + } + + @Override + public void removeColorChangeListener(ColorChangeListener listener) { + grid.removeColorChangeListener(listener); + } + + @Override + public void valueChange(ValueChangeEvent event) { + if (grid == null) { + return; + } + + if (event.getProperty().getValue() == ColorRangePropertyId.ALL) { + grid.setColorGrid(createAllColors(14, 10)); + } else if (event.getProperty().getValue() == ColorRangePropertyId.RED) { + grid.setColorGrid(createColors(new Color(0xFF, 0, 0), 14, 10)); + } else if (event.getProperty() + .getValue() == ColorRangePropertyId.GREEN) { + grid.setColorGrid(createColors(new Color(0, 0xFF, 0), 14, 10)); + } else if (event.getProperty() + .getValue() == ColorRangePropertyId.BLUE) { + grid.setColorGrid(createColors(new Color(0, 0, 0xFF), 14, 10)); + } + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorSelector.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorSelector.java new file mode 100644 index 0000000000..d9264745a8 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/ColorSelector.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.io.Serializable; + +import com.vaadin.shared.ui.colorpicker.Color; + +/** + * An interface for a color selector. + * + * @since 7.0.0 + */ +public interface ColorSelector extends Serializable, HasColorChangeListener { + + /** + * Sets the color. + * + * @param color + * the new color + */ + public void setColor(Color color); + + /** + * Gets the color. + * + * @return the color + */ + public Color getColor(); +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/HasColorChangeListener.java b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/HasColorChangeListener.java new file mode 100644 index 0000000000..7980111e2b --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/components/colorpicker/HasColorChangeListener.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2016 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.ui.components.colorpicker; + +import java.io.Serializable; + +public interface HasColorChangeListener extends Serializable { + + /** + * Adds a {@link ColorChangeListener} to the component. + * + * @param listener + */ + void addColorChangeListener(ColorChangeListener listener); + + /** + * Removes a {@link ColorChangeListener} from the component. + * + * @param listener + */ + void removeColorChangeListener(ColorChangeListener listener); + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/AbstractFilterTestBase.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/AbstractFilterTestBase.java new file mode 100644 index 0000000000..979f472e20 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/AbstractFilterTestBase.java @@ -0,0 +1,97 @@ +package com.vaadin.data.util.filter; + +import junit.framework.TestCase; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; + +public abstract class AbstractFilterTestBase<FILTERTYPE extends Filter> + extends TestCase { + + protected static final String PROPERTY1 = "property1"; + protected static final String PROPERTY2 = "property2"; + + protected static class TestItem<T1, T2> extends PropertysetItem { + + public TestItem(T1 value1, T2 value2) { + addItemProperty(PROPERTY1, new ObjectProperty<T1>(value1)); + addItemProperty(PROPERTY2, new ObjectProperty<T2>(value2)); + } + } + + protected static class NullProperty implements Property<String> { + + @Override + public String getValue() { + return null; + } + + @Override + public void setValue(String newValue) throws ReadOnlyException { + throw new ReadOnlyException(); + } + + @Override + public Class<String> getType() { + return String.class; + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public void setReadOnly(boolean newStatus) { + // do nothing + } + + } + + public static class SameItemFilter implements Filter { + + private final Item item; + private final Object propertyId; + + public SameItemFilter(Item item) { + this(item, ""); + } + + public SameItemFilter(Item item, Object propertyId) { + this.item = item; + this.propertyId = propertyId; + } + + @Override + public boolean passesFilter(Object itemId, Item item) + throws UnsupportedOperationException { + return this.item == item; + } + + @Override + public boolean appliesToProperty(Object propertyId) { + return this.propertyId != null ? this.propertyId.equals(propertyId) + : true; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !getClass().equals(obj.getClass())) { + return false; + } + SameItemFilter other = (SameItemFilter) obj; + return item == other.item + && (propertyId == null ? other.propertyId == null + : propertyId.equals(other.propertyId)); + } + + @Override + public int hashCode() { + return item.hashCode(); + } + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/AndOrFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/AndOrFilterTest.java new file mode 100644 index 0000000000..f825ef64c6 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/AndOrFilterTest.java @@ -0,0 +1,246 @@ +package com.vaadin.data.util.filter; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.BeanItem; + +public class AndOrFilterTest + extends AbstractFilterTestBase<AbstractJunctionFilter> { + + protected Item item1 = new BeanItem<Integer>(1); + protected Item item2 = new BeanItem<Integer>(2); + + @Test + public void testNoFilterAnd() { + Filter filter = new And(); + + Assert.assertTrue(filter.passesFilter(null, item1)); + } + + @Test + public void testSingleFilterAnd() { + Filter filter = new And(new SameItemFilter(item1)); + + Assert.assertTrue(filter.passesFilter(null, item1)); + Assert.assertFalse(filter.passesFilter(null, item2)); + } + + @Test + public void testTwoFilterAnd() { + Filter filter1 = new And(new SameItemFilter(item1), + new SameItemFilter(item1)); + Filter filter2 = new And(new SameItemFilter(item1), + new SameItemFilter(item2)); + + Assert.assertTrue(filter1.passesFilter(null, item1)); + Assert.assertFalse(filter1.passesFilter(null, item2)); + + Assert.assertFalse(filter2.passesFilter(null, item1)); + Assert.assertFalse(filter2.passesFilter(null, item2)); + } + + @Test + public void testThreeFilterAnd() { + Filter filter1 = new And(new SameItemFilter(item1), + new SameItemFilter(item1), new SameItemFilter(item1)); + Filter filter2 = new And(new SameItemFilter(item1), + new SameItemFilter(item1), new SameItemFilter(item2)); + + Assert.assertTrue(filter1.passesFilter(null, item1)); + Assert.assertFalse(filter1.passesFilter(null, item2)); + + Assert.assertFalse(filter2.passesFilter(null, item1)); + Assert.assertFalse(filter2.passesFilter(null, item2)); + } + + @Test + public void testNoFilterOr() { + Filter filter = new Or(); + + Assert.assertFalse(filter.passesFilter(null, item1)); + } + + @Test + public void testSingleFilterOr() { + Filter filter = new Or(new SameItemFilter(item1)); + + Assert.assertTrue(filter.passesFilter(null, item1)); + Assert.assertFalse(filter.passesFilter(null, item2)); + } + + @Test + public void testTwoFilterOr() { + Filter filter1 = new Or(new SameItemFilter(item1), + new SameItemFilter(item1)); + Filter filter2 = new Or(new SameItemFilter(item1), + new SameItemFilter(item2)); + + Assert.assertTrue(filter1.passesFilter(null, item1)); + Assert.assertFalse(filter1.passesFilter(null, item2)); + + Assert.assertTrue(filter2.passesFilter(null, item1)); + Assert.assertTrue(filter2.passesFilter(null, item2)); + } + + @Test + public void testThreeFilterOr() { + Filter filter1 = new Or(new SameItemFilter(item1), + new SameItemFilter(item1), new SameItemFilter(item1)); + Filter filter2 = new Or(new SameItemFilter(item1), + new SameItemFilter(item1), new SameItemFilter(item2)); + + Assert.assertTrue(filter1.passesFilter(null, item1)); + Assert.assertFalse(filter1.passesFilter(null, item2)); + + Assert.assertTrue(filter2.passesFilter(null, item1)); + Assert.assertTrue(filter2.passesFilter(null, item2)); + } + + @Test + public void testAndEqualsHashCode() { + Filter filter0 = new And(); + Filter filter0b = new And(); + Filter filter1a = new And(new SameItemFilter(item1)); + Filter filter1a2 = new And(new SameItemFilter(item1)); + Filter filter1b = new And(new SameItemFilter(item2)); + Filter filter2a = new And(new SameItemFilter(item1), + new SameItemFilter(item1)); + Filter filter2b = new And(new SameItemFilter(item1), + new SameItemFilter(item2)); + Filter filter2b2 = new And(new SameItemFilter(item1), + new SameItemFilter(item2)); + Filter other0 = new Or(); + Filter other1 = new Or(new SameItemFilter(item1)); + + Assert.assertEquals(filter0, filter0); + Assert.assertEquals(filter0, filter0b); + Assert.assertFalse(filter0.equals(filter1a)); + Assert.assertFalse(filter0.equals(other0)); + Assert.assertFalse(filter0.equals(other1)); + + Assert.assertFalse(filter1a.equals(filter1b)); + Assert.assertFalse(filter1a.equals(other1)); + + Assert.assertFalse(filter1a.equals(filter2a)); + Assert.assertFalse(filter2a.equals(filter1a)); + + Assert.assertFalse(filter2a.equals(filter2b)); + Assert.assertEquals(filter2b, filter2b2); + + // hashCode() + Assert.assertEquals(filter0.hashCode(), filter0.hashCode()); + Assert.assertEquals(filter0.hashCode(), filter0b.hashCode()); + Assert.assertEquals(filter1a.hashCode(), filter1a.hashCode()); + Assert.assertEquals(filter1a.hashCode(), filter1a2.hashCode()); + Assert.assertEquals(filter2a.hashCode(), filter2a.hashCode()); + Assert.assertEquals(filter2b.hashCode(), filter2b2.hashCode()); + } + + @Test + public void testOrEqualsHashCode() { + Filter filter0 = new Or(); + Filter filter0b = new Or(); + Filter filter1a = new Or(new SameItemFilter(item1)); + Filter filter1a2 = new Or(new SameItemFilter(item1)); + Filter filter1b = new Or(new SameItemFilter(item2)); + Filter filter2a = new Or(new SameItemFilter(item1), + new SameItemFilter(item1)); + Filter filter2b = new Or(new SameItemFilter(item1), + new SameItemFilter(item2)); + Filter filter2b2 = new Or(new SameItemFilter(item1), + new SameItemFilter(item2)); + Filter other0 = new And(); + Filter other1 = new And(new SameItemFilter(item1)); + + Assert.assertEquals(filter0, filter0); + Assert.assertEquals(filter0, filter0b); + Assert.assertFalse(filter0.equals(filter1a)); + Assert.assertFalse(filter0.equals(other0)); + Assert.assertFalse(filter0.equals(other1)); + + Assert.assertFalse(filter1a.equals(filter1b)); + Assert.assertFalse(filter1a.equals(other1)); + + Assert.assertFalse(filter1a.equals(filter2a)); + Assert.assertFalse(filter2a.equals(filter1a)); + + Assert.assertFalse(filter2a.equals(filter2b)); + Assert.assertEquals(filter2b, filter2b2); + + // hashCode() + Assert.assertEquals(filter0.hashCode(), filter0.hashCode()); + Assert.assertEquals(filter0.hashCode(), filter0b.hashCode()); + Assert.assertEquals(filter1a.hashCode(), filter1a.hashCode()); + Assert.assertEquals(filter1a.hashCode(), filter1a2.hashCode()); + Assert.assertEquals(filter2a.hashCode(), filter2a.hashCode()); + Assert.assertEquals(filter2b.hashCode(), filter2b2.hashCode()); + } + + @Test + public void testAndAppliesToProperty() { + Filter filter0 = new And(); + Filter filter1a = new And(new SameItemFilter(item1, "a")); + Filter filter1b = new And(new SameItemFilter(item1, "b")); + Filter filter2aa = new And(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "a")); + Filter filter2ab = new And(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "b")); + Filter filter3abc = new And(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "b"), new SameItemFilter(item1, "c")); + + // empty And does not filter out anything + Assert.assertFalse(filter0.appliesToProperty("a")); + Assert.assertFalse(filter0.appliesToProperty("d")); + + Assert.assertTrue(filter1a.appliesToProperty("a")); + Assert.assertFalse(filter1a.appliesToProperty("b")); + Assert.assertFalse(filter1b.appliesToProperty("a")); + Assert.assertTrue(filter1b.appliesToProperty("b")); + + Assert.assertTrue(filter2aa.appliesToProperty("a")); + Assert.assertFalse(filter2aa.appliesToProperty("b")); + Assert.assertTrue(filter2ab.appliesToProperty("a")); + Assert.assertTrue(filter2ab.appliesToProperty("b")); + + Assert.assertTrue(filter3abc.appliesToProperty("a")); + Assert.assertTrue(filter3abc.appliesToProperty("b")); + Assert.assertTrue(filter3abc.appliesToProperty("c")); + Assert.assertFalse(filter3abc.appliesToProperty("d")); + } + + @Test + public void testOrAppliesToProperty() { + Filter filter0 = new Or(); + Filter filter1a = new Or(new SameItemFilter(item1, "a")); + Filter filter1b = new Or(new SameItemFilter(item1, "b")); + Filter filter2aa = new Or(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "a")); + Filter filter2ab = new Or(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "b")); + Filter filter3abc = new Or(new SameItemFilter(item1, "a"), + new SameItemFilter(item1, "b"), new SameItemFilter(item1, "c")); + + // empty Or filters out everything + Assert.assertTrue(filter0.appliesToProperty("a")); + Assert.assertTrue(filter0.appliesToProperty("d")); + + Assert.assertTrue(filter1a.appliesToProperty("a")); + Assert.assertFalse(filter1a.appliesToProperty("b")); + Assert.assertFalse(filter1b.appliesToProperty("a")); + Assert.assertTrue(filter1b.appliesToProperty("b")); + + Assert.assertTrue(filter2aa.appliesToProperty("a")); + Assert.assertFalse(filter2aa.appliesToProperty("b")); + Assert.assertTrue(filter2ab.appliesToProperty("a")); + Assert.assertTrue(filter2ab.appliesToProperty("b")); + + Assert.assertTrue(filter3abc.appliesToProperty("a")); + Assert.assertTrue(filter3abc.appliesToProperty("b")); + Assert.assertTrue(filter3abc.appliesToProperty("c")); + Assert.assertFalse(filter3abc.appliesToProperty("d")); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterDateTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterDateTest.java new file mode 100644 index 0000000000..7c3dba9db3 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterDateTest.java @@ -0,0 +1,142 @@ +package com.vaadin.data.util.filter; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Compare.Greater; +import com.vaadin.data.util.filter.Compare.GreaterOrEqual; +import com.vaadin.data.util.filter.Compare.Less; +import com.vaadin.data.util.filter.Compare.LessOrEqual; + +public class CompareFilterDateTest extends AbstractFilterTestBase<Compare> { + + protected Item itemNullUtilDate; + protected Item itemNullSqlDate; + protected Item itemUtilDate; + protected Item itemSqlDate; + + protected SimpleDateFormat formatter = new SimpleDateFormat("ddMMyyyy"); + + protected Filter equalCompUtilDate; + protected Filter greaterCompUtilDate; + protected Filter lessCompUtilDate; + protected Filter greaterEqualCompUtilDate; + protected Filter lessEqualCompUtilDate; + + protected Filter equalCompSqlDate; + protected Filter greaterCompSqlDate; + protected Filter lessCompSqlDate; + protected Filter greaterEqualCompSqlDate; + protected Filter lessEqualCompSqlDate; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + equalCompUtilDate = new Equal(PROPERTY1, formatter.parse("26072016")); + greaterCompUtilDate = new Greater(PROPERTY1, + formatter.parse("26072016")); + lessCompUtilDate = new Less(PROPERTY1, formatter.parse("26072016")); + greaterEqualCompUtilDate = new GreaterOrEqual(PROPERTY1, + formatter.parse("26072016")); + lessEqualCompUtilDate = new LessOrEqual(PROPERTY1, + formatter.parse("26072016")); + + equalCompSqlDate = new Equal(PROPERTY1, + new java.sql.Date(formatter.parse("26072016").getTime())); + greaterCompSqlDate = new Greater(PROPERTY1, + new java.sql.Date(formatter.parse("26072016").getTime())); + lessCompSqlDate = new Less(PROPERTY1, + new java.sql.Date(formatter.parse("26072016").getTime())); + greaterEqualCompSqlDate = new GreaterOrEqual(PROPERTY1, + new java.sql.Date(formatter.parse("26072016").getTime())); + lessEqualCompSqlDate = new LessOrEqual(PROPERTY1, + new java.sql.Date(formatter.parse("26072016").getTime())); + + itemNullUtilDate = new PropertysetItem(); + itemNullUtilDate.addItemProperty(PROPERTY1, + new ObjectProperty<Date>(null, Date.class)); + itemNullSqlDate = new PropertysetItem(); + itemNullSqlDate.addItemProperty(PROPERTY1, + new ObjectProperty<java.sql.Date>(null, java.sql.Date.class)); + itemUtilDate = new PropertysetItem(); + itemUtilDate.addItemProperty(PROPERTY1, new ObjectProperty<Date>( + formatter.parse("25072016"), Date.class)); + itemSqlDate = new PropertysetItem(); + itemSqlDate.addItemProperty(PROPERTY1, + new ObjectProperty<java.sql.Date>( + new java.sql.Date( + formatter.parse("25072016").getTime()), + java.sql.Date.class)); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + itemNullUtilDate = null; + itemNullSqlDate = null; + itemUtilDate = null; + itemSqlDate = null; + } + + @Test + public void testCompareUtilDatesAndUtilDates() { + Assert.assertFalse( + equalCompUtilDate.passesFilter(null, itemNullUtilDate)); + Assert.assertFalse(equalCompUtilDate.passesFilter(null, itemUtilDate)); + Assert.assertFalse( + greaterCompUtilDate.passesFilter(null, itemUtilDate)); + Assert.assertTrue(lessCompUtilDate.passesFilter(null, itemUtilDate)); + Assert.assertFalse( + greaterEqualCompUtilDate.passesFilter(null, itemUtilDate)); + Assert.assertTrue( + lessEqualCompUtilDate.passesFilter(null, itemUtilDate)); + } + + @Test + public void testCompareUtilDatesAndSqlDates() { + Assert.assertFalse( + equalCompUtilDate.passesFilter(null, itemNullSqlDate)); + Assert.assertFalse(equalCompUtilDate.passesFilter(null, itemSqlDate)); + Assert.assertFalse(greaterCompUtilDate.passesFilter(null, itemSqlDate)); + Assert.assertTrue(lessCompUtilDate.passesFilter(null, itemSqlDate)); + Assert.assertFalse( + greaterEqualCompUtilDate.passesFilter(null, itemSqlDate)); + Assert.assertTrue( + lessEqualCompUtilDate.passesFilter(null, itemSqlDate)); + } + + @Test + public void testCompareSqlDatesAndSqlDates() { + Assert.assertFalse( + equalCompSqlDate.passesFilter(null, itemNullSqlDate)); + Assert.assertFalse(equalCompSqlDate.passesFilter(null, itemSqlDate)); + Assert.assertFalse(greaterCompSqlDate.passesFilter(null, itemSqlDate)); + Assert.assertTrue(lessCompSqlDate.passesFilter(null, itemSqlDate)); + Assert.assertFalse( + greaterEqualCompSqlDate.passesFilter(null, itemSqlDate)); + Assert.assertTrue(lessEqualCompSqlDate.passesFilter(null, itemSqlDate)); + } + + @Test + public void testCompareSqlDatesAndUtilDates() { + Assert.assertFalse( + equalCompSqlDate.passesFilter(null, itemNullUtilDate)); + Assert.assertFalse(equalCompSqlDate.passesFilter(null, itemUtilDate)); + Assert.assertFalse(greaterCompSqlDate.passesFilter(null, itemUtilDate)); + Assert.assertTrue(lessCompSqlDate.passesFilter(null, itemUtilDate)); + Assert.assertFalse( + greaterEqualCompSqlDate.passesFilter(null, itemUtilDate)); + Assert.assertTrue( + lessEqualCompSqlDate.passesFilter(null, itemUtilDate)); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterTest.java new file mode 100644 index 0000000000..4bdad3c54d --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/CompareFilterTest.java @@ -0,0 +1,322 @@ +package com.vaadin.data.util.filter; + +import java.math.BigDecimal; +import java.util.Date; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Compare.Greater; +import com.vaadin.data.util.filter.Compare.GreaterOrEqual; +import com.vaadin.data.util.filter.Compare.Less; +import com.vaadin.data.util.filter.Compare.LessOrEqual; + +public class CompareFilterTest extends AbstractFilterTestBase<Compare> { + + protected Item itemNull; + protected Item itemEmpty; + protected Item itemA; + protected Item itemB; + protected Item itemC; + + protected final Filter equalB = new Equal(PROPERTY1, "b"); + protected final Filter greaterB = new Greater(PROPERTY1, "b"); + protected final Filter lessB = new Less(PROPERTY1, "b"); + protected final Filter greaterEqualB = new GreaterOrEqual(PROPERTY1, "b"); + protected final Filter lessEqualB = new LessOrEqual(PROPERTY1, "b"); + + protected final Filter equalNull = new Equal(PROPERTY1, null); + protected final Filter greaterNull = new Greater(PROPERTY1, null); + protected final Filter lessNull = new Less(PROPERTY1, null); + protected final Filter greaterEqualNull = new GreaterOrEqual(PROPERTY1, + null); + protected final Filter lessEqualNull = new LessOrEqual(PROPERTY1, null); + + @Override + protected void setUp() throws Exception { + super.setUp(); + itemNull = new PropertysetItem(); + itemNull.addItemProperty(PROPERTY1, + new ObjectProperty<String>(null, String.class)); + itemEmpty = new PropertysetItem(); + itemEmpty.addItemProperty(PROPERTY1, + new ObjectProperty<String>("", String.class)); + itemA = new PropertysetItem(); + itemA.addItemProperty(PROPERTY1, + new ObjectProperty<String>("a", String.class)); + itemB = new PropertysetItem(); + itemB.addItemProperty(PROPERTY1, + new ObjectProperty<String>("b", String.class)); + itemC = new PropertysetItem(); + itemC.addItemProperty(PROPERTY1, + new ObjectProperty<String>("c", String.class)); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + itemNull = null; + itemEmpty = null; + itemA = null; + itemB = null; + } + + @Test + public void testCompareString() { + Assert.assertFalse(equalB.passesFilter(null, itemEmpty)); + Assert.assertFalse(equalB.passesFilter(null, itemA)); + Assert.assertTrue(equalB.passesFilter(null, itemB)); + Assert.assertFalse(equalB.passesFilter(null, itemC)); + + Assert.assertFalse(greaterB.passesFilter(null, itemEmpty)); + Assert.assertFalse(greaterB.passesFilter(null, itemA)); + Assert.assertFalse(greaterB.passesFilter(null, itemB)); + Assert.assertTrue(greaterB.passesFilter(null, itemC)); + + Assert.assertTrue(lessB.passesFilter(null, itemEmpty)); + Assert.assertTrue(lessB.passesFilter(null, itemA)); + Assert.assertFalse(lessB.passesFilter(null, itemB)); + Assert.assertFalse(lessB.passesFilter(null, itemC)); + + Assert.assertFalse(greaterEqualB.passesFilter(null, itemEmpty)); + Assert.assertFalse(greaterEqualB.passesFilter(null, itemA)); + Assert.assertTrue(greaterEqualB.passesFilter(null, itemB)); + Assert.assertTrue(greaterEqualB.passesFilter(null, itemC)); + + Assert.assertTrue(lessEqualB.passesFilter(null, itemEmpty)); + Assert.assertTrue(lessEqualB.passesFilter(null, itemA)); + Assert.assertTrue(lessEqualB.passesFilter(null, itemB)); + Assert.assertFalse(lessEqualB.passesFilter(null, itemC)); + } + + @Test + public void testCompareWithNull() { + // null comparisons: null is less than any other value + Assert.assertFalse(equalB.passesFilter(null, itemNull)); + Assert.assertTrue(greaterB.passesFilter(null, itemNull)); + Assert.assertFalse(lessB.passesFilter(null, itemNull)); + Assert.assertTrue(greaterEqualB.passesFilter(null, itemNull)); + Assert.assertFalse(lessEqualB.passesFilter(null, itemNull)); + + Assert.assertTrue(equalNull.passesFilter(null, itemNull)); + Assert.assertFalse(greaterNull.passesFilter(null, itemNull)); + Assert.assertFalse(lessNull.passesFilter(null, itemNull)); + Assert.assertTrue(greaterEqualNull.passesFilter(null, itemNull)); + Assert.assertTrue(lessEqualNull.passesFilter(null, itemNull)); + + Assert.assertFalse(equalNull.passesFilter(null, itemA)); + Assert.assertFalse(greaterNull.passesFilter(null, itemA)); + Assert.assertTrue(lessNull.passesFilter(null, itemA)); + Assert.assertFalse(greaterEqualNull.passesFilter(null, itemA)); + Assert.assertTrue(lessEqualNull.passesFilter(null, itemA)); + } + + @Test + public void testCompareInteger() { + int negative = -1; + int zero = 0; + int positive = 1; + + Item itemNegative = new PropertysetItem(); + itemNegative.addItemProperty(PROPERTY1, + new ObjectProperty<Integer>(negative, Integer.class)); + Item itemZero = new PropertysetItem(); + itemZero.addItemProperty(PROPERTY1, + new ObjectProperty<Integer>(zero, Integer.class)); + Item itemPositive = new PropertysetItem(); + itemPositive.addItemProperty(PROPERTY1, + new ObjectProperty<Integer>(positive, Integer.class)); + + Filter equalZero = new Equal(PROPERTY1, zero); + Assert.assertFalse(equalZero.passesFilter(null, itemNegative)); + Assert.assertTrue(equalZero.passesFilter(null, itemZero)); + Assert.assertFalse(equalZero.passesFilter(null, itemPositive)); + + Filter isPositive = new Greater(PROPERTY1, zero); + Assert.assertFalse(isPositive.passesFilter(null, itemNegative)); + Assert.assertFalse(isPositive.passesFilter(null, itemZero)); + Assert.assertTrue(isPositive.passesFilter(null, itemPositive)); + + Filter isNegative = new Less(PROPERTY1, zero); + Assert.assertTrue(isNegative.passesFilter(null, itemNegative)); + Assert.assertFalse(isNegative.passesFilter(null, itemZero)); + Assert.assertFalse(isNegative.passesFilter(null, itemPositive)); + + Filter isNonNegative = new GreaterOrEqual(PROPERTY1, zero); + Assert.assertFalse(isNonNegative.passesFilter(null, itemNegative)); + Assert.assertTrue(isNonNegative.passesFilter(null, itemZero)); + Assert.assertTrue(isNonNegative.passesFilter(null, itemPositive)); + + Filter isNonPositive = new LessOrEqual(PROPERTY1, zero); + Assert.assertTrue(isNonPositive.passesFilter(null, itemNegative)); + Assert.assertTrue(isNonPositive.passesFilter(null, itemZero)); + Assert.assertFalse(isNonPositive.passesFilter(null, itemPositive)); + } + + @Test + public void testCompareBigDecimal() { + BigDecimal negative = new BigDecimal(-1); + BigDecimal zero = new BigDecimal(0); + BigDecimal positive = new BigDecimal(1); + positive.setScale(1); + BigDecimal positiveScaleTwo = new BigDecimal(1).setScale(2); + + Item itemNegative = new PropertysetItem(); + itemNegative.addItemProperty(PROPERTY1, + new ObjectProperty<BigDecimal>(negative, BigDecimal.class)); + Item itemZero = new PropertysetItem(); + itemZero.addItemProperty(PROPERTY1, + new ObjectProperty<BigDecimal>(zero, BigDecimal.class)); + Item itemPositive = new PropertysetItem(); + itemPositive.addItemProperty(PROPERTY1, + new ObjectProperty<BigDecimal>(positive, BigDecimal.class)); + Item itemPositiveScaleTwo = new PropertysetItem(); + itemPositiveScaleTwo.addItemProperty(PROPERTY1, + new ObjectProperty<BigDecimal>(positiveScaleTwo, + BigDecimal.class)); + + Filter equalZero = new Equal(PROPERTY1, zero); + Assert.assertFalse(equalZero.passesFilter(null, itemNegative)); + Assert.assertTrue(equalZero.passesFilter(null, itemZero)); + Assert.assertFalse(equalZero.passesFilter(null, itemPositive)); + + Filter isPositive = new Greater(PROPERTY1, zero); + Assert.assertFalse(isPositive.passesFilter(null, itemNegative)); + Assert.assertFalse(isPositive.passesFilter(null, itemZero)); + Assert.assertTrue(isPositive.passesFilter(null, itemPositive)); + + Filter isNegative = new Less(PROPERTY1, zero); + Assert.assertTrue(isNegative.passesFilter(null, itemNegative)); + Assert.assertFalse(isNegative.passesFilter(null, itemZero)); + Assert.assertFalse(isNegative.passesFilter(null, itemPositive)); + + Filter isNonNegative = new GreaterOrEqual(PROPERTY1, zero); + Assert.assertFalse(isNonNegative.passesFilter(null, itemNegative)); + Assert.assertTrue(isNonNegative.passesFilter(null, itemZero)); + Assert.assertTrue(isNonNegative.passesFilter(null, itemPositive)); + + Filter isNonPositive = new LessOrEqual(PROPERTY1, zero); + Assert.assertTrue(isNonPositive.passesFilter(null, itemNegative)); + Assert.assertTrue(isNonPositive.passesFilter(null, itemZero)); + Assert.assertFalse(isNonPositive.passesFilter(null, itemPositive)); + + Filter isPositiveScaleTwo = new Equal(PROPERTY1, positiveScaleTwo); + Assert.assertTrue( + isPositiveScaleTwo.passesFilter(null, itemPositiveScaleTwo)); + Assert.assertTrue(isPositiveScaleTwo.passesFilter(null, itemPositive)); + + } + + @Test + public void testCompareDate() { + Date now = new Date(); + // new Date() is only accurate to the millisecond, so repeating it gives + // the same date + Date earlier = new Date(now.getTime() - 1); + Date later = new Date(now.getTime() + 1); + + Item itemEarlier = new PropertysetItem(); + itemEarlier.addItemProperty(PROPERTY1, + new ObjectProperty<Date>(earlier, Date.class)); + Item itemNow = new PropertysetItem(); + itemNow.addItemProperty(PROPERTY1, + new ObjectProperty<Date>(now, Date.class)); + Item itemLater = new PropertysetItem(); + itemLater.addItemProperty(PROPERTY1, + new ObjectProperty<Date>(later, Date.class)); + + Filter equalNow = new Equal(PROPERTY1, now); + Assert.assertFalse(equalNow.passesFilter(null, itemEarlier)); + Assert.assertTrue(equalNow.passesFilter(null, itemNow)); + Assert.assertFalse(equalNow.passesFilter(null, itemLater)); + + Filter after = new Greater(PROPERTY1, now); + Assert.assertFalse(after.passesFilter(null, itemEarlier)); + Assert.assertFalse(after.passesFilter(null, itemNow)); + Assert.assertTrue(after.passesFilter(null, itemLater)); + + Filter before = new Less(PROPERTY1, now); + Assert.assertTrue(before.passesFilter(null, itemEarlier)); + Assert.assertFalse(before.passesFilter(null, itemNow)); + Assert.assertFalse(before.passesFilter(null, itemLater)); + + Filter afterOrNow = new GreaterOrEqual(PROPERTY1, now); + Assert.assertFalse(afterOrNow.passesFilter(null, itemEarlier)); + Assert.assertTrue(afterOrNow.passesFilter(null, itemNow)); + Assert.assertTrue(afterOrNow.passesFilter(null, itemLater)); + + Filter beforeOrNow = new LessOrEqual(PROPERTY1, now); + Assert.assertTrue(beforeOrNow.passesFilter(null, itemEarlier)); + Assert.assertTrue(beforeOrNow.passesFilter(null, itemNow)); + Assert.assertFalse(beforeOrNow.passesFilter(null, itemLater)); + } + + @Test + public void testCompareAppliesToProperty() { + Filter filterA = new Equal("a", 1); + Filter filterB = new Equal("b", 1); + + Assert.assertTrue(filterA.appliesToProperty("a")); + Assert.assertFalse(filterA.appliesToProperty("b")); + Assert.assertFalse(filterB.appliesToProperty("a")); + Assert.assertTrue(filterB.appliesToProperty("b")); + } + + @Test + public void testCompareEqualsHashCode() { + // most checks with Equal filter, then only some with others + Filter equalNull2 = new Equal(PROPERTY1, null); + Filter equalNullProperty2 = new Equal(PROPERTY2, null); + Filter equalEmpty = new Equal(PROPERTY1, ""); + Filter equalEmpty2 = new Equal(PROPERTY1, ""); + Filter equalEmptyProperty2 = new Equal(PROPERTY2, ""); + Filter equalA = new Equal(PROPERTY1, "a"); + Filter equalB2 = new Equal(PROPERTY1, "b"); + Filter equalBProperty2 = new Equal(PROPERTY2, "b"); + + Filter greaterEmpty = new Greater(PROPERTY1, ""); + + // equals() + Assert.assertEquals(equalNull, equalNull); + Assert.assertEquals(equalNull, equalNull2); + Assert.assertFalse(equalNull.equals(equalNullProperty2)); + Assert.assertFalse(equalNull.equals(equalEmpty)); + Assert.assertFalse(equalNull.equals(equalB)); + + Assert.assertEquals(equalEmpty, equalEmpty); + Assert.assertFalse(equalEmpty.equals(equalNull)); + Assert.assertEquals(equalEmpty, equalEmpty2); + Assert.assertFalse(equalEmpty.equals(equalEmptyProperty2)); + Assert.assertFalse(equalEmpty.equals(equalB)); + + Assert.assertEquals(equalB, equalB); + Assert.assertFalse(equalB.equals(equalNull)); + Assert.assertFalse(equalB.equals(equalEmpty)); + Assert.assertEquals(equalB, equalB2); + Assert.assertFalse(equalB.equals(equalBProperty2)); + Assert.assertFalse(equalB.equals(equalA)); + + Assert.assertEquals(greaterB, greaterB); + Assert.assertFalse(greaterB.equals(lessB)); + Assert.assertFalse(greaterB.equals(greaterEqualB)); + Assert.assertFalse(greaterB.equals(lessEqualB)); + + Assert.assertFalse(greaterNull.equals(greaterEmpty)); + Assert.assertFalse(greaterNull.equals(greaterB)); + Assert.assertFalse(greaterEmpty.equals(greaterNull)); + Assert.assertFalse(greaterEmpty.equals(greaterB)); + Assert.assertFalse(greaterB.equals(greaterNull)); + Assert.assertFalse(greaterB.equals(greaterEmpty)); + + // hashCode() + Assert.assertEquals(equalNull.hashCode(), equalNull2.hashCode()); + Assert.assertEquals(equalEmpty.hashCode(), equalEmpty2.hashCode()); + Assert.assertEquals(equalB.hashCode(), equalB2.hashCode()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/IsNullFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/IsNullFilterTest.java new file mode 100644 index 0000000000..18013cd41c --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/IsNullFilterTest.java @@ -0,0 +1,61 @@ +package com.vaadin.data.util.filter; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; + +public class IsNullFilterTest extends AbstractFilterTestBase<IsNull> { + + @Test + public void testIsNull() { + Item item1 = new PropertysetItem(); + item1.addItemProperty("a", + new ObjectProperty<String>(null, String.class)); + item1.addItemProperty("b", + new ObjectProperty<String>("b", String.class)); + Item item2 = new PropertysetItem(); + item2.addItemProperty("a", + new ObjectProperty<String>("a", String.class)); + item2.addItemProperty("b", + new ObjectProperty<String>(null, String.class)); + + Filter filter1 = new IsNull("a"); + Filter filter2 = new IsNull("b"); + + Assert.assertTrue(filter1.passesFilter(null, item1)); + Assert.assertFalse(filter1.passesFilter(null, item2)); + Assert.assertFalse(filter2.passesFilter(null, item1)); + Assert.assertTrue(filter2.passesFilter(null, item2)); + } + + @Test + public void testIsNullAppliesToProperty() { + Filter filterA = new IsNull("a"); + Filter filterB = new IsNull("b"); + + Assert.assertTrue(filterA.appliesToProperty("a")); + Assert.assertFalse(filterA.appliesToProperty("b")); + Assert.assertFalse(filterB.appliesToProperty("a")); + Assert.assertTrue(filterB.appliesToProperty("b")); + } + + @Test + public void testIsNullEqualsHashCode() { + Filter filter1 = new IsNull("a"); + Filter filter1b = new IsNull("a"); + Filter filter2 = new IsNull("b"); + + // equals() + Assert.assertEquals(filter1, filter1b); + Assert.assertFalse(filter1.equals(filter2)); + Assert.assertFalse(filter1.equals(new And())); + + // hashCode() + Assert.assertEquals(filter1.hashCode(), filter1b.hashCode()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/LikeFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/LikeFilterTest.java new file mode 100644 index 0000000000..39054008cd --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/LikeFilterTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2016 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.data.util.filter; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; + +public class LikeFilterTest extends AbstractFilterTestBase<Like> { + + protected Item item1 = new PropertysetItem(); + protected Item item2 = new PropertysetItem(); + protected Item item3 = new PropertysetItem(); + + @Test + public void testLikeWithNulls() { + + Like filter = new Like("value", "a"); + + item1.addItemProperty("value", new ObjectProperty<String>("a")); + item2.addItemProperty("value", new ObjectProperty<String>("b")); + item3.addItemProperty("value", + new ObjectProperty<String>(null, String.class)); + + Assert.assertTrue(filter.passesFilter(null, item1)); + Assert.assertFalse(filter.passesFilter(null, item2)); + Assert.assertFalse(filter.passesFilter(null, item3)); + + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/NotFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/NotFilterTest.java new file mode 100644 index 0000000000..e797b484f8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/NotFilterTest.java @@ -0,0 +1,54 @@ +package com.vaadin.data.util.filter; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.BeanItem; + +public class NotFilterTest extends AbstractFilterTestBase<Not> { + + protected Item item1 = new BeanItem<Integer>(1); + protected Item item2 = new BeanItem<Integer>(2); + + @Test + public void testNot() { + Filter origFilter = new SameItemFilter(item1); + Filter filter = new Not(origFilter); + + Assert.assertTrue(origFilter.passesFilter(null, item1)); + Assert.assertFalse(origFilter.passesFilter(null, item2)); + Assert.assertFalse(filter.passesFilter(null, item1)); + Assert.assertTrue(filter.passesFilter(null, item2)); + } + + @Test + public void testANotAppliesToProperty() { + Filter filterA = new Not(new SameItemFilter(item1, "a")); + Filter filterB = new Not(new SameItemFilter(item1, "b")); + + Assert.assertTrue(filterA.appliesToProperty("a")); + Assert.assertFalse(filterA.appliesToProperty("b")); + Assert.assertFalse(filterB.appliesToProperty("a")); + Assert.assertTrue(filterB.appliesToProperty("b")); + } + + @Test + public void testNotEqualsHashCode() { + Filter origFilter = new SameItemFilter(item1); + Filter filter1 = new Not(origFilter); + Filter filter1b = new Not(new SameItemFilter(item1)); + Filter filter2 = new Not(new SameItemFilter(item2)); + + // equals() + Assert.assertEquals(filter1, filter1b); + Assert.assertFalse(filter1.equals(filter2)); + Assert.assertFalse(filter1.equals(origFilter)); + Assert.assertFalse(filter1.equals(new And())); + + // hashCode() + Assert.assertEquals(filter1.hashCode(), filter1b.hashCode()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/filter/SimpleStringFilterTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/filter/SimpleStringFilterTest.java new file mode 100644 index 0000000000..dd8267107a --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/filter/SimpleStringFilterTest.java @@ -0,0 +1,139 @@ +package com.vaadin.data.util.filter; + +import org.junit.Assert; +import org.junit.Test; + +public class SimpleStringFilterTest + extends AbstractFilterTestBase<SimpleStringFilter> { + + protected static TestItem<String, String> createTestItem() { + return new TestItem<String, String>("abcde", "TeSt"); + } + + protected TestItem<String, String> getTestItem() { + return createTestItem(); + } + + protected SimpleStringFilter f(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + return new SimpleStringFilter(propertyId, filterString, ignoreCase, + onlyMatchPrefix); + } + + protected boolean passes(Object propertyId, String filterString, + boolean ignoreCase, boolean onlyMatchPrefix) { + return f(propertyId, filterString, ignoreCase, onlyMatchPrefix) + .passesFilter(null, getTestItem()); + } + + @Test + public void testStartsWithCaseSensitive() { + Assert.assertTrue(passes(PROPERTY1, "ab", false, true)); + Assert.assertTrue(passes(PROPERTY1, "", false, true)); + + Assert.assertFalse(passes(PROPERTY2, "ab", false, true)); + Assert.assertFalse(passes(PROPERTY1, "AB", false, true)); + } + + @Test + public void testStartsWithCaseInsensitive() { + Assert.assertTrue(passes(PROPERTY1, "AB", true, true)); + Assert.assertTrue(passes(PROPERTY2, "te", true, true)); + Assert.assertFalse(passes(PROPERTY2, "AB", true, true)); + } + + @Test + public void testContainsCaseSensitive() { + Assert.assertTrue(passes(PROPERTY1, "ab", false, false)); + Assert.assertTrue(passes(PROPERTY1, "abcde", false, false)); + Assert.assertTrue(passes(PROPERTY1, "cd", false, false)); + Assert.assertTrue(passes(PROPERTY1, "e", false, false)); + Assert.assertTrue(passes(PROPERTY1, "", false, false)); + + Assert.assertFalse(passes(PROPERTY2, "ab", false, false)); + Assert.assertFalse(passes(PROPERTY1, "es", false, false)); + } + + @Test + public void testContainsCaseInsensitive() { + Assert.assertTrue(passes(PROPERTY1, "AB", true, false)); + Assert.assertTrue(passes(PROPERTY1, "aBcDe", true, false)); + Assert.assertTrue(passes(PROPERTY1, "CD", true, false)); + Assert.assertTrue(passes(PROPERTY1, "", true, false)); + + Assert.assertTrue(passes(PROPERTY2, "es", true, false)); + + Assert.assertFalse(passes(PROPERTY2, "ab", true, false)); + } + + @Test + public void testAppliesToProperty() { + SimpleStringFilter filter = f(PROPERTY1, "ab", false, true); + Assert.assertTrue(filter.appliesToProperty(PROPERTY1)); + Assert.assertFalse(filter.appliesToProperty(PROPERTY2)); + Assert.assertFalse(filter.appliesToProperty("other")); + } + + @Test + public void testEqualsHashCode() { + SimpleStringFilter filter = f(PROPERTY1, "ab", false, true); + + SimpleStringFilter f1 = f(PROPERTY2, "ab", false, true); + SimpleStringFilter f1b = f(PROPERTY2, "ab", false, true); + SimpleStringFilter f2 = f(PROPERTY1, "cd", false, true); + SimpleStringFilter f2b = f(PROPERTY1, "cd", false, true); + SimpleStringFilter f3 = f(PROPERTY1, "ab", true, true); + SimpleStringFilter f3b = f(PROPERTY1, "ab", true, true); + SimpleStringFilter f4 = f(PROPERTY1, "ab", false, false); + SimpleStringFilter f4b = f(PROPERTY1, "ab", false, false); + + // equal but not same instance + Assert.assertEquals(f1, f1b); + Assert.assertEquals(f2, f2b); + Assert.assertEquals(f3, f3b); + Assert.assertEquals(f4, f4b); + + // more than one property differ + Assert.assertFalse(f1.equals(f2)); + Assert.assertFalse(f1.equals(f3)); + Assert.assertFalse(f1.equals(f4)); + Assert.assertFalse(f2.equals(f1)); + Assert.assertFalse(f2.equals(f3)); + Assert.assertFalse(f2.equals(f4)); + Assert.assertFalse(f3.equals(f1)); + Assert.assertFalse(f3.equals(f2)); + Assert.assertFalse(f3.equals(f4)); + Assert.assertFalse(f4.equals(f1)); + Assert.assertFalse(f4.equals(f2)); + Assert.assertFalse(f4.equals(f3)); + + // only one property differs + Assert.assertFalse(filter.equals(f1)); + Assert.assertFalse(filter.equals(f2)); + Assert.assertFalse(filter.equals(f3)); + Assert.assertFalse(filter.equals(f4)); + + Assert.assertFalse(f1.equals(null)); + Assert.assertFalse(f1.equals(new Object())); + + Assert.assertEquals(f1.hashCode(), f1b.hashCode()); + Assert.assertEquals(f2.hashCode(), f2b.hashCode()); + Assert.assertEquals(f3.hashCode(), f3b.hashCode()); + Assert.assertEquals(f4.hashCode(), f4b.hashCode()); + } + + @Test + public void testNonExistentProperty() { + Assert.assertFalse(passes("other1", "ab", false, true)); + } + + @Test + public void testNullValueForProperty() { + TestItem<String, String> item = createTestItem(); + item.addItemProperty("other1", new NullProperty()); + + Assert.assertFalse( + f("other1", "ab", false, true).passesFilter(null, item)); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractBeanContainerListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractBeanContainerListenersTest.java new file mode 100644 index 0000000000..98a2513515 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractBeanContainerListenersTest.java @@ -0,0 +1,16 @@ +package com.vaadin.tests.server; + +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; + +public class AbstractBeanContainerListenersTest + extends AbstractListenerMethodsTestBase { + public void testPropertySetChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(BeanItemContainer.class, + PropertySetChangeEvent.class, PropertySetChangeListener.class, + new BeanItemContainer<PropertySetChangeListener>( + PropertySetChangeListener.class)); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractContainerListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractContainerListenersTest.java new file mode 100644 index 0000000000..5e0d464fe8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractContainerListenersTest.java @@ -0,0 +1,26 @@ +package com.vaadin.tests.server; + +import org.junit.Test; + +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; + +public class AbstractContainerListenersTest + extends AbstractListenerMethodsTestBase { + + @Test + public void testItemSetChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(IndexedContainer.class, + ItemSetChangeEvent.class, ItemSetChangeListener.class); + } + + @Test + public void testPropertySetChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(IndexedContainer.class, + PropertySetChangeEvent.class, PropertySetChangeListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractInMemoryContainerListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractInMemoryContainerListenersTest.java new file mode 100644 index 0000000000..d5a7131182 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/AbstractInMemoryContainerListenersTest.java @@ -0,0 +1,14 @@ +package com.vaadin.tests.server; + +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; + +public class AbstractInMemoryContainerListenersTest + extends AbstractListenerMethodsTestBase { + public void testItemSetChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(IndexedContainer.class, + ItemSetChangeEvent.class, ItemSetChangeListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/IndexedContainerListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/IndexedContainerListenersTest.java new file mode 100644 index 0000000000..d8a1290b68 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/IndexedContainerListenersTest.java @@ -0,0 +1,26 @@ +package com.vaadin.tests.server; + +import org.junit.Test; + +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; + +public class IndexedContainerListenersTest + extends AbstractListenerMethodsTestBase { + + @Test + public void testValueChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(IndexedContainer.class, ValueChangeEvent.class, + ValueChangeListener.class); + } + + @Test + public void testPropertySetChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(IndexedContainer.class, + PropertySetChangeEvent.class, PropertySetChangeListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/SerializationTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/SerializationTest.java new file mode 100644 index 0000000000..30b0729ace --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/SerializationTest.java @@ -0,0 +1,135 @@ +package com.vaadin.tests.server; + +import static org.junit.Assert.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.MethodProperty; +import com.vaadin.server.VaadinSession; +import com.vaadin.v7.data.validator.LegacyRegexpValidator; + +public class SerializationTest { + + @Test + public void testValidators() throws Exception { + LegacyRegexpValidator validator = new LegacyRegexpValidator(".*", + "Error"); + validator.validate("aaa"); + LegacyRegexpValidator validator2 = serializeAndDeserialize(validator); + validator2.validate("aaa"); + } + + @Test + public void testIndedexContainerItemIds() throws Exception { + IndexedContainer ic = new IndexedContainer(); + ic.addContainerProperty("prop1", String.class, null); + Object id = ic.addItem(); + ic.getItem(id).getItemProperty("prop1").setValue("1"); + + Item item2 = ic.addItem("item2"); + item2.getItemProperty("prop1").setValue("2"); + + serializeAndDeserialize(ic); + } + + @Test + public void testMethodPropertyGetter() throws Exception { + MethodProperty<?> mp = new MethodProperty<Object>(new Data(), + "dummyGetter"); + serializeAndDeserialize(mp); + } + + @Test + public void testMethodPropertyGetterAndSetter() throws Exception { + MethodProperty<?> mp = new MethodProperty<Object>(new Data(), + "dummyGetterAndSetter"); + serializeAndDeserialize(mp); + } + + @Test + public void testMethodPropertyInt() throws Exception { + MethodProperty<?> mp = new MethodProperty<Object>(new Data(), + "dummyInt"); + serializeAndDeserialize(mp); + } + + @Test + public void testVaadinSession() throws Exception { + VaadinSession session = new VaadinSession(null); + + session = serializeAndDeserialize(session); + + assertNotNull( + "Pending access queue was not recreated after deserialization", + session.getPendingAccessQueue()); + } + + private static <S extends Serializable> S serializeAndDeserialize(S s) + throws IOException, ClassNotFoundException { + // Serialize and deserialize + + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bs); + out.writeObject(s); + byte[] data = bs.toByteArray(); + ObjectInputStream in = new ObjectInputStream( + new ByteArrayInputStream(data)); + @SuppressWarnings("unchecked") + S s2 = (S) in.readObject(); + + // using special toString(Object) method to avoid calling + // Property.toString(), which will be temporarily disabled + // TODO This is hilariously broken (#12723) + if (s.equals(s2)) { + System.out.println(toString(s) + " equals " + toString(s2)); + } else { + System.out.println(toString(s) + " does NOT equal " + toString(s2)); + } + + return s2; + } + + private static String toString(Object o) { + if (o instanceof Property) { + return String.valueOf(((Property<?>) o).getValue()); + } else { + return String.valueOf(o); + } + } + + public static class Data implements Serializable { + private String dummyGetter; + private String dummyGetterAndSetter; + private int dummyInt; + + public String getDummyGetterAndSetter() { + return dummyGetterAndSetter; + } + + public void setDummyGetterAndSetter(String dummyGetterAndSetter) { + this.dummyGetterAndSetter = dummyGetterAndSetter; + } + + public int getDummyInt() { + return dummyInt; + } + + public void setDummyInt(int dummyInt) { + this.dummyInt = dummyInt; + } + + public String getDummyGetter() { + return dummyGetter; + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarBasicsTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarBasicsTest.java new file mode 100644 index 0000000000..7d011afc8b --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarBasicsTest.java @@ -0,0 +1,290 @@ +/* + * Copyright 2000-2016 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.tests.server.component.calendar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.ui.Calendar; +import com.vaadin.ui.Calendar.TimeFormat; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClick; +import com.vaadin.ui.components.calendar.event.BasicEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; + +/** + * Basic API tests for the calendar + */ +public class CalendarBasicsTest { + + @Test + public void testEmptyConstructorInitialization() { + + Calendar calendar = new Calendar(); + + // The calendar should have a basic event provider with no events + CalendarEventProvider provider = calendar.getEventProvider(); + assertNotNull("Event provider should not be null", provider); + + // Basic event handlers should be registered + assertNotNull(calendar.getHandler(BackwardEvent.EVENT_ID)); + assertNotNull(calendar.getHandler(ForwardEvent.EVENT_ID)); + assertNotNull(calendar.getHandler(WeekClick.EVENT_ID)); + assertNotNull(calendar.getHandler(DateClickEvent.EVENT_ID)); + assertNotNull(calendar.getHandler(MoveEvent.EVENT_ID)); + assertNotNull(calendar.getHandler(EventResize.EVENT_ID)); + + // Calendar should have undefined size + assertTrue(calendar.getWidth() < 0); + assertTrue(calendar.getHeight() < 0); + } + + @Test + public void testConstructorWithCaption() { + final String caption = "My Calendar Caption"; + Calendar calendar = new Calendar(caption); + assertEquals(caption, calendar.getCaption()); + } + + @Test + public void testConstructorWithCustomEventProvider() { + BasicEventProvider myProvider = new BasicEventProvider(); + Calendar calendar = new Calendar(myProvider); + assertEquals(myProvider, calendar.getEventProvider()); + } + + @Test + public void testConstructorWithCustomEventProviderAndCaption() { + BasicEventProvider myProvider = new BasicEventProvider(); + final String caption = "My Calendar Caption"; + Calendar calendar = new Calendar(caption, myProvider); + assertEquals(caption, calendar.getCaption()); + assertEquals(myProvider, calendar.getEventProvider()); + } + + @Test + public void testDefaultStartAndEndDates() { + Calendar calendar = new Calendar(); + + // If no start and end date is set the calendar will display the current + // week + java.util.Calendar c = new GregorianCalendar(); + java.util.Calendar c2 = new GregorianCalendar(); + + c2.setTime(calendar.getStartDate()); + assertEquals(c.getFirstDayOfWeek(), + c2.get(java.util.Calendar.DAY_OF_WEEK)); + c2.setTime(calendar.getEndDate()); + + c.set(java.util.Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek() + 6); + assertEquals(c.get(java.util.Calendar.DAY_OF_WEEK), + c2.get(java.util.Calendar.DAY_OF_WEEK)); + } + + @Test + public void testCustomStartAndEndDates() { + Calendar calendar = new Calendar(); + java.util.Calendar c = new GregorianCalendar(); + + Date start = c.getTime(); + c.add(java.util.Calendar.DATE, 3); + Date end = c.getTime(); + + calendar.setStartDate(start); + calendar.setEndDate(end); + + assertEquals(start.getTime(), calendar.getStartDate().getTime()); + assertEquals(end.getTime(), calendar.getEndDate().getTime()); + } + + @Test + public void testCustomLocale() { + Calendar calendar = new Calendar(); + calendar.setLocale(Locale.CANADA_FRENCH); + + // Setting the locale should set the internal calendars locale + assertEquals(Locale.CANADA_FRENCH, calendar.getLocale()); + java.util.Calendar c = new GregorianCalendar(Locale.CANADA_FRENCH); + assertEquals(c.getTimeZone().getRawOffset(), + calendar.getInternalCalendar().getTimeZone().getRawOffset()); + } + + @Test + public void testTimeFormat() { + Calendar calendar = new Calendar(); + + // The default timeformat depends on the current locale + calendar.setLocale(Locale.ENGLISH); + assertEquals(TimeFormat.Format12H, calendar.getTimeFormat()); + + calendar.setLocale(Locale.ITALIAN); + assertEquals(TimeFormat.Format24H, calendar.getTimeFormat()); + + // Setting a specific time format overrides the locale + calendar.setTimeFormat(TimeFormat.Format12H); + assertEquals(TimeFormat.Format12H, calendar.getTimeFormat()); + } + + @Test + public void testTimeZone() { + Calendar calendar = new Calendar(); + calendar.setLocale(Locale.CANADA_FRENCH); + + // By default the calendars timezone is returned + assertEquals(calendar.getInternalCalendar().getTimeZone(), + calendar.getTimeZone()); + + // One can override the default behaviour by specifying a timezone + TimeZone customTimeZone = TimeZone.getTimeZone("Europe/Helsinki"); + calendar.setTimeZone(customTimeZone); + assertEquals(customTimeZone, calendar.getTimeZone()); + } + + @Test + public void testVisibleDaysOfWeek() { + Calendar calendar = new Calendar(); + + // The defaults are the whole week + assertEquals(1, calendar.getFirstVisibleDayOfWeek()); + assertEquals(7, calendar.getLastVisibleDayOfWeek()); + + calendar.setFirstVisibleDayOfWeek(0); // Invalid input + assertEquals(1, calendar.getFirstVisibleDayOfWeek()); + + calendar.setLastVisibleDayOfWeek(0); // Invalid input + assertEquals(7, calendar.getLastVisibleDayOfWeek()); + + calendar.setFirstVisibleDayOfWeek(8); // Invalid input + assertEquals(1, calendar.getFirstVisibleDayOfWeek()); + + calendar.setLastVisibleDayOfWeek(8); // Invalid input + assertEquals(7, calendar.getLastVisibleDayOfWeek()); + + calendar.setFirstVisibleDayOfWeek(4); + assertEquals(4, calendar.getFirstVisibleDayOfWeek()); + + calendar.setLastVisibleDayOfWeek(6); + assertEquals(6, calendar.getLastVisibleDayOfWeek()); + + calendar.setFirstVisibleDayOfWeek(7); // Invalid since last day is 6 + assertEquals(4, calendar.getFirstVisibleDayOfWeek()); + + calendar.setLastVisibleDayOfWeek(2); // Invalid since first day is 4 + assertEquals(6, calendar.getLastVisibleDayOfWeek()); + } + + @Test + public void testVisibleHoursInDay() { + Calendar calendar = new Calendar(); + + // Defaults are the whole day + assertEquals(0, calendar.getFirstVisibleHourOfDay()); + assertEquals(23, calendar.getLastVisibleHourOfDay()); + } + + @Test + public void isClientChangeAllowed_connectorEnabled() { + TestCalendar calendar = new TestCalendar(true); + Assert.assertTrue( + "Calendar with enabled connector doesn't allow client change", + calendar.isClientChangeAllowed()); + } + + // regression test to ensure old functionality is not broken + @Test + public void defaultFirstDayOfWeek() { + Calendar calendar = new Calendar(); + calendar.setLocale(Locale.GERMAN); + // simulating consequences of markAsDirty + calendar.beforeClientResponse(true); + assertEquals(java.util.Calendar.MONDAY, + calendar.getInternalCalendar().getFirstDayOfWeek()); + } + + @Test + public void customFirstDayOfWeek() { + Calendar calendar = new Calendar(); + calendar.setLocale(Locale.GERMAN); + calendar.setFirstDayOfWeek(java.util.Calendar.SUNDAY); + + // simulating consequences of markAsDirty + calendar.beforeClientResponse(true); + assertEquals(java.util.Calendar.SUNDAY, + calendar.getInternalCalendar().getFirstDayOfWeek()); + } + + @Test + public void customFirstDayOfWeekCanSetEvenBeforeLocale() { + Calendar calendar = new Calendar(); + calendar.setFirstDayOfWeek(java.util.Calendar.SUNDAY); + + calendar.setLocale(Locale.GERMAN); + // simulating consequences of markAsDirty + calendar.beforeClientResponse(true); + assertEquals(java.util.Calendar.SUNDAY, + calendar.getInternalCalendar().getFirstDayOfWeek()); + } + + @Test + public void customFirstDayOfWeekSetNullRestoresDefault() { + Calendar calendar = new Calendar(); + calendar.setLocale(Locale.GERMAN); + calendar.setFirstDayOfWeek(java.util.Calendar.SUNDAY); + calendar.setFirstDayOfWeek(null); + // simulating consequences of markAsDirty + calendar.beforeClientResponse(true); + assertEquals(java.util.Calendar.MONDAY, + calendar.getInternalCalendar().getFirstDayOfWeek()); + } + + @Test(expected = IllegalArgumentException.class) + public void customFirstDayOfWeekValidation() { + Calendar calendar = new Calendar(); + int someWrongDayOfWeek = 10; + calendar.setFirstDayOfWeek(someWrongDayOfWeek); + } + + private static class TestCalendar extends Calendar { + TestCalendar(boolean connectorEnabled) { + isConnectorEnabled = connectorEnabled; + } + + @Override + public boolean isConnectorEnabled() { + return isConnectorEnabled; + } + + @Override + public boolean isClientChangeAllowed() { + return super.isClientChangeAllowed(); + } + + private final boolean isConnectorEnabled; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarDeclarativeTest.java new file mode 100644 index 0000000000..f6896ff15d --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/CalendarDeclarativeTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2016 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.tests.server.component.calendar; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import org.junit.Test; + +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.Calendar; +import com.vaadin.ui.Calendar.TimeFormat; + +public class CalendarDeclarativeTest extends DeclarativeTestBase<Calendar> { + + @Test + public void testEmpty() { + verifyDeclarativeDesign("<vaadin-calendar></vaadin-calendar>", + new Calendar()); + } + + @Test + public void testCalendarAllFeatures() throws ParseException { + String design = "<vaadin-calendar start-date='2014-11-17' end-date='2014-11-23' " + + "first-visible-day-of-week=2 last-visible-day-of-week=5 " + + "time-zone='EST' time-format='12h' first-visible-hour-of-day=8 " + + "last-visible-hour-of-day=18 weekly-caption-format='mmm MM/dd' />"; + + DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + Calendar calendar = new Calendar(); + calendar.setStartDate(format.parse("2014-11-17")); + calendar.setEndDate(format.parse("2014-11-23")); + calendar.setFirstVisibleDayOfWeek(2); + calendar.setLastVisibleDayOfWeek(5); + calendar.setTimeZone(TimeZone.getTimeZone("EST")); + calendar.setTimeFormat(TimeFormat.Format12H); + calendar.setFirstVisibleHourOfDay(8); + calendar.setLastVisibleHourOfDay(18); + calendar.setWeeklyCaptionFormat("mmm MM/dd"); + verifyDeclarativeDesign(design, calendar); + } + + protected void verifyDeclarativeDesign(String design, Calendar expected) { + testRead(design, expected); + testWrite(design, expected); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerDataSourceTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerDataSourceTest.java new file mode 100644 index 0000000000..9cc78269f8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerDataSourceTest.java @@ -0,0 +1,396 @@ +/* + * Copyright 2000-2016 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.tests.server.component.calendar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Date; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Calendar; +import com.vaadin.ui.components.calendar.ContainerEventProvider; +import com.vaadin.ui.components.calendar.event.BasicEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent; + +public class ContainerDataSourceTest { + + private Calendar calendar; + + @Before + public void setUp() { + calendar = new Calendar(); + } + + /** + * Tests adding a bean item container to the Calendar + */ + @Test + public void testWithBeanItemContainer() { + + // Create a container to use as a datasource + Indexed container = createTestBeanItemContainer(); + + // Set datasource + calendar.setContainerDataSource(container); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime(((CalendarEvent) container.getIdByIndex(0)).getStart()); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Test the all events are returned + List<CalendarEvent> events = calendar.getEventProvider() + .getEvents(start, end); + assertEquals(container.size(), events.size()); + + // Test that a certain range is returned + cal.setTime(((CalendarEvent) container.getIdByIndex(6)).getStart()); + end = cal.getTime(); + events = calendar.getEventProvider().getEvents(start, end); + assertEquals(6, events.size()); + } + + /** + * This tests tests that if you give the Calendar an unsorted (== not sorted + * by starting date) container then the calendar should gracefully handle + * it. In this case the size of the container will be wrong. The test is + * exactly the same as {@link #testWithBeanItemContainer()} except that the + * beans has been intentionally sorted by caption instead of date. + */ + @Test + public void testWithUnsortedBeanItemContainer() { + // Create a container to use as a datasource + Indexed container = createTestBeanItemContainer(); + + // Make the container sorted by caption + ((Sortable) container).sort(new Object[] { "caption" }, + new boolean[] { true }); + + // Set data source + calendar.setContainerDataSource(container); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime(((CalendarEvent) container.getIdByIndex(0)).getStart()); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Test the all events are returned + List<CalendarEvent> events = calendar.getEventProvider() + .getEvents(start, end); + assertEquals(container.size(), events.size()); + + // Test that a certain range is returned + cal.setTime(((CalendarEvent) container.getIdByIndex(6)).getStart()); + end = cal.getTime(); + events = calendar.getEventProvider().getEvents(start, end); + + // The events size is 1 since the getEvents returns the wrong range + assertEquals(1, events.size()); + } + + /** + * Tests adding a Indexed container to the Calendar + */ + @Test + public void testWithIndexedContainer() { + + // Create a container to use as a datasource + Indexed container = createTestIndexedContainer(); + + // Set datasource + calendar.setContainerDataSource(container, "testCaption", + "testDescription", "testStartDate", "testEndDate", null); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime((Date) container.getItem(container.getIdByIndex(0)) + .getItemProperty("testStartDate").getValue()); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Test the all events are returned + List<CalendarEvent> events = calendar.getEventProvider() + .getEvents(start, end); + assertEquals(container.size(), events.size()); + + // Check that event values are present + CalendarEvent e = events.get(0); + assertEquals("Test 1", e.getCaption()); + assertEquals("Description 1", e.getDescription()); + assertTrue(e.getStart().compareTo(start) == 0); + + // Test that a certain range is returned + cal.setTime((Date) container.getItem(container.getIdByIndex(6)) + .getItemProperty("testStartDate").getValue()); + end = cal.getTime(); + events = calendar.getEventProvider().getEvents(start, end); + assertEquals(6, events.size()); + } + + @Test + public void testNullLimitsBeanItemContainer() { + // Create a container to use as a datasource + Indexed container = createTestBeanItemContainer(); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime(((CalendarEvent) container.getIdByIndex(0)).getStart()); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Set datasource + calendar.setContainerDataSource(container); + + // Test null start time + List<CalendarEvent> events = calendar.getEventProvider().getEvents(null, + end); + assertEquals(container.size(), events.size()); + + // Test null end time + events = calendar.getEventProvider().getEvents(start, null); + assertEquals(container.size(), events.size()); + + // Test both null times + events = calendar.getEventProvider().getEvents(null, null); + assertEquals(container.size(), events.size()); + } + + @Test + public void testNullLimitsIndexedContainer() { + // Create a container to use as a datasource + Indexed container = createTestIndexedContainer(); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime((Date) container.getItem(container.getIdByIndex(0)) + .getItemProperty("testStartDate").getValue()); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Set datasource + calendar.setContainerDataSource(container, "testCaption", + "testDescription", "testStartDate", "testEndDate", null); + + // Test null start time + List<CalendarEvent> events = calendar.getEventProvider().getEvents(null, + end); + assertEquals(container.size(), events.size()); + + // Test null end time + events = calendar.getEventProvider().getEvents(start, null); + assertEquals(container.size(), events.size()); + + // Test both null times + events = calendar.getEventProvider().getEvents(null, null); + assertEquals(container.size(), events.size()); + } + + /** + * Tests the addEvent convenience method with the default event provider + */ + @Test + public void testAddEventConvinienceMethod() { + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + + // Add an event + BasicEvent event = new BasicEvent("Test", "Test", start); + calendar.addEvent(event); + + // Ensure event exists + List<CalendarEvent> events = calendar.getEvents(start, end); + assertEquals(1, events.size()); + assertEquals(events.get(0).getCaption(), event.getCaption()); + assertEquals(events.get(0).getDescription(), event.getDescription()); + assertEquals(events.get(0).getStart(), event.getStart()); + } + + /** + * Test the removeEvent convenience method with the default event provider + */ + @Test + public void testRemoveEventConvinienceMethod() { + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + + // Add an event + CalendarEvent event = new BasicEvent("Test", "Test", start); + calendar.addEvent(event); + + // Ensure event exists + assertEquals(1, calendar.getEvents(start, end).size()); + + // Remove event + calendar.removeEvent(event); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + } + + @Test + public void testAddEventConvinienceMethodWithCustomEventProvider() { + + // Use a container data source + calendar.setEventProvider(new ContainerEventProvider( + new BeanItemContainer<BasicEvent>(BasicEvent.class))); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + + // Add an event + BasicEvent event = new BasicEvent("Test", "Test", start); + calendar.addEvent(event); + + // Ensure event exists + List<CalendarEvent> events = calendar.getEvents(start, end); + assertEquals(1, events.size()); + assertEquals(events.get(0).getCaption(), event.getCaption()); + assertEquals(events.get(0).getDescription(), event.getDescription()); + assertEquals(events.get(0).getStart(), event.getStart()); + } + + @Test + public void testRemoveEventConvinienceMethodWithCustomEventProvider() { + + // Use a container data source + calendar.setEventProvider(new ContainerEventProvider( + new BeanItemContainer<BasicEvent>(BasicEvent.class))); + + // Start and end dates to query for + java.util.Calendar cal = java.util.Calendar.getInstance(); + Date start = cal.getTime(); + cal.add(java.util.Calendar.MONTH, 1); + Date end = cal.getTime(); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + + // Add an event + BasicEvent event = new BasicEvent("Test", "Test", start); + calendar.addEvent(event); + + // Ensure event exists + List<CalendarEvent> events = calendar.getEvents(start, end); + assertEquals(1, events.size()); + + // Remove event + calendar.removeEvent(event); + + // Ensure no events + assertEquals(0, calendar.getEvents(start, end).size()); + } + + @Test + public void testStyleNamePropertyRetrieved() { + IndexedContainer ic = (IndexedContainer) createTestIndexedContainer(); + ic.addContainerProperty("testStyleName", String.class, ""); + for (int i = 0; i < 10; i++) { + Item item = ic.getItem(ic.getIdByIndex(i)); + @SuppressWarnings("unchecked") + Property<String> itemProperty = item + .getItemProperty("testStyleName"); + itemProperty.setValue("testStyle"); + } + + ContainerEventProvider provider = new ContainerEventProvider(ic); + provider.setCaptionProperty("testCaption"); + provider.setDescriptionProperty("testDescription"); + provider.setStartDateProperty("testStartDate"); + provider.setEndDateProperty("testEndDate"); + provider.setStyleNameProperty("testStyleName"); + + calendar.setEventProvider(provider); + java.util.Calendar cal = java.util.Calendar.getInstance(); + Date now = cal.getTime(); + cal.add(java.util.Calendar.DAY_OF_MONTH, 20); + Date then = cal.getTime(); + List<CalendarEvent> events = calendar.getEventProvider().getEvents(now, + then); + for (CalendarEvent ce : events) { + assertEquals("testStyle", ce.getStyleName()); + } + } + + private static Indexed createTestBeanItemContainer() { + BeanItemContainer<CalendarEvent> eventContainer = new BeanItemContainer<CalendarEvent>( + CalendarEvent.class); + java.util.Calendar cal = java.util.Calendar.getInstance(); + for (int i = 1; i <= 10; i++) { + eventContainer.addBean(new BasicEvent("Test " + i, + "Description " + i, cal.getTime())); + cal.add(java.util.Calendar.DAY_OF_MONTH, 2); + } + return eventContainer; + } + + private static Indexed createTestIndexedContainer() { + IndexedContainer container = new IndexedContainer(); + container.addContainerProperty("testCaption", String.class, ""); + container.addContainerProperty("testDescription", String.class, ""); + container.addContainerProperty("testStartDate", Date.class, null); + container.addContainerProperty("testEndDate", Date.class, null); + + java.util.Calendar cal = java.util.Calendar.getInstance(); + for (int i = 1; i <= 10; i++) { + Item item = container.getItem(container.addItem()); + item.getItemProperty("testCaption").setValue("Test " + i); + item.getItemProperty("testDescription") + .setValue("Description " + i); + item.getItemProperty("testStartDate").setValue(cal.getTime()); + item.getItemProperty("testEndDate").setValue(cal.getTime()); + cal.add(java.util.Calendar.DAY_OF_MONTH, 2); + } + return container; + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerEventProviderTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerEventProviderTest.java new file mode 100644 index 0000000000..401b5861ce --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/calendar/ContainerEventProviderTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2016 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.tests.server.component.calendar; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.ui.components.calendar.ContainerEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEvent; + +/** + * + * @author Vaadin Ltd + */ +public class ContainerEventProviderTest { + + @Test + public void testDefaultAllDayProperty() { + ContainerEventProvider provider = new ContainerEventProvider(null); + Assert.assertEquals(ContainerEventProvider.ALL_DAY_PROPERTY, + provider.getAllDayProperty()); + + } + + @Test + public void testSetAllDayProperty() { + ContainerEventProvider provider = new ContainerEventProvider(null); + Object prop = new Object(); + provider.setAllDayProperty(prop); + Assert.assertEquals(prop, provider.getAllDayProperty()); + } + + @Test + public void testGetEvents() { + BeanItemContainer<EventBean> container = new BeanItemContainer<EventBean>( + EventBean.class); + EventBean bean = new EventBean(); + container.addBean(bean); + ContainerEventProvider provider = new ContainerEventProvider(container); + List<CalendarEvent> events = provider.getEvents(bean.getStart(), + bean.getEnd()); + Assert.assertTrue(events.get(0).isAllDay()); + } + + public static class EventBean { + + public boolean isAllDay() { + return true; + } + + public void setAllDay(boolean allDay) { + } + + public Date getStart() { + return Calendar.getInstance().getTime(); + } + + public Date getEnd() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 10); + return calendar.getTime(); + } + + public void setStart(Date date) { + } + + public void setEnd(Date date) { + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/AbstractColorPickerDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/AbstractColorPickerDeclarativeTest.java new file mode 100644 index 0000000000..2e3b8e412e --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/AbstractColorPickerDeclarativeTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2000-2016 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.tests.server.component.colorpicker; + +import org.junit.Test; + +import com.vaadin.shared.ui.colorpicker.Color; +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.AbstractColorPicker; +import com.vaadin.ui.AbstractColorPicker.PopupStyle; +import com.vaadin.ui.ColorPicker; +import com.vaadin.ui.ColorPickerArea; + +public class AbstractColorPickerDeclarativeTest + extends DeclarativeTestBase<AbstractColorPicker> { + + @Test + public void testAllAbstractColorPickerFeatures() { + String design = "<vaadin-color-picker color='#fafafa' default-caption-enabled position='100,100'" + + " popup-style='simple' rgb-visibility='false' hsv-visibility='false'" + + " history-visibility=false textfield-visibility=false />"; + ColorPicker colorPicker = new ColorPicker(); + int colorInt = Integer.parseInt("fafafa", 16); + colorPicker.setColor(new Color(colorInt)); + colorPicker.setDefaultCaptionEnabled(true); + colorPicker.setPosition(100, 100); + colorPicker.setPopupStyle(PopupStyle.POPUP_SIMPLE); + colorPicker.setRGBVisibility(false); + colorPicker.setHSVVisibility(false); + colorPicker.setSwatchesVisibility(true); + colorPicker.setHistoryVisibility(false); + colorPicker.setTextfieldVisibility(false); + + testWrite(design, colorPicker); + testRead(design, colorPicker); + } + + @Test + public void testEmptyColorPicker() { + String design = "<vaadin-color-picker />"; + ColorPicker colorPicker = new ColorPicker(); + testRead(design, colorPicker); + testWrite(design, colorPicker); + } + + @Test + public void testAllAbstractColorPickerAreaFeatures() { + String design = "<vaadin-color-picker-area color='#fafafa' default-caption-enabled position='100,100'" + + " popup-style='simple' rgb-visibility='false' hsv-visibility='false'" + + " history-visibility=false textfield-visibility=false />"; + AbstractColorPicker colorPicker = new ColorPickerArea(); + int colorInt = Integer.parseInt("fafafa", 16); + colorPicker.setColor(new Color(colorInt)); + colorPicker.setDefaultCaptionEnabled(true); + colorPicker.setPosition(100, 100); + colorPicker.setPopupStyle(PopupStyle.POPUP_SIMPLE); + colorPicker.setRGBVisibility(false); + colorPicker.setHSVVisibility(false); + colorPicker.setSwatchesVisibility(true); + colorPicker.setHistoryVisibility(false); + colorPicker.setTextfieldVisibility(false); + + testWrite(design, colorPicker); + testRead(design, colorPicker); + } + + @Test + public void testEmptyColorPickerArea() { + String design = "<vaadin-color-picker-area />"; + AbstractColorPicker colorPicker = new ColorPickerArea(); + testRead(design, colorPicker); + testWrite(design, colorPicker); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/ColorConversionsTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/ColorConversionsTest.java new file mode 100644 index 0000000000..a55ed89691 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/colorpicker/ColorConversionsTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2016 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.tests.server.component.colorpicker; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import com.vaadin.shared.ui.colorpicker.Color; + +public class ColorConversionsTest { + + @Test + public void convertHSL2RGB() { + + int rgb = Color.HSLtoRGB(100, 50, 50); + Color c = new Color(rgb); + assertEquals(106, c.getRed()); + assertEquals(191, c.getGreen()); + assertEquals(64, c.getBlue()); + assertEquals("#6abf40", c.getCSS()); + + rgb = Color.HSLtoRGB(0, 50, 50); + c = new Color(rgb); + assertEquals(191, c.getRed()); + assertEquals(64, c.getGreen()); + assertEquals(64, c.getBlue()); + assertEquals("#bf4040", c.getCSS()); + + rgb = Color.HSLtoRGB(50, 0, 50); + c = new Color(rgb); + assertEquals(128, c.getRed()); + assertEquals(128, c.getGreen()); + assertEquals(128, c.getBlue()); + assertEquals("#808080", c.getCSS()); + + rgb = Color.HSLtoRGB(50, 100, 0); + c = new Color(rgb); + assertEquals(0, c.getRed(), 0); + assertEquals(0, c.getGreen(), 0); + assertEquals(0, c.getBlue(), 0); + assertEquals("#000000", c.getCSS()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxDeclarativeTest.java new file mode 100644 index 0000000000..482267f63d --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxDeclarativeTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2016 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.tests.server.component.combobox; + +import org.junit.Test; + +import com.vaadin.shared.ui.combobox.FilteringMode; +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.ComboBox; + +public class ComboBoxDeclarativeTest extends DeclarativeTestBase<ComboBox> { + + @Test + public void testReadOnlyWithOptionsRead() { + testRead(getReadOnlyWithOptionsDesign(), + getReadOnlyWithOptionsExpected()); + } + + private ComboBox getReadOnlyWithOptionsExpected() { + ComboBox cb = new ComboBox(); + cb.setTextInputAllowed(false); + cb.addItem("Hello"); + cb.addItem("World"); + return cb; + } + + private String getReadOnlyWithOptionsDesign() { + return "<vaadin-combo-box text-input-allowed='false'><option>Hello</option><option>World</option></vaadin-combo-box>"; + } + + @Test + public void testReadOnlyWithOptionsWrite() { + testWrite(stripOptionTags(getReadOnlyWithOptionsDesign()), + getReadOnlyWithOptionsExpected()); + } + + @Test + public void testBasicRead() { + testRead(getBasicDesign(), getBasicExpected()); + } + + @Test + public void testBasicWrite() { + testWrite(getBasicDesign(), getBasicExpected()); + } + + @Test + public void testReadOnlyValue() { + String design = "<vaadin-combo-box readonly value='foo'><option selected>foo</option></vaadin-combo-box>"; + + ComboBox comboBox = new ComboBox(); + comboBox.addItems("foo", "bar"); + comboBox.setValue("foo"); + comboBox.setReadOnly(true); + + testRead(design, comboBox); + + // Selects items are not written out by default + String design2 = "<vaadin-combo-box readonly></vaadin-combo-box>"; + testWrite(design2, comboBox); + } + + private String getBasicDesign() { + return "<vaadin-combo-box input-prompt=\"Select something\" filtering-mode=\"off\" scroll-to-selected-item='false'>"; + } + + private ComboBox getBasicExpected() { + ComboBox cb = new ComboBox(); + cb.setInputPrompt("Select something"); + cb.setTextInputAllowed(true); + cb.setFilteringMode(FilteringMode.OFF); + cb.setScrollToSelectedItem(false); + return cb; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxStateTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxStateTest.java new file mode 100644 index 0000000000..e8ca64895a --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/combobox/ComboBoxStateTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2016 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.tests.server.component.combobox; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.shared.ui.combobox.ComboBoxState; +import com.vaadin.ui.ComboBox; + +/** + * Tests for ComboBox state. + * + */ +public class ComboBoxStateTest { + @Test + public void getState_comboboxHasCustomState() { + TestComboBox combobox = new TestComboBox(); + ComboBoxState state = combobox.getState(); + Assert.assertEquals("Unexpected state class", ComboBoxState.class, + state.getClass()); + } + + @Test + public void getPrimaryStyleName_comboboxHasCustomPrimaryStyleName() { + ComboBox combobox = new ComboBox(); + ComboBoxState state = new ComboBoxState(); + Assert.assertEquals("Unexpected primary style name", + state.primaryStyleName, combobox.getPrimaryStyleName()); + } + + @Test + public void comboboxStateHasCustomPrimaryStyleName() { + ComboBoxState state = new ComboBoxState(); + Assert.assertEquals("Unexpected primary style name", "v-filterselect", + state.primaryStyleName); + } + + private static class TestComboBox extends ComboBox { + + @Override + public ComboBoxState getState() { + return super.getState(); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/ListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/ListenersTest.java new file mode 100644 index 0000000000..ade24a0df6 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/ListenersTest.java @@ -0,0 +1,143 @@ +package com.vaadin.tests.server.component.tree; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.ui.Tree; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.Tree.ExpandListener; + +public class ListenersTest implements ExpandListener, CollapseListener { + private int expandCalled; + private int collapseCalled; + private Object lastExpanded; + private Object lastCollapsed; + + @Before + public void setUp() { + expandCalled = 0; + } + + @Test + public void testExpandListener() { + Tree tree = createTree(10, 20, false); + tree.addListener((ExpandListener) this); + List<Object> rootIds = new ArrayList<Object>(tree.rootItemIds()); + + assertEquals(10, rootIds.size()); + assertEquals(10 + 10 * 20 + 10, tree.size()); + + // Expanding should send one expand event for the root item id + tree.expandItem(rootIds.get(0)); + assertEquals(1, expandCalled); + assertEquals(rootIds.get(0), lastExpanded); + + // Expand should send one event for each expanded item id. + // In this case root + child 4 + expandCalled = 0; + tree.expandItemsRecursively(rootIds.get(1)); + assertEquals(2, expandCalled); + List<Object> c = new ArrayList<Object>( + tree.getChildren(rootIds.get(1))); + + assertEquals(c.get(4), lastExpanded); + + // Expanding an already expanded item should send no expand event + expandCalled = 0; + tree.expandItem(rootIds.get(0)); + assertEquals(0, expandCalled); + } + + /** + * Creates a tree with "rootItems" roots, each with "children" children, + * each with 1 child. + * + * @param rootItems + * @param children + * @param expand + * @return + */ + private Tree createTree(int rootItems, int children, boolean expand) { + Tree tree = new Tree(); + for (int i = 0; i < rootItems; i++) { + String rootId = "root " + i; + tree.addItem(rootId); + if (expand) { + tree.expandItemsRecursively(rootId); + } else { + tree.collapseItemsRecursively(rootId); + + } + for (int j = 0; j < children; j++) { + String childId = "child " + i + "/" + j; + tree.addItem(childId); + tree.setParent(childId, rootId); + tree.setChildrenAllowed(childId, false); + if (j == 4) { + tree.setChildrenAllowed(childId, true); + Object grandChildId = tree.addItem(); + tree.setParent(grandChildId, childId); + tree.setChildrenAllowed(grandChildId, false); + if (expand) { + tree.expandItemsRecursively(childId); + } else { + tree.collapseItemsRecursively(childId); + } + } + } + } + + return tree; + } + + @Test + public void testCollapseListener() { + Tree tree = createTree(7, 15, true); + tree.addListener((CollapseListener) this); + + List<Object> rootIds = new ArrayList<Object>(tree.rootItemIds()); + + assertEquals(7, rootIds.size()); + assertEquals(7 + 7 * 15 + 7, tree.size()); + + // Expanding should send one expand event for the root item id + tree.collapseItem(rootIds.get(0)); + assertEquals(1, collapseCalled); + assertEquals(rootIds.get(0), lastCollapsed); + + // Collapse sends one event for each collapsed node. + // In this case root + child 4 + collapseCalled = 0; + tree.collapseItemsRecursively(rootIds.get(1)); + assertEquals(2, collapseCalled); + List<Object> c = new ArrayList<Object>( + tree.getChildren(rootIds.get(1))); + assertEquals(c.get(4), lastCollapsed); + + // Collapsing an already expanded item should send no expand event + collapseCalled = 0; + tree.collapseItem(rootIds.get(0)); + assertEquals(0, collapseCalled); + } + + @Override + public void nodeExpand(ExpandEvent event) { + lastExpanded = event.getItemId(); + expandCalled++; + + } + + @Override + public void nodeCollapse(CollapseEvent event) { + lastCollapsed = event.getItemId(); + collapseCalled++; + + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java new file mode 100644 index 0000000000..1e73f953b2 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeDeclarativeTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2016 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.tests.server.component.tree; + +import org.junit.Test; + +import com.vaadin.server.ExternalResource; +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.Tree; +import com.vaadin.ui.Tree.TreeDragMode; + +/** + * Tests the declarative support for implementations of {@link Tree}. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class TreeDeclarativeTest extends DeclarativeTestBase<Tree> { + + @Test + public void testDragMode() { + String design = "<vaadin-tree drag-mode='node' />"; + + Tree tree = new Tree(); + tree.setDragMode(TreeDragMode.NODE); + + testRead(design, tree); + testWrite(design, tree); + } + + @Test + public void testEmpty() { + testRead("<vaadin-tree />", new Tree()); + testWrite("<vaadin-tree />", new Tree()); + } + + @Test + public void testNodes() { + String design = "<vaadin-tree>" // + + " <node text='Node'/>" // + + " <node text='Parent'>" // + + " <node text='Child'>" // + + " <node text='Grandchild'/>" // + + " </node>" // + + " </node>" // + + " <node text='With icon' icon='http://example.com/icon.png'/>" // + + "</vaadin-tree>"; + + Tree tree = new Tree(); + + tree.addItem("Node"); + + tree.addItem("Parent"); + + tree.addItem("Child"); + tree.setParent("Child", "Parent"); + + tree.addItem("Grandchild"); + tree.setParent("Grandchild", "Child"); + + tree.addItem("With icon"); + tree.setItemIcon("With icon", + new ExternalResource("http://example.com/icon.png")); + + testRead(design, tree); + testWrite(design, tree, true); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeListenersTest.java new file mode 100644 index 0000000000..ec10c4fe39 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeListenersTest.java @@ -0,0 +1,33 @@ +package com.vaadin.tests.server.component.tree; + +import org.junit.Test; + +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; +import com.vaadin.ui.Tree; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.Tree.ExpandListener; + +public class TreeListenersTest extends AbstractListenerMethodsTestBase { + + @Test + public void testExpandListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Tree.class, ExpandEvent.class, + ExpandListener.class); + } + + @Test + public void testItemClickListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Tree.class, ItemClickEvent.class, + ItemClickListener.class); + } + + @Test + public void testCollapseListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Tree.class, CollapseEvent.class, + CollapseListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeTest.java new file mode 100644 index 0000000000..ed455415ea --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/tree/TreeTest.java @@ -0,0 +1,178 @@ +package com.vaadin.tests.server.component.tree; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.HashSet; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.shared.ui.tree.TreeState; +import com.vaadin.ui.Tree; + +public class TreeTest { + + private Tree tree; + private Tree tree2; + private Tree tree3; + private Tree tree4; + + @Before + public void setUp() { + tree = new Tree(); + tree.addItem("parent"); + tree.addItem("child"); + tree.setChildrenAllowed("parent", true); + tree.setParent("child", "parent"); + + tree2 = new Tree("Caption"); + tree2.addItem("parent"); + tree2.addItem("child"); + tree2.setChildrenAllowed("parent", true); + tree2.setParent("child", "parent"); + + tree3 = new Tree("Caption", null); + tree3.addItem("parent"); + tree3.addItem("child"); + tree3.setChildrenAllowed("parent", true); + tree3.setParent("child", "parent"); + + tree4 = new Tree("Caption", new IndexedContainer()); + tree4.addItem("parent"); + tree4.addItem("child"); + tree4.setChildrenAllowed("parent", true); + tree4.setParent("child", "parent"); + } + + @Test + public void testRemoveChildren() { + assertTrue(tree.hasChildren("parent")); + tree.removeItem("child"); + assertFalse(tree.hasChildren("parent")); + + assertTrue(tree2.hasChildren("parent")); + tree2.removeItem("child"); + assertFalse(tree2.hasChildren("parent")); + + assertTrue(tree3.hasChildren("parent")); + tree3.removeItem("child"); + assertFalse(tree3.hasChildren("parent")); + + assertTrue(tree4.hasChildren("parent")); + tree4.removeItem("child"); + assertFalse(tree4.hasChildren("parent")); + } + + @Test + public void testContainerTypeIsHierarchical() { + assertTrue(HierarchicalContainer.class + .isAssignableFrom(tree.getContainerDataSource().getClass())); + assertTrue(HierarchicalContainer.class + .isAssignableFrom(tree2.getContainerDataSource().getClass())); + assertTrue(HierarchicalContainer.class + .isAssignableFrom(tree3.getContainerDataSource().getClass())); + assertFalse(HierarchicalContainer.class + .isAssignableFrom(tree4.getContainerDataSource().getClass())); + assertTrue(Container.Hierarchical.class + .isAssignableFrom(tree4.getContainerDataSource().getClass())); + } + + @Test + public void testRemoveExpandedItems() throws Exception { + tree.expandItem("parent"); + tree.expandItem("child"); + + Field expandedField = tree.getClass().getDeclaredField("expanded"); + Field expandedItemIdField = tree.getClass() + .getDeclaredField("expandedItemId"); + + expandedField.setAccessible(true); + expandedItemIdField.setAccessible(true); + + HashSet<Object> expanded = (HashSet<Object>) expandedField.get(tree); + Object expandedItemId = expandedItemIdField.get(tree); + + assertEquals(2, expanded.size()); + assertTrue("Contains parent", expanded.contains("parent")); + assertTrue("Contains child", expanded.contains("child")); + assertEquals("child", expandedItemId); + + tree.removeItem("parent"); + + expanded = (HashSet<Object>) expandedField.get(tree); + expandedItemId = expandedItemIdField.get(tree); + + assertEquals(1, expanded.size()); + assertTrue("Contains child", expanded.contains("child")); + assertEquals("child", expandedItemId); + + tree.removeItem("child"); + + expanded = (HashSet<Object>) expandedField.get(tree); + expandedItemId = expandedItemIdField.get(tree); + + assertEquals(0, expanded.size()); + assertNull(expandedItemId); + } + + @Test + public void testRemoveExpandedItemsOnContainerChange() throws Exception { + tree.expandItem("parent"); + tree.expandItem("child"); + + tree.setContainerDataSource(new HierarchicalContainer()); + + Field expandedField = tree.getClass().getDeclaredField("expanded"); + Field expandedItemIdField = tree.getClass() + .getDeclaredField("expandedItemId"); + + expandedField.setAccessible(true); + expandedItemIdField.setAccessible(true); + + HashSet<Object> expanded = (HashSet<Object>) expandedField.get(tree); + assertEquals(0, expanded.size()); + + Object expandedItemId = expandedItemIdField.get(tree); + assertNull(expandedItemId); + } + + @Test + public void getState_treeHasCustomState() { + TestTree table = new TestTree(); + TreeState state = table.getState(); + Assert.assertEquals("Unexpected state class", TreeState.class, + state.getClass()); + } + + @Test + public void getPrimaryStyleName_treeHasCustomPrimaryStyleName() { + Tree table = new Tree(); + TreeState state = new TreeState(); + Assert.assertEquals("Unexpected primary style name", + state.primaryStyleName, table.getPrimaryStyleName()); + } + + @Test + public void treeStateHasCustomPrimaryStyleName() { + TreeState state = new TreeState(); + Assert.assertEquals("Unexpected primary style name", "v-tree", + state.primaryStyleName); + } + + private static class TestTree extends Tree { + + @Override + public TreeState getState() { + return super.getState(); + } + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java new file mode 100644 index 0000000000..02a2ebfd77 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2016 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.tests.server.component.twincolselect; + +import java.util.Arrays; + +import org.junit.Test; + +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.TwinColSelect; + +/** + * Test cases for reading the properties of selection components. + * + * @author Vaadin Ltd + */ +public class TwinColSelectDeclarativeTest + extends DeclarativeTestBase<TwinColSelect> { + + public String getBasicDesign() { + return "<vaadin-twin-col-select rows=5 right-column-caption='Selected values' left-column-caption='Unselected values'>\n" + + " <option>First item</option>\n" + + " <option selected>Second item</option>\n" + + " <option selected>Third item</option>\n" + + "</vaadin-twin-col-select>"; + + } + + public TwinColSelect getBasicExpected() { + TwinColSelect s = new TwinColSelect(); + s.setRightColumnCaption("Selected values"); + s.setLeftColumnCaption("Unselected values"); + s.addItem("First item"); + s.addItem("Second item"); + s.addItem("Third item"); + s.setValue(Arrays.asList(new Object[] { "Second item", "Third item" })); + s.setRows(5); + return s; + } + + @Test + public void testReadBasic() { + testRead(getBasicDesign(), getBasicExpected()); + } + + @Test + public void testWriteBasic() { + testWrite(stripOptionTags(getBasicDesign()), getBasicExpected()); + } + + @Test + public void testReadEmpty() { + testRead("<vaadin-twin-col-select />", new TwinColSelect()); + } + + @Test + public void testWriteEmpty() { + testWrite("<vaadin-twin-col-select />", new TwinColSelect()); + } + +}
\ No newline at end of file diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectStateTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectStateTest.java new file mode 100644 index 0000000000..9348de63c2 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectStateTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2016 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.tests.server.component.twincolselect; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.shared.ui.twincolselect.TwinColSelectState; +import com.vaadin.ui.TwinColSelect; + +/** + * Tests for TwinColSelectState. + * + */ +public class TwinColSelectStateTest { + + @Test + public void getState_selectHasCustomState() { + TestTwinColSelect select = new TestTwinColSelect(); + TwinColSelectState state = select.getState(); + Assert.assertEquals("Unexpected state class", TwinColSelectState.class, + state.getClass()); + } + + @Test + public void getPrimaryStyleName_selectHasCustomPrimaryStyleName() { + TwinColSelect table = new TwinColSelect(); + TwinColSelectState state = new TwinColSelectState(); + Assert.assertEquals("Unexpected primary style name", + state.primaryStyleName, table.getPrimaryStyleName()); + } + + @Test + public void selectStateHasCustomPrimaryStyleName() { + TwinColSelectState state = new TwinColSelectState(); + Assert.assertEquals("Unexpected primary style name", "v-select-twincol", + state.primaryStyleName); + } + + private static class TestTwinColSelect extends TwinColSelect { + + @Override + public TwinColSelectState getState() { + return super.getState(); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/components/ComboBoxValueChangeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/components/ComboBoxValueChangeTest.java new file mode 100644 index 0000000000..66a4ec2370 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/components/ComboBoxValueChangeTest.java @@ -0,0 +1,46 @@ +package com.vaadin.tests.server.components; + +import org.junit.Before; + +import com.vaadin.server.ServerRpcManager; +import com.vaadin.server.ServerRpcMethodInvocation; +import com.vaadin.shared.ui.combobox.ComboBoxServerRpc; +import com.vaadin.ui.ComboBox; +import com.vaadin.v7.ui.LegacyAbstractField; + +/** + * Check that the value change listener for a combo box is triggered exactly + * once when setting the value, at the correct time. + * + * See <a href="http://dev.vaadin.com/ticket/4394">Ticket 4394</a>. + */ +public class ComboBoxValueChangeTest + extends AbstractFieldValueChangeTestBase<Object> { + + @Before + public void setUp() { + ComboBox combo = new ComboBox() { + @Override + public String getConnectorId() { + return "id"; + } + }; + combo.addItem("myvalue"); + super.setUp(combo); + } + + @Override + protected void setValue(LegacyAbstractField<Object> field) { + ComboBox combo = (ComboBox) field; + ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( + combo.getConnectorId(), ComboBoxServerRpc.class, + "setSelectedItem", 1); + invocation.setParameters(new Object[] { "myvalue" }); + try { + ServerRpcManager.applyInvocation(combo, invocation); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} |