diff options
Diffstat (limited to 'server')
51 files changed, 2558 insertions, 457 deletions
diff --git a/server/src/com/vaadin/annotations/Widgetset.java b/server/src/com/vaadin/annotations/Widgetset.java index 40276c18a2..006bf59acf 100644 --- a/server/src/com/vaadin/annotations/Widgetset.java +++ b/server/src/com/vaadin/annotations/Widgetset.java @@ -24,7 +24,7 @@ import java.lang.annotation.Target; import com.vaadin.ui.UI; /** - * Defines a specific theme for a {@link UI}. + * Defines a specific widgetset for a {@link UI}. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index e93db52a35..bf553f31d2 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -582,6 +582,60 @@ public interface Container extends Serializable { public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException; + /** + * An <code>Event</code> object specifying information about the added + * items. + */ + public interface ItemAddEvent extends ItemSetChangeEvent { + + /** + * Gets the item id of the first added item. + * + * @return item id of the first added item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first added item. + * + * @return index of the first added item + */ + public int getFirstIndex(); + + /** + * Gets the number of the added items. + * + * @return the number of added items. + */ + public int getAddedItemsCount(); + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + */ + public interface ItemRemoveEvent extends ItemSetChangeEvent { + /** + * Gets the item id of the first removed item. + * + * @return item id of the first removed item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first removed item. + * + * @return index of the first removed item + */ + public int getFirstIndex(); + + /** + * Gets the number of the removed items. + * + * @return the number of removed items + */ + public int getRemovedItemsCount(); + } } /** diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index 35403d6419..67239996a2 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -222,6 +222,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends @Override public boolean removeAllItems() { int origSize = size(); + IDTYPE firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -234,7 +235,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends // fire event only if the visible view changed, regardless of whether // filtered out items were removed or not if (origSize != 0) { - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -679,6 +680,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends 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 @@ -699,13 +702,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends if (modified) { // Filter the contents when all items have been added if (isFiltered()) { - filterAll(); - } else { - fireItemSetChange(); + 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. * @@ -845,8 +857,32 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends * @return true if the property was added */ public boolean addNestedContainerProperty(String propertyId) { + return addNestedContainerProperty(propertyId, false); + } + + /** + * Adds a nested container property for the container, e.g. + * "manager.address.street". + * + * All intermediate getters must exist and must return non-null values when + * the property value is accessed or the <code>nullBeansAllowed</code> must + * be set to true. If the <code>nullBeansAllowed</code> flag is set to true, + * calling getValue of the added property will return null if the property + * or any of its intermediate getters returns null. If set to false, null + * values returned by intermediate getters will cause NullPointerException. + * The default value is false to ensure backwards compatibility. + * + * @see NestedMethodProperty + * + * @param propertyId + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @return true if the property was added + */ + public boolean addNestedContainerProperty(String propertyId, + boolean nullBeansAllowed) { return addContainerProperty(propertyId, new NestedPropertyDescriptor( - propertyId, type)); + propertyId, type, nullBeansAllowed)); } /** @@ -864,13 +900,42 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends */ @SuppressWarnings("unchecked") public void addNestedContainerBean(String propertyId) { + addNestedContainerBean(propertyId, false); + } + + /** + * 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. + * + * Unless + * <code>nullBeansAllowed<code> is set to true, all intermediate getters must + * exist and must return non-null values when the property values are + * accessed. If the <code>nullBeansAllowed</code> flag is set to true, + * calling getValue of the added subproperties will return null if the + * property or any of their intermediate getters returns null. If set to + * false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @see NestedMethodProperty + * @see #addNestedContainerProperty(String) + * + * @param propertyId + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + @SuppressWarnings("unchecked") + public void addNestedContainerBean(String propertyId, + boolean nullBeansAllowed) { 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); + qualifiedPropertyId, (Class<BEANTYPE>) type, + nullBeansAllowed); model.put(qualifiedPropertyId, pd); model.remove(propertyId); for (BeanItem<BEANTYPE> item : itemIdToItem.values()) { diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java index 84304431bc..9a7922b928 100644 --- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -15,8 +15,10 @@ */ 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; @@ -146,6 +148,85 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } } + 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> + */ + 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> + */ + 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. * @@ -898,36 +979,69 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE * Notify item set change listeners that an item has been added to the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. - * * @param postion - * position of the added item in the view (if visible) + * 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) { - fireItemSetChange(); + 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. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. + * @param position + * position of the removed item in the view prior to removal * - * @param postion - * 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) { - fireItemSetChange(); + 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 @@ -946,6 +1060,21 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } /** + * Returns the item id of the first visible item after filtering. 'Null' is + * returned if there is no visible items. + * + * For internal use only. + * + * @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 diff --git a/server/src/com/vaadin/data/util/BeanItem.java b/server/src/com/vaadin/data/util/BeanItem.java index fc51be8f36..4834fe4f89 100644 --- a/server/src/com/vaadin/data/util/BeanItem.java +++ b/server/src/com/vaadin/data/util/BeanItem.java @@ -268,6 +268,27 @@ public class BeanItem<BT> extends PropertysetItem { } /** + * Adds a nested property to the item. If the <code>nullBeansAllowed</code> + * flag is set to true, calling getValue of the added property will return + * null if the property or any of its intermediate getters returns null. If + * set to false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @param nestedPropertyId + * property id to add. This property must not exist in the item + * already and must of of form "field1.field2" where field2 is a + * field in the object referenced to by field1 + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + public void addNestedProperty(String nestedPropertyId, + boolean nullBeansAllowed) { + addItemProperty(nestedPropertyId, new NestedMethodProperty<Object>( + getBean(), nestedPropertyId, nullBeansAllowed)); + } + + /** * Gets the underlying JavaBean object. * * @return the bean object. diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java index d7bf70caf6..5d20919208 100644 --- a/server/src/com/vaadin/data/util/IndexedContainer.java +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -226,6 +226,7 @@ public class IndexedContainer extends @Override public boolean removeAllItems() { int origSize = size(); + Object firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -235,7 +236,7 @@ public class IndexedContainer extends // filtered out items were removed or not if (origSize != 0) { // Sends a change event - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -620,8 +621,7 @@ public class IndexedContainer extends @Override protected void fireItemAdded(int position, Object itemId, Item item) { if (position >= 0) { - fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, - position)); + super.fireItemAdded(position, itemId, item); } } @@ -1211,4 +1211,5 @@ public class IndexedContainer extends public Collection<Filter> getContainerFilters() { return super.getContainerFilters(); } + } diff --git a/server/src/com/vaadin/data/util/NestedMethodProperty.java b/server/src/com/vaadin/data/util/NestedMethodProperty.java index b62ecfbfc3..7a3963c17e 100644 --- a/server/src/com/vaadin/data/util/NestedMethodProperty.java +++ b/server/src/com/vaadin/data/util/NestedMethodProperty.java @@ -32,7 +32,7 @@ import com.vaadin.data.util.MethodProperty.MethodException; * can contain multiple levels of nesting. * * When accessing the property value, all intermediate getters must return - * non-null values. + * non-null values or the <code>nullBeansAllowed</code> must be set to true. * * @see MethodProperty * @@ -55,6 +55,15 @@ public class NestedMethodProperty<T> extends AbstractProperty<T> { */ private Object instance; + /** + * a boolean flag indicating whether intermediate getters may return null + * values. If the flag is set to true, calling getValue will return null if + * the property or any of its intermediate getters returns null. If set to + * false, intermediate getters returning null value will throw Exception. + * The default value is false to ensure backwards compatibility. + */ + private boolean nullBeansAllowed = false; + private Class<? extends T> type; /* Special serialization to handle method references */ @@ -85,7 +94,33 @@ public class NestedMethodProperty<T> extends AbstractProperty<T> { * if the property name is invalid */ public NestedMethodProperty(Object instance, String propertyName) { + this(instance, propertyName, false); + } + + /** + * Constructs a nested method property for a given object instance. The + * property name is a dot separated string pointing to a nested property, + * e.g. "manager.address.street". The <code>nullBeansAllowed</code> controls + * the behavior in cases where the intermediate getters may return null + * values. If the flag is set to true, calling getValue will return null if + * the property or any of its intermediate getters returns null. If set to + * false, null values returned by intermediate getters will cause + * NullPointerException. The default value is false to ensure backwards + * compatibility. + * + * @param instance + * top-level bean to which the property applies + * @param propertyName + * dot separated nested property name + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedMethodProperty(Object instance, String propertyName, + boolean nullBeansAllowed) { this.instance = instance; + this.nullBeansAllowed = nullBeansAllowed; initialize(instance.getClass(), propertyName); } @@ -104,6 +139,25 @@ public class NestedMethodProperty<T> extends AbstractProperty<T> { } /** + * For internal use to deduce property type etc. without a bean instance. + * Calling {@link #setValue(Object)} or {@link #getValue()} on properties + * constructed this way is not supported. + * + * @param instanceClass + * class of the top-level bean + * @param propertyName + * dot separated nested property name + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + */ + NestedMethodProperty(Class<?> instanceClass, String propertyName, + boolean nullBeansAllowed) { + instance = null; + this.nullBeansAllowed = nullBeansAllowed; + initialize(instanceClass, propertyName); + } + + /** * Initializes most of the internal fields based on the top-level bean * instance and property name (dot-separated string). * @@ -199,6 +253,9 @@ public class NestedMethodProperty<T> extends AbstractProperty<T> { Object object = instance; for (Method m : getMethods) { object = m.invoke(object); + if (object == null && nullBeansAllowed) { + return null; + } } return (T) object; } catch (final Throwable e) { diff --git a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java index b2055fe776..67eb30fae5 100644 --- a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java +++ b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java @@ -34,6 +34,7 @@ public class NestedPropertyDescriptor<BT> implements private final String name; private final Class<?> propertyType; + private final boolean nullBeansAllowed; /** * Creates a property descriptor that can create MethodProperty instances to @@ -48,10 +49,29 @@ public class NestedPropertyDescriptor<BT> implements */ public NestedPropertyDescriptor(String name, Class<BT> beanType) throws IllegalArgumentException { + this(name, beanType, false); + } + + /** + * Creates a property descriptor that can create MethodProperty instances to + * access the underlying bean property. + * + * @param name + * of the property in a dotted path format, e.g. "address.street" + * @param beanType + * type (class) of the top-level bean + * @param nullBeansAllowed + * set true to allow null values from intermediate getters + * @throws IllegalArgumentException + * if the property name is invalid + */ + public NestedPropertyDescriptor(String name, Class<BT> beanType, + boolean nullBeansAllowed) throws IllegalArgumentException { this.name = name; NestedMethodProperty<?> property = new NestedMethodProperty<Object>( - beanType, name); + beanType, name, nullBeansAllowed); this.propertyType = property.getType(); + this.nullBeansAllowed = nullBeansAllowed; } @Override @@ -66,7 +86,7 @@ public class NestedPropertyDescriptor<BT> implements @Override public Property<?> createProperty(BT bean) { - return new NestedMethodProperty<Object>(bean, name); + return new NestedMethodProperty<Object>(bean, name, nullBeansAllowed); } } diff --git a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java index cadfdcc774..4d3717e9ba 100644 --- a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java +++ b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java @@ -16,6 +16,7 @@ package com.vaadin.data.util.converter; +import java.math.BigDecimal; import java.util.Date; import java.util.logging.Logger; @@ -103,10 +104,10 @@ public class DefaultConverterFactory implements ConverterFactory { return new StringToIntegerConverter(); } else if (Long.class.isAssignableFrom(sourceType)) { return new StringToLongConverter(); + } else if (BigDecimal.class.isAssignableFrom(sourceType)) { + return new StringToBigDecimalConverter(); } else if (Boolean.class.isAssignableFrom(sourceType)) { return new StringToBooleanConverter(); - } else if (Number.class.isAssignableFrom(sourceType)) { - return new StringToNumberConverter(); } else if (Date.class.isAssignableFrom(sourceType)) { return new StringToDateConverter(); } else { diff --git a/server/src/com/vaadin/data/util/converter/StringToBigDecimalConverter.java b/server/src/com/vaadin/data/util/converter/StringToBigDecimalConverter.java new file mode 100644 index 0000000000..75d4cedd23 --- /dev/null +++ b/server/src/com/vaadin/data/util/converter/StringToBigDecimalConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2013 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.converter; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * A converter that converts from {@link String} to {@link BigDecimal} and back. + * Uses the given locale and a {@link NumberFormat} instance for formatting and + * parsing. + * <p> + * Leading and trailing white spaces are ignored when converting from a String. + * </p> + * <p> + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + * </p> + * + * @author Vaadin Ltd + * @since 7.2 + */ +public class StringToBigDecimalConverter extends + AbstractStringToNumberConverter<BigDecimal> { + @Override + protected NumberFormat getFormat(Locale locale) { + NumberFormat numberFormat = super.getFormat(locale); + if (numberFormat instanceof DecimalFormat) { + ((DecimalFormat) numberFormat).setParseBigDecimal(true); + } + + return numberFormat; + } + + @Override + public BigDecimal convertToModel(String value, + Class<? extends BigDecimal> targetType, Locale locale) + throws com.vaadin.data.util.converter.Converter.ConversionException { + return (BigDecimal) convertToNumber(value, BigDecimal.class, locale); + } + + @Override + public Class<BigDecimal> getModelType() { + return BigDecimal.class; + } +} diff --git a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java deleted file mode 100644 index 22df42403f..0000000000 --- a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2000-2013 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.converter; - -import java.text.NumberFormat; -import java.util.Locale; - -/** - * A converter that converts from {@link Number} to {@link String} and back. - * Uses the given locale and {@link NumberFormat} for formatting and parsing. - * <p> - * Override and overwrite {@link #getFormat(Locale)} to use a different format. - * </p> - * - * @author Vaadin Ltd - * @since 7.0 - */ -public class StringToNumberConverter extends - AbstractStringToNumberConverter<Number> { - - /* - * (non-Javadoc) - * - * @see - * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, - * java.lang.Class, java.util.Locale) - */ - @Override - public Number convertToModel(String value, - Class<? extends Number> targetType, Locale locale) - throws ConversionException { - if (targetType != getModelType()) { - throw new ConversionException("Converter only supports " - + getModelType().getName() + " (targetType was " - + targetType.getName() + ")"); - } - - return convertToNumber(value, targetType, locale); - } - - /* - * (non-Javadoc) - * - * @see com.vaadin.data.util.converter.Converter#getModelType() - */ - @Override - public Class<Number> getModelType() { - return Number.class; - } - -} diff --git a/server/src/com/vaadin/data/validator/BeanValidator.java b/server/src/com/vaadin/data/validator/BeanValidator.java index ea7189bc5e..54efa51ac1 100644 --- a/server/src/com/vaadin/data/validator/BeanValidator.java +++ b/server/src/com/vaadin/data/validator/BeanValidator.java @@ -17,8 +17,6 @@ package com.vaadin.data.validator; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import java.util.Set; @@ -115,7 +113,9 @@ public class BeanValidator implements Validator { Set<?> violations = getJavaxBeanValidator().validateValue(beanClass, propertyName, value); if (violations.size() > 0) { - List<String> exceptions = new ArrayList<String>(); + InvalidValueException[] causes = new InvalidValueException[violations + .size()]; + int i = 0; for (Object v : violations) { final ConstraintViolation<?> violation = (ConstraintViolation<?>) v; String msg = getJavaxBeanValidatorFactory() @@ -123,16 +123,11 @@ public class BeanValidator implements Validator { violation.getMessageTemplate(), new SimpleContext(value, violation .getConstraintDescriptor()), locale); - exceptions.add(msg); + causes[i] = new InvalidValueException(msg); + ++i; } - StringBuilder b = new StringBuilder(); - for (int i = 0; i < exceptions.size(); i++) { - if (i != 0) { - b.append("<br/>"); - } - b.append(exceptions.get(i)); - } - throw new InvalidValueException(b.toString()); + + throw new InvalidValueException(null, causes); } } diff --git a/server/src/com/vaadin/event/UIEvents.java b/server/src/com/vaadin/event/UIEvents.java new file mode 100644 index 0000000000..321bfc9251 --- /dev/null +++ b/server/src/com/vaadin/event/UIEvents.java @@ -0,0 +1,116 @@ +/* + * Copyright 2000-2013 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.event; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; +import com.vaadin.util.ReflectTools; + +/** + * A class that contains events, listeners and handlers specific to the + * {@link UI} class. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface UIEvents { + + /** + * A {@link PollListener} receives and handles {@link PollEvent PollEvents} + * fired by {@link PollNotifier PollNotifiers}. + * + * @since 7.2 + * @author Vaadin Ltd + */ + public interface PollListener extends Serializable { + public static final Method POLL_METHOD = ReflectTools.findMethod( + PollListener.class, "poll", PollEvent.class); + + /** + * A poll request has been received by the server. + * + * @param event + * poll event + */ + public void poll(PollEvent event); + } + + /** + * An event that is fired whenever a client polls the server for + * asynchronous UI updates. + * + * @since 7.2 + * @author Vaadin Ltd + */ + public static class PollEvent extends Component.Event { + public PollEvent(UI ui) { + super(ui); + } + + /** + * Get the {@link UI} instance that received the poll request. + * + * @return the {@link UI} that received the poll request. Never + * <code>null</code>. + */ + public UI getUI() { + /* + * This cast is safe to make, since this class' constructor + * constrains the source to be a UI instance. + */ + return (UI) getComponent(); + } + } + + /** + * The interface for adding and removing {@link PollEvent} listeners. + * <p> + * By implementing this interface, a class publicly announces that it is + * able to send {@link PollEvent PollEvents} whenever the client sends a + * periodic poll message to the client, to check for asynchronous + * server-side modifications. + * + * @since 7.2 + * @see UI#setPollInterval(int) + */ + public interface PollNotifier extends Serializable { + /** + * Add a poll listener. + * <p> + * The listener is called whenever the client polls the server for + * asynchronous UI updates. + * + * @see UI#setPollInterval(int) + * @see #removePollListener(PollListener) + * @param listener + * the {@link PollListener} to add + */ + public void addPollListener(PollListener listener); + + /** + * Remove a poll listener. + * + * @see #addPollListener(PollListener) + * @param listener + * the listener to be removed + */ + public void removePollListener(PollListener listener); + } + +} diff --git a/server/src/com/vaadin/server/BootstrapHandler.java b/server/src/com/vaadin/server/BootstrapHandler.java index 0a4949ffa7..5a117958a0 100644 --- a/server/src/com/vaadin/server/BootstrapHandler.java +++ b/server/src/com/vaadin/server/BootstrapHandler.java @@ -146,14 +146,15 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { } @Override + protected boolean canHandleRequest(VaadinRequest request) { + // We do not want to handle /APP requests here, instead let it fall + // through and produce a 404 + return !ServletPortletHelper.isAppRequest(request); + } + + @Override public boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { - if (ServletPortletHelper.isAppRequest(request)) { - // We do not want to handle /APP requests here, instead let it fall - // through and produce a 404 - return false; - } - try { // Update WebBrowser here only to make WebBrowser information // available in init for LegacyApplications diff --git a/server/src/com/vaadin/server/Page.java b/server/src/com/vaadin/server/Page.java index 037d8e8352..5c8b1aeb42 100644 --- a/server/src/com/vaadin/server/Page.java +++ b/server/src/com/vaadin/server/Page.java @@ -476,6 +476,8 @@ public class Page implements Serializable { private final PageState state; + private String windowName; + public Page(UI uI, PageState state) { this.uI = uI; this.state = state; @@ -637,6 +639,7 @@ public class Page implements Serializable { String location = request.getParameter("v-loc"); String clientWidth = request.getParameter("v-cw"); String clientHeight = request.getParameter("v-ch"); + windowName = request.getParameter("v-wn"); if (location != null) { try { @@ -662,6 +665,17 @@ public class Page implements Serializable { } /** + * Gets the window.name value of the browser window of this page. + * + * @since 7.2 + * + * @return the window name, <code>null</code> if the name is not known + */ + public String getWindowName() { + return windowName; + } + + /** * Updates the internal state with the given values. Does not resize the * Page or browser window. * @@ -1124,7 +1138,7 @@ public class Page implements Serializable { * the new page title to set */ public void setTitle(String title) { - uI.getRpcProxy(PageClientRpc.class).setTitle(title); + getState(true).title = title; } /** diff --git a/server/src/com/vaadin/server/ServiceDestroyEvent.java b/server/src/com/vaadin/server/ServiceDestroyEvent.java new file mode 100644 index 0000000000..2ae4cc10af --- /dev/null +++ b/server/src/com/vaadin/server/ServiceDestroyEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2013 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.server; + +import java.util.EventObject; + +/** + * Event fired to {@link ServiceDestroyListener} when a {@link VaadinService} is + * being destroyed. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ServiceDestroyEvent extends EventObject { + + /** + * Creates a new event for the given service. + * + * @param service + * the service being destroyed + */ + public ServiceDestroyEvent(VaadinService service) { + super(service); + } + + /* + * (non-Javadoc) + * + * @see java.util.EventObject#getSource() + */ + @Override + public VaadinService getSource() { + return (VaadinService) super.getSource(); + } + +} diff --git a/server/src/com/vaadin/server/ServiceDestroyListener.java b/server/src/com/vaadin/server/ServiceDestroyListener.java new file mode 100644 index 0000000000..ad4966dd58 --- /dev/null +++ b/server/src/com/vaadin/server/ServiceDestroyListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2013 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.server; + +import java.io.Serializable; + +/** + * Listener that gets notified when the {@link VaadinService} to which it has + * been registered is destroyed. + * + * @see VaadinService#addServiceDestroyListener(ServiceDestroyListener) + * @see VaadinService#removeServiceDestroyListener(ServiceDestroyListener) + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface ServiceDestroyListener extends Serializable { + /** + * Invoked when a service is destroyed + * + * @param event + * the event + */ + public void serviceDestroy(ServiceDestroyEvent event); +} diff --git a/server/src/com/vaadin/server/SynchronizedRequestHandler.java b/server/src/com/vaadin/server/SynchronizedRequestHandler.java index ac730dcecb..c695855d7d 100644 --- a/server/src/com/vaadin/server/SynchronizedRequestHandler.java +++ b/server/src/com/vaadin/server/SynchronizedRequestHandler.java @@ -32,6 +32,10 @@ public abstract class SynchronizedRequestHandler implements RequestHandler { @Override public boolean handleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { + if (!canHandleRequest(request)) { + return false; + } + session.lock(); try { return synchronizedHandleRequest(session, request, response); @@ -62,4 +66,25 @@ public abstract class SynchronizedRequestHandler implements RequestHandler { public abstract boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException; + /** + * Check whether a request may be handled by this handler. This can be used + * as an optimization to avoid locking the session just to investigate some + * method property. The default implementation just returns + * <code>true</code> which means that all requests will be handled by + * calling + * {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)} + * with the session locked. + * + * @since 7.2 + * @param request + * the request to handle + * @return <code>true</code> if the request handling should continue once + * the session has been locked; <code>false</code> if there's no + * need to lock the session since the request would still not be + * handled. + */ + protected boolean canHandleRequest(VaadinRequest request) { + return true; + } + } diff --git a/server/src/com/vaadin/server/SystemMessages.java b/server/src/com/vaadin/server/SystemMessages.java index 5e0fde1d4a..299c725207 100644 --- a/server/src/com/vaadin/server/SystemMessages.java +++ b/server/src/com/vaadin/server/SystemMessages.java @@ -63,32 +63,32 @@ public class SystemMessages implements Serializable { protected String sessionExpiredURL = null; protected boolean sessionExpiredNotificationEnabled = true; protected String sessionExpiredCaption = "Session Expired"; - protected String sessionExpiredMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + protected String sessionExpiredMessage = "Take note of any unsaved data, and <u>click here</u> or press ESC key to continue."; protected String communicationErrorURL = null; protected boolean communicationErrorNotificationEnabled = true; protected String communicationErrorCaption = "Communication problem"; - protected String communicationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + protected String communicationErrorMessage = "Take note of any unsaved data, and <u>click here</u> or press ESC to continue."; protected String authenticationErrorURL = null; protected boolean authenticationErrorNotificationEnabled = true; protected String authenticationErrorCaption = "Authentication problem"; - protected String authenticationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue."; + protected String authenticationErrorMessage = "Take note of any unsaved data, and <u>click here</u> or press ESC to continue."; protected String internalErrorURL = null; protected boolean internalErrorNotificationEnabled = true; protected String internalErrorCaption = "Internal error"; - protected String internalErrorMessage = "Please notify the administrator.<br/>Take note of any unsaved data, and <u>click here</u> to continue."; + protected String internalErrorMessage = "Please notify the administrator.<br/>Take note of any unsaved data, and <u>click here</u> or press ESC to continue."; protected String outOfSyncURL = null; protected boolean outOfSyncNotificationEnabled = true; protected String outOfSyncCaption = "Out of sync"; - protected String outOfSyncMessage = "Something has caused us to be out of sync with the server.<br/>Take note of any unsaved data, and <u>click here</u> to re-sync."; + protected String outOfSyncMessage = "Something has caused us to be out of sync with the server.<br/>Take note of any unsaved data, and <u>click here</u> or press ESC to re-sync."; protected String cookiesDisabledURL = null; protected boolean cookiesDisabledNotificationEnabled = true; protected String cookiesDisabledCaption = "Cookies disabled"; - protected String cookiesDisabledMessage = "This application requires cookies to function.<br/>Please enable cookies in your browser and <u>click here</u> to try again."; + protected String cookiesDisabledMessage = "This application requires cookies to function.<br/>Please enable cookies in your browser and <u>click here</u> or press ESC to try again."; /** * Use {@link CustomizedSystemMessages} to customize diff --git a/server/src/com/vaadin/server/VaadinPortlet.java b/server/src/com/vaadin/server/VaadinPortlet.java index 093a1c9152..a41f301219 100644 --- a/server/src/com/vaadin/server/VaadinPortlet.java +++ b/server/src/com/vaadin/server/VaadinPortlet.java @@ -503,6 +503,12 @@ public class VaadinPortlet extends GenericPortlet implements Constants, handleRequest(request, response); } + @Override + public void destroy() { + super.destroy(); + getService().destroy(); + } + private static final Logger getLogger() { return Logger.getLogger(VaadinPortlet.class.getName()); } diff --git a/server/src/com/vaadin/server/VaadinService.java b/server/src/com/vaadin/server/VaadinService.java index 44ceaaaf87..aff0124d16 100644 --- a/server/src/com/vaadin/server/VaadinService.java +++ b/server/src/com/vaadin/server/VaadinService.java @@ -41,7 +41,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; +import javax.portlet.Portlet; import javax.portlet.PortletContext; +import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; @@ -97,6 +99,10 @@ public abstract class VaadinService implements Serializable { .findMethod(SessionDestroyListener.class, "sessionDestroy", SessionDestroyEvent.class); + private static final Method SERVICE_DESTROY_METHOD = ReflectTools + .findMethod(ServiceDestroyListener.class, "serviceDestroy", + ServiceDestroyEvent.class); + /** * @deprecated As of 7.0. Only supported for {@link LegacyApplication}. */ @@ -1674,23 +1680,6 @@ public abstract class VaadinService implements Serializable { FutureAccess future = new FutureAccess(session, runnable); session.getPendingAccessQueue().add(future); - ensureAccessQueuePurged(session); - - return future; - } - - /** - * Makes sure the pending access queue is purged for the provided session. - * If the session is currently locked by the current thread or some other - * thread, the queue will be purged when the session is unlocked. If the - * lock is not held by any thread, it is acquired and the queue is purged - * right away. - * - * @since 7.1.2 - * @param session - * the session for which the access queue should be purged - */ - public void ensureAccessQueuePurged(VaadinSession session) { /* * If no thread is currently holding the lock, pending changes for UIs * with automatic push would not be processed and pushed until the next @@ -1713,6 +1702,8 @@ public abstract class VaadinService implements Serializable { } catch (InterruptedException e) { // Just ignore } + + return future; } /** @@ -1759,4 +1750,50 @@ public abstract class VaadinService implements Serializable { CurrentInstance.restoreInstances(oldInstances); } } + + /** + * Adds a service destroy listener that gets notified when this service is + * destroyed. + * + * @since 7.2 + * @param listener + * the service destroy listener to add + * + * @see #destroy() + * @see #removeServiceDestroyListener(ServiceDestroyListener) + * @see ServiceDestroyListener + */ + public void addServiceDestroyListener(ServiceDestroyListener listener) { + eventRouter.addListener(ServiceDestroyEvent.class, listener, + SERVICE_DESTROY_METHOD); + } + + /** + * Removes a service destroy listener that was previously added with + * {@link #addServiceDestroyListener(ServiceDestroyListener)}. + * + * @since 7.2 + * @param listener + * the service destroy listener to remove + */ + public void removeServiceDestroyListener(ServiceDestroyListener listener) { + eventRouter.removeListener(ServiceDestroyEvent.class, listener, + SERVICE_DESTROY_METHOD); + } + + /** + * Called when the servlet, portlet or similar for this service is being + * destroyed. After this method has been called, no more requests will be + * handled by this service. + * + * @see #addServiceDestroyListener(ServiceDestroyListener) + * @see Servlet#destroy() + * @see Portlet#destroy() + * + * @since 7.2 + */ + public void destroy() { + eventRouter.fireEvent(new ServiceDestroyEvent(this)); + } + } diff --git a/server/src/com/vaadin/server/VaadinServlet.java b/server/src/com/vaadin/server/VaadinServlet.java index 7c0f9599f3..baf97d23d9 100644 --- a/server/src/com/vaadin/server/VaadinServlet.java +++ b/server/src/com/vaadin/server/VaadinServlet.java @@ -43,7 +43,6 @@ import javax.servlet.http.HttpServletResponse; import com.vaadin.annotations.VaadinServletConfiguration; import com.vaadin.annotations.VaadinServletConfiguration.InitParameterName; import com.vaadin.sass.internal.ScssStylesheet; -import com.vaadin.server.communication.PushRequestHandler; import com.vaadin.server.communication.ServletUIInitHandler; import com.vaadin.shared.JsonConstants; import com.vaadin.ui.UI; @@ -670,21 +669,11 @@ public class VaadinServlet extends HttpServlet implements Constants { // Provide modification timestamp to the browser if it is known. if (lastModifiedTime > 0) { response.setDateHeader("Last-Modified", lastModifiedTime); - /* - * The browser is allowed to cache for 1 hour without checking if - * the file has changed. This forces browsers to fetch a new version - * when the Vaadin version is updated. This will cause more requests - * to the servlet than without this but for high volume sites the - * static files should never be served through the servlet. The - * cache timeout can be configured by setting the resourceCacheTime - * parameter in web.xml - */ - int resourceCacheTime = getService().getDeploymentConfiguration() - .getResourceCacheTime(); - String cacheControl = "max-age=" - + String.valueOf(resourceCacheTime); - if (filename.contains("nocache")) { - cacheControl = "public, max-age=0, must-revalidate"; + + String cacheControl = "public, max-age=0, must-revalidate"; + int resourceCacheTime = getCacheTime(filename); + if (resourceCacheTime > 0) { + cacheControl = "max-age=" + String.valueOf(resourceCacheTime); } response.setHeader("Cache-Control", cacheControl); } @@ -693,6 +682,43 @@ public class VaadinServlet extends HttpServlet implements Constants { } /** + * Calculates the cache lifetime for the given filename in seconds. By + * default filenames containing ".nocache." return 0, filenames containing + * ".cache." return one year, all other return the value defined in the + * web.xml using resourceCacheTime (defaults to 1 hour). + * + * @param filename + * @return cache lifetime for the given filename in seconds + */ + protected int getCacheTime(String filename) { + /* + * GWT conventions: + * + * - files containing .nocache. will not be cached. + * + * - files containing .cache. will be cached for one year. + * + * https://developers.google.com/web-toolkit/doc/latest/ + * DevGuideCompilingAndDebugging#perfect_caching + */ + if (filename.contains(".nocache.")) { + return 0; + } + if (filename.contains(".cache.")) { + return 60 * 60 * 24 * 365; + } + /* + * For all other files, the browser is allowed to cache for 1 hour + * without checking if the file has changed. This forces browsers to + * fetch a new version when the Vaadin version is updated. This will + * cause more requests to the servlet than without this but for high + * volume sites the static files should never be served through the + * servlet. + */ + return getService().getDeploymentConfiguration().getResourceCacheTime(); + } + + /** * Writes the contents of the given resourceUrl in the response. Can be * overridden to add/modify response headers and similar. * @@ -982,20 +1008,8 @@ public class VaadinServlet extends HttpServlet implements Constants { } protected boolean isStaticResourceRequest(HttpServletRequest request) { - String pathInfo = request.getPathInfo(); - if (pathInfo == null) { - return false; - } - - if ((request.getContextPath() != null) - && (request.getRequestURI().startsWith("/VAADIN/"))) { - return true; - } else if (request.getRequestURI().startsWith( - request.getContextPath() + "/VAADIN/")) { - return true; - } - - return false; + return request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/"); } /** @@ -1075,15 +1089,15 @@ public class VaadinServlet extends HttpServlet implements Constants { return u; } + /* + * (non-Javadoc) + * + * @see javax.servlet.GenericServlet#destroy() + */ @Override public void destroy() { super.destroy(); - - for (RequestHandler handler : getService().getRequestHandlers()) { - if (handler instanceof PushRequestHandler) { - ((PushRequestHandler) handler).destroy(); - } - } + getService().destroy(); } /** diff --git a/server/src/com/vaadin/server/VaadinSession.java b/server/src/com/vaadin/server/VaadinSession.java index fd2ed79acd..f34721944a 100644 --- a/server/src/com/vaadin/server/VaadinSession.java +++ b/server/src/com/vaadin/server/VaadinSession.java @@ -43,7 +43,6 @@ import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; -import com.vaadin.annotations.PreserveOnRefresh; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterFactory; import com.vaadin.data.util.converter.DefaultConverterFactory; @@ -205,7 +204,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { private int nextUIId = 0; private Map<Integer, UI> uIs = new HashMap<Integer, UI>(); - private final Map<String, Integer> retainOnRefreshUIs = new HashMap<String, Integer>(); + private final Map<String, Integer> embedIdMap = new HashMap<String, Integer>(); private final EventRouter eventRouter = new EventRouter(); @@ -828,10 +827,13 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ public void removeUI(UI ui) { assert hasLock(); - int id = ui.getUIId(); + Integer id = Integer.valueOf(ui.getUIId()); ui.setSession(null); uIs.remove(id); - retainOnRefreshUIs.values().remove(id); + String embedId = ui.getEmbedId(); + if (embedId != null && id.equals(embedIdMap.get(embedId))) { + embedIdMap.remove(embedId); + } } /** @@ -938,14 +940,12 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ public void unlock() { assert hasLock(); - boolean ultimateRelease = false; try { /* * Run pending tasks and push if the reentrant lock will actually be * released by this unlock() invocation. */ if (((ReentrantLock) getLockInstance()).getHoldCount() == 1) { - ultimateRelease = true; getService().runPendingAccessTasks(this); for (UI ui : getUIs()) { @@ -963,18 +963,6 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } finally { getLockInstance().unlock(); } - - /* - * If the session is locked when a new access task is added, it is - * assumed that the queue will be purged when the lock is released. This - * might however not happen if a task is enqueued between the moment - * when unlock() purges the queue and the moment when the lock is - * actually released. This means that the queue should be purged again - * if it is not empty after unlocking. - */ - if (ultimateRelease && !getPendingAccessQueue().isEmpty()) { - getService().ensureAccessQueuePurged(this); - } } /** @@ -1099,20 +1087,6 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } /** - * Gets the mapping from <code>window.name</code> to UI id for UIs that are - * should be retained on refresh. - * - * @see VaadinService#preserveUIOnRefresh(VaadinRequest, UI, UIProvider) - * @see PreserveOnRefresh - * - * @return the mapping between window names and UI ids for this session. - */ - public Map<String, Integer> getPreserveOnRefreshUIs() { - assert hasLock(); - return retainOnRefreshUIs; - } - - /** * Adds an initialized UI to this session. * * @param ui @@ -1129,7 +1103,21 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { "The UI belongs to a different session"); } - uIs.put(Integer.valueOf(ui.getUIId()), ui); + Integer uiId = Integer.valueOf(ui.getUIId()); + uIs.put(uiId, ui); + + String embedId = ui.getEmbedId(); + if (embedId != null) { + Integer previousUiId = embedIdMap.put(embedId, uiId); + if (previousUiId != null) { + UI previousUi = uIs.get(previousUiId); + assert previousUi != null + && embedId.equals(previousUi.getEmbedId()) : "UI id map and embed id map not in sync"; + + // Will fire cleanup events at the end of the request handling. + previousUi.close(); + } + } } /** @@ -1340,4 +1328,25 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { stream.defaultReadObject(); pendingAccessQueue = new ConcurrentLinkedQueue<FutureAccess>(); } + + /** + * Finds the UI with the corresponding embed id. + * + * @since 7.2 + * @param embedId + * the embed id + * @return the UI with the corresponding embed id, or <code>null</code> if + * no UI is found + * + * @see UI#getEmbedId() + */ + public UI getUIByEmbedId(String embedId) { + Integer uiId = embedIdMap.get(embedId); + if (uiId == null) { + return null; + } else { + return getUIById(uiId.intValue()); + } + } + } diff --git a/server/src/com/vaadin/server/communication/FileUploadHandler.java b/server/src/com/vaadin/server/communication/FileUploadHandler.java index 3f6bfd9267..41a16601fe 100644 --- a/server/src/com/vaadin/server/communication/FileUploadHandler.java +++ b/server/src/com/vaadin/server/communication/FileUploadHandler.java @@ -284,7 +284,7 @@ public class FileUploadHandler implements RequestHandler { // if boundary string does not exist, the posted file is from // XHR2.post(File) doHandleXhrFilePost(session, request, response, streamVariable, - variableName, source, request.getContentLength()); + variableName, source, getContentLength(request)); } return true; } @@ -336,7 +336,7 @@ public class FileUploadHandler implements RequestHandler { final InputStream inputStream = request.getInputStream(); - int contentLength = request.getContentLength(); + long contentLength = getContentLength(request); boolean atStart = false; boolean firstFileFieldFound = false; @@ -403,9 +403,22 @@ public class FileUploadHandler implements RequestHandler { } + /* + * request.getContentLength() is limited to "int" by the Servlet + * specification. To support larger file uploads manually evaluate the + * Content-Length header which can contain long values. + */ + private long getContentLength(VaadinRequest request) { + try { + return Long.parseLong(request.getHeader("Content-Length")); + } catch (NumberFormatException e) { + return -1l; + } + } + private void handleFileUploadValidationAndData(VaadinSession session, InputStream inputStream, StreamVariable streamVariable, - String filename, String mimeType, int contentLength, + String filename, String mimeType, long contentLength, ClientConnector connector, String variableName) throws UploadException { session.lock(); @@ -474,7 +487,7 @@ public class FileUploadHandler implements RequestHandler { protected void doHandleXhrFilePost(VaadinSession session, VaadinRequest request, VaadinResponse response, StreamVariable streamVariable, String variableName, - ClientConnector owner, int contentLength) throws IOException { + ClientConnector owner, long contentLength) throws IOException { // These are unknown in filexhr ATM, maybe add to Accept header that // is accessible in portlets @@ -504,7 +517,7 @@ public class FileUploadHandler implements RequestHandler { */ protected final boolean streamToReceiver(VaadinSession session, final InputStream in, StreamVariable streamVariable, - String filename, String type, int contentLength) + String filename, String type, long contentLength) throws UploadException { if (streamVariable == null) { throw new IllegalStateException( @@ -512,7 +525,7 @@ public class FileUploadHandler implements RequestHandler { } OutputStream out = null; - int totalBytes = 0; + long totalBytes = 0; StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( filename, type, contentLength); try { diff --git a/server/src/com/vaadin/server/communication/HeartbeatHandler.java b/server/src/com/vaadin/server/communication/HeartbeatHandler.java index 4c95859203..04cb1b5a25 100644 --- a/server/src/com/vaadin/server/communication/HeartbeatHandler.java +++ b/server/src/com/vaadin/server/communication/HeartbeatHandler.java @@ -43,6 +43,11 @@ import com.vaadin.ui.UI; public class HeartbeatHandler extends SynchronizedRequestHandler implements SessionExpiredHandler { + @Override + protected boolean canHandleRequest(VaadinRequest request) { + return ServletPortletHelper.isHeartbeatRequest(request); + } + /** * Handles a heartbeat request for the given session. Reads the GET * parameter named {@link UIConstants#UI_ID_PARAMETER} to identify the UI. @@ -53,10 +58,6 @@ public class HeartbeatHandler extends SynchronizedRequestHandler implements @Override public boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { - if (!ServletPortletHelper.isHeartbeatRequest(request)) { - return false; - } - UI ui = session.getService().findUI(request); if (ui != null) { ui.setLastHeartbeatTimestamp(System.currentTimeMillis()); diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java index 8d0da24896..74595322a0 100644 --- a/server/src/com/vaadin/server/communication/PushRequestHandler.java +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -28,6 +28,8 @@ import org.atmosphere.cpr.AtmosphereRequest; import org.atmosphere.cpr.AtmosphereResponse; import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServiceDestroyEvent; +import com.vaadin.server.ServiceDestroyListener; import com.vaadin.server.ServiceException; import com.vaadin.server.ServletPortletHelper; import com.vaadin.server.SessionExpiredHandler; @@ -63,6 +65,13 @@ public class PushRequestHandler implements RequestHandler, } }; + service.addServiceDestroyListener(new ServiceDestroyListener() { + @Override + public void serviceDestroy(ServiceDestroyEvent event) { + destroy(); + } + }); + pushHandler = new PushHandler(service); atmosphere.addAtmosphereHandler("/*", pushHandler); atmosphere.addInitParameter(ApplicationConfig.PROPERTY_SESSION_SUPPORT, diff --git a/server/src/com/vaadin/server/communication/ServerRpcHandler.java b/server/src/com/vaadin/server/communication/ServerRpcHandler.java index f14d703454..432a9ea893 100644 --- a/server/src/com/vaadin/server/communication/ServerRpcHandler.java +++ b/server/src/com/vaadin/server/communication/ServerRpcHandler.java @@ -20,8 +20,6 @@ import java.io.IOException; import java.io.Reader; import java.io.Serializable; import java.lang.reflect.Type; -import java.text.CharacterIterator; -import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -32,6 +30,7 @@ import java.util.logging.Logger; import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import com.vaadin.server.ClientConnector; import com.vaadin.server.JsonCodec; @@ -62,10 +61,71 @@ import com.vaadin.ui.UI; */ public class ServerRpcHandler implements Serializable { - /* Variable records indexes */ - public static final char VAR_BURST_SEPARATOR = '\u001d'; + /** + * A data transfer object representing an RPC request sent by the client + * side. + * + * @since 7.2 + * @author Vaadin Ltd + */ + public static class RpcRequest { + + private final String csrfToken; + private final JSONArray invocations; + private final int syncId; + private final JSONObject json; + + public RpcRequest(String jsonString) throws JSONException { + json = new JSONObject(jsonString); + csrfToken = json.getString(ApplicationConstants.CSRF_TOKEN); + syncId = json.getInt(ApplicationConstants.SERVER_SYNC_ID); + invocations = new JSONArray( + json.getString(ApplicationConstants.RPC_INVOCATIONS)); + } - public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + /** + * Gets the CSRF security token (double submit cookie) for this request. + * + * @return the CSRF security token for this current change request + */ + public String getCsrfToken() { + return csrfToken; + } + + /** + * Gets the data to recreate the RPC as requested by the client side. + * + * @return the data describing which RPC should be made, and all their + * data + */ + public JSONArray getRpcInvocationsData() { + return invocations; + } + + /** + * Gets the sync id last seen by the client. + * + * @return the last sync id given by the server, according to the + * client's request + */ + public int getSyncId() { + return syncId; + } + + /** + * Gets the entire request in JSON format, as it was received from the + * client. + * <p> + * <em>Note:</em> This is a shared reference - any modifications made + * will be shared. + * + * @return the raw JSON object that was received from the client + * + */ + public JSONObject getRawJson() { + return json; + } + } private static final int MAX_BUFFER_SIZE = 64 * 1024; @@ -90,45 +150,50 @@ public class ServerRpcHandler implements Serializable { throws IOException, InvalidUIDLSecurityKeyException, JSONException { ui.getSession().setLastRequestTimestamp(System.currentTimeMillis()); - String changes = getMessage(reader); + String changeMessage = getMessage(reader); - final String[] bursts = changes.split(String - .valueOf(VAR_BURST_SEPARATOR)); - - if (bursts.length > 2) { - throw new RuntimeException( - "Multiple variable bursts not supported in Vaadin 7"); - } else if (bursts.length <= 1) { + if (changeMessage == null || changeMessage.equals("")) { // The client sometimes sends empty messages, this is probably a bug return; } + RpcRequest rpcRequest = new RpcRequest(changeMessage); + // Security: double cookie submission pattern unless disabled by // property - if (!VaadinService.isCsrfTokenValid(ui.getSession(), bursts[0])) { + if (!VaadinService.isCsrfTokenValid(ui.getSession(), + rpcRequest.getCsrfToken())) { throw new InvalidUIDLSecurityKeyException(""); } - handleBurst(ui, unescapeBurst(bursts[1])); + handleInvocations(ui, rpcRequest.getSyncId(), + rpcRequest.getRpcInvocationsData()); + + ui.getConnectorTracker().cleanConcurrentlyRemovedConnectorIds( + rpcRequest.getSyncId()); } /** - * Processes a message burst received from the client. - * - * A burst can contain any number of RPC calls, including legacy variable - * change calls that are processed separately. - * + * Processes invocations data received from the client. + * <p> + * The invocations data can contain any number of RPC calls, including + * legacy variable change calls that are processed separately. + * <p> * Consecutive changes to the value of the same variable are combined and * changeVariables() is only called once for them. This preserves the Vaadin * 6 semantics for components and add-ons that do not use Vaadin 7 RPC * directly. * - * @param source * @param uI - * the UI receiving the burst - * @param burst - * the content of the burst as a String to be parsed + * the UI receiving the invocations data + * @param lastSyncIdSeenByClient + * the most recent sync id the client has seen at the time the + * request was sent + * @param invocationsData + * JSON containing all information needed to execute all + * requested RPC calls. */ - private void handleBurst(UI uI, String burst) { + private void handleInvocations(UI uI, int lastSyncIdSeenByClient, + JSONArray invocationsData) { // TODO PUSH Refactor so that this is not needed LegacyCommunicationManager manager = uI.getSession() .getCommunicationManager(); @@ -137,7 +202,8 @@ public class ServerRpcHandler implements Serializable { Set<Connector> enabledConnectors = new HashSet<Connector>(); List<MethodInvocation> invocations = parseInvocations( - uI.getConnectorTracker(), burst); + uI.getConnectorTracker(), invocationsData, + lastSyncIdSeenByClient); for (MethodInvocation invocation : invocations) { final ClientConnector connector = manager.getConnector(uI, invocation.getConnectorId()); @@ -243,21 +309,22 @@ public class ServerRpcHandler implements Serializable { } /** - * Parse a message burst from the client into a list of MethodInvocation - * instances. + * Parse JSON from the client into a list of MethodInvocation instances. * * @param connectorTracker * The ConnectorTracker used to lookup connectors - * @param burst - * message string (JSON) + * @param invocationsJson + * JSON containing all information needed to execute all + * requested RPC calls. + * @param lastSyncIdSeenByClient + * the most recent sync id the client has seen at the time the + * request was sent * @return list of MethodInvocation to perform * @throws JSONException */ private List<MethodInvocation> parseInvocations( - ConnectorTracker connectorTracker, String burst) - throws JSONException { - JSONArray invocationsJson = new JSONArray(burst); - + ConnectorTracker connectorTracker, JSONArray invocationsJson, + int lastSyncIdSeenByClient) throws JSONException { ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); MethodInvocation previousInvocation = null; @@ -267,7 +334,8 @@ public class ServerRpcHandler implements Serializable { JSONArray invocationJson = invocationsJson.getJSONArray(i); MethodInvocation invocation = parseInvocation(invocationJson, - previousInvocation, connectorTracker); + previousInvocation, connectorTracker, + lastSyncIdSeenByClient); if (invocation != null) { // Can be null if the invocation was a legacy invocation and it // was merged with the previous one or if the invocation was @@ -281,7 +349,8 @@ public class ServerRpcHandler implements Serializable { private MethodInvocation parseInvocation(JSONArray invocationJson, MethodInvocation previousInvocation, - ConnectorTracker connectorTracker) throws JSONException { + ConnectorTracker connectorTracker, long lastSyncIdSeenByClient) + throws JSONException { String connectorId = invocationJson.getString(0); String interfaceName = invocationJson.getString(1); String methodName = invocationJson.getString(2); @@ -289,18 +358,22 @@ public class ServerRpcHandler implements Serializable { if (connectorTracker.getConnector(connectorId) == null && !connectorId .equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) { - getLogger() - .log(Level.WARNING, - "RPC call to " - + interfaceName - + "." - + methodName - + " received for connector " - + connectorId - + " but no such connector could be found. Resynchronizing client."); - // This is likely an out of sync issue (client tries to update a - // connector which is not present). Force resync. - connectorTracker.markAllConnectorsDirty(); + + if (!connectorTracker.connectorWasPresentAsRequestWasSent( + connectorId, lastSyncIdSeenByClient)) { + getLogger() + .log(Level.WARNING, + "RPC call to " + + interfaceName + + "." + + methodName + + " received for connector " + + connectorId + + " but no such connector could be found. Resynchronizing client."); + // This is likely an out of sync issue (client tries to update a + // connector which is not present). Force resync. + connectorTracker.markAllConnectorsDirty(); + } return null; } @@ -396,50 +469,6 @@ public class ServerRpcHandler implements Serializable { owner.changeVariables(source, m); } - /** - * Unescape encoded burst separator characters in a burst received from the - * client. This protects from separator injection attacks. - * - * @param encodedValue - * to decode - * @return decoded value - */ - protected String unescapeBurst(String encodedValue) { - final StringBuilder result = new StringBuilder(); - final StringCharacterIterator iterator = new StringCharacterIterator( - encodedValue); - char character = iterator.current(); - while (character != CharacterIterator.DONE) { - if (VAR_ESCAPE_CHARACTER == character) { - character = iterator.next(); - switch (character) { - case VAR_ESCAPE_CHARACTER + 0x30: - // escaped escape character - result.append(VAR_ESCAPE_CHARACTER); - break; - case VAR_BURST_SEPARATOR + 0x30: - // +0x30 makes these letters for easier reading - result.append((char) (character - 0x30)); - break; - case CharacterIterator.DONE: - // error - throw new RuntimeException( - "Communication error: Unexpected end of message"); - default: - // other escaped character - probably a client-server - // version mismatch - throw new RuntimeException( - "Invalid escaped character from the client - check that the widgetset and server versions match"); - } - } else { - // not a special character - add it to the result as is - result.append(character); - } - character = iterator.next(); - } - return result.toString(); - } - protected String getMessage(Reader reader) throws IOException { StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE); diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java index d4b0bc709f..6ab9d9dc58 100644 --- a/server/src/com/vaadin/server/communication/UIInitHandler.java +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.util.List; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,6 +37,7 @@ import com.vaadin.server.VaadinResponse; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinSession; import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.JsonConstants; import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.Transport; import com.vaadin.shared.ui.ui.UIConstants; @@ -56,12 +56,13 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { protected abstract boolean isInitRequest(VaadinRequest request); @Override + protected boolean canHandleRequest(VaadinRequest request) { + return isInitRequest(request); + } + + @Override public boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { - if (!isInitRequest(request)) { - return false; - } - StringWriter stringWriter = new StringWriter(); try { @@ -107,7 +108,7 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { static boolean commitJsonResponse(VaadinRequest request, VaadinResponse response, String json) throws IOException { // The response was produced without errors so write it to the client - response.setContentType("application/json; charset=UTF-8"); + response.setContentType(JsonConstants.JSON_CONTENT_TYPE); // Ensure that the browser does not cache UIDL responses. // iOS 6 Safari requires this (#9732) @@ -163,31 +164,29 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { return null; } - // Check for an existing UI based on window.name + // Check for an existing UI based on embed id - // Special parameter sent by vaadinBootstrap.js - String windowName = request.getParameter("v-wn"); + String embedId = getEmbedId(request); - Map<String, Integer> retainOnRefreshUIs = session - .getPreserveOnRefreshUIs(); - if (windowName != null && !retainOnRefreshUIs.isEmpty()) { - // Check for a known UI - - Integer retainedUIId = retainOnRefreshUIs.get(windowName); - - if (retainedUIId != null) { - UI retainedUI = session.getUIById(retainedUIId.intValue()); + UI retainedUI = session.getUIByEmbedId(embedId); + if (retainedUI != null) { + if (vaadinService.preserveUIOnRefresh(provider, new UICreateEvent( + request, uiClass))) { if (uiClass.isInstance(retainedUI)) { reinitUI(retainedUI, request); return retainedUI; } else { getLogger().info( - "Not using retained UI in " + windowName - + " because retained UI was of type " + "Not using the preserved UI " + embedId + + " because it is of type " + retainedUI.getClass() + " but " + uiClass + " is expected for the request."); } } + /* + * Previous UI without preserve on refresh will be closed when the + * new UI gets added to the session. + */ } // No existing UI found - go on by creating and initializing one @@ -220,26 +219,45 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { // Set thread local here so it is available in init UI.setCurrent(ui); - ui.doInit(request, uiId.intValue()); + ui.doInit(request, uiId.intValue(), embedId); session.addUI(ui); - // Remember if it should be remembered - if (vaadinService.preserveUIOnRefresh(provider, event)) { - // Remember this UI - if (windowName == null) { - getLogger().warning( - "There is no window.name available for UI " + uiClass - + " that should be preserved."); - } else { - session.getPreserveOnRefreshUIs().put(windowName, uiId); - } + // Warn if the window can't be preserved + if (embedId == null + && vaadinService.preserveUIOnRefresh(provider, event)) { + getLogger().warning( + "There is no embed id available for UI " + uiClass + + " that should be preserved."); } return ui; } /** + * Constructs an embed id based on information in the request. + * + * @since 7.2 + * + * @param request + * the request to get embed information from + * @return the embed id, or <code>null</code> if id is not available. + * + * @see UI#getEmbedId() + */ + protected String getEmbedId(VaadinRequest request) { + // Parameters sent by vaadinBootstrap.js + String windowName = request.getParameter("v-wn"); + String appId = request.getParameter("v-appId"); + + if (windowName != null && appId != null) { + return windowName + '.' + appId; + } else { + return null; + } + } + + /** * Updates a UI that has already been initialized but is now loaded again, * e.g. because of {@link PreserveOnRefresh}. * diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java index d52c5e9fe0..cf25910fa4 100644 --- a/server/src/com/vaadin/server/communication/UidlRequestHandler.java +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -60,11 +60,13 @@ public class UidlRequestHandler extends SynchronizedRequestHandler implements } @Override + protected boolean canHandleRequest(VaadinRequest request) { + return ServletPortletHelper.isUIDLRequest(request); + } + + @Override public boolean synchronizedHandleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { - if (!ServletPortletHelper.isUIDLRequest(request)) { - return false; - } UI uI = session.getService().findUI(request); if (uI == null) { // This should not happen but it will if the UI has been closed. We diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java index 60933a75c2..b46fbbf58a 100644 --- a/server/src/com/vaadin/server/communication/UidlWriter.java +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -38,6 +38,7 @@ import com.vaadin.server.LegacyCommunicationManager; import com.vaadin.server.LegacyCommunicationManager.ClientCache; import com.vaadin.server.SystemMessages; import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.ConnectorTracker; import com.vaadin.ui.UI; @@ -98,6 +99,9 @@ public class UidlWriter implements Serializable { uiConnectorTracker.setWritingResponse(true); try { + writer.write("\"" + ApplicationConstants.SERVER_SYNC_ID + + "\": " + uiConnectorTracker.getCurrentSyncId() + ", "); + writer.write("\"changes\" : "); JsonPaintTarget paintTarget = new JsonPaintTarget(manager, writer, diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java index 485327bb54..c385805675 100644 --- a/server/src/com/vaadin/ui/Component.java +++ b/server/src/com/vaadin/ui/Component.java @@ -651,7 +651,7 @@ public interface Component extends ClientConnector, Sizeable, Serializable { public Locale getLocale(); /** - * Adds an unique id for component that get's transferred to terminal for + * Adds an unique id for component that is used in the client-side for * testing purposes. Keeping identifiers unique is the responsibility of the * programmer. * @@ -661,7 +661,7 @@ public interface Component extends ClientConnector, Sizeable, Serializable { public void setId(String id); /** - * Get's currently set debug identifier + * Gets currently set debug identifier * * @return current id, null if not set */ @@ -669,7 +669,7 @@ public interface Component extends ClientConnector, Sizeable, Serializable { /** * <p> - * Gets the component's description, used in tooltips and can be displayed + * Gets the components description, used in tooltips and can be displayed * directly in certain other components such as forms. The description can * be used to briefly describe the state of the component to the user. The * description string may contain certain XML tags: diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java index 0f8ec60104..33d585adca 100644 --- a/server/src/com/vaadin/ui/ConnectorTracker.java +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -81,6 +82,16 @@ public class ConnectorTracker implements Serializable { private Map<StreamVariable, String> streamVariableToSeckey; + private int currentSyncId = 0; + + /** + * Map to track on which syncId each connector was removed. + * + * @see #getCurrentSyncId() + * @see #cleanConcurrentlyRemovedConnectorIds(long) + */ + private TreeMap<Integer, Set<String>> syncIdToUnregisteredConnectorIds = new TreeMap<Integer, Set<String>>(); + /** * Gets a logger for this class * @@ -170,6 +181,15 @@ public class ConnectorTracker implements Serializable { + " is not the one that was registered for that id"); } + Set<String> unregisteredConnectorIds = syncIdToUnregisteredConnectorIds + .get(currentSyncId); + if (unregisteredConnectorIds == null) { + unregisteredConnectorIds = new HashSet<String>(); + syncIdToUnregisteredConnectorIds.put(currentSyncId, + unregisteredConnectorIds); + } + unregisteredConnectorIds.add(connectorId); + dirtyConnectors.remove(connector); if (unregisteredConnectors.add(connector)) { if (getLogger().isLoggable(Level.FINE)) { @@ -570,12 +590,18 @@ public class ConnectorTracker implements Serializable { /** * Sets the current response write status. Connectors can not be marked as * dirty when the response is written. + * <p> + * This method has a side-effect of incrementing the sync id by one (see + * {@link #getCurrentSyncId()}), if {@link #isWritingResponse()} returns + * <code>false</code> and <code>writingResponse</code> is set to + * <code>true</code>. * * @param writingResponse * the new response status. * * @see #markDirty(ClientConnector) * @see #isWritingResponse() + * @see #getCurrentSyncId() * * @throws IllegalArgumentException * if the new response status is the same as the previous value. @@ -587,6 +613,14 @@ public class ConnectorTracker implements Serializable { throw new IllegalArgumentException( "The old value is same as the new value"); } + + /* + * the right hand side of the && is unnecessary here because of the + * if-clause above, but rigorous coding is always rigorous coding. + */ + if (writingResponse && !this.writingResponse) { + currentSyncId++; + } this.writingResponse = writingResponse; } @@ -732,4 +766,105 @@ public class ConnectorTracker implements Serializable { } return streamVariableToSeckey.get(variable); } + + /** + * Check whether a connector was present on the client when the it was + * creating this request, but was removed server-side before the request + * arrived. + * + * @since 7.2 + * @param connectorId + * The connector id to check for whether it was removed + * concurrently or not. + * @param lastSyncIdSeenByClient + * the most recent sync id the client has seen at the time the + * request was sent + * @return <code>true</code> if the connector was removed before the client + * had a chance to react to it. + */ + public boolean connectorWasPresentAsRequestWasSent(String connectorId, + long lastSyncIdSeenByClient) { + + assert getConnector(connectorId) == null : "Connector " + connectorId + + " is still attached"; + + boolean clientRequestIsTooOld = lastSyncIdSeenByClient < currentSyncId; + if (clientRequestIsTooOld) { + /* + * The headMap call is present here because we're only interested in + * connectors removed "in the past" (i.e. the server has removed + * them before the client ever knew about that), since those are the + * ones that we choose to handle as a special case. + */ + /*- + * Server Client + * [#1 add table] ---------. + * \ + * [push: #2 remove table]-. `--> [adding table, storing #1] + * \ .- [table from request #1 needs more data] + * \/ + * /`-> [removing table, storing #2] + * [#1 < #2 - ignoring] <---´ + */ + for (Set<String> unregisteredConnectors : syncIdToUnregisteredConnectorIds + .headMap(currentSyncId).values()) { + if (unregisteredConnectors.contains(connectorId)) { + return true; + } + } + } + + return false; + } + + /** + * Gets the most recently generated server sync id. + * <p> + * The sync id is incremented by one whenever a new response is being + * written. This id is then sent over to the client. The client then adds + * the most recent sync id to each communication packet it sends back to the + * server. This way, the server knows at what state the client is when the + * packet is sent. If the state has changed on the server side since that, + * the server can try to adjust the way it handles the actions from the + * client side. + * + * @see #setWritingResponse(boolean) + * @see #connectorWasPresentAsRequestWasSent(String, long) + * @since 7.2 + * @return the current sync id + */ + public int getCurrentSyncId() { + return currentSyncId; + } + + /** + * Maintains the bookkeeping connector removal and concurrency by removing + * entries that have become too old. + * <p> + * <em>It is important to run this call for each transmission from the client</em> + * , otherwise the bookkeeping gets out of date and the results form + * {@link #connectorWasPresentAsRequestWasSent(String, long)} will become + * invalid (that is, even though the client knew the component was removed, + * the aforementioned method would start claiming otherwise). + * <p> + * Entries that both client and server agree upon are removed. Since + * argument is the last sync id that the client has seen from the server, we + * know that entries earlier than that cannot cause any problems anymore. + * + * @see #connectorWasPresentAsRequestWasSent(String, long) + * @since 7.2 + * @param lastSyncIdSeenByClient + * the sync id the client has most recently received from the + * server. + */ + public void cleanConcurrentlyRemovedConnectorIds(int lastSyncIdSeenByClient) { + /* + * We remove all entries _older_ than the one reported right now, + * because the remaining still contain components that might cause + * conflicts. In any case, it's better to clean up too little than too + * much, especially as the data will hardly grow into the kilobytes. + */ + syncIdToUnregisteredConnectorIds.headMap(lastSyncIdSeenByClient) + .clear(); + } } diff --git a/server/src/com/vaadin/ui/DragAndDropWrapper.java b/server/src/com/vaadin/ui/DragAndDropWrapper.java index 5d6825c868..2ab3e872c6 100644 --- a/server/src/com/vaadin/ui/DragAndDropWrapper.java +++ b/server/src/com/vaadin/ui/DragAndDropWrapper.java @@ -56,7 +56,7 @@ public class DragAndDropWrapper extends CustomComponent implements DropTarget, for (int i = 0; i < fc; i++) { Html5File file = new Html5File( (String) rawVariables.get("fn" + i), // name - (Integer) rawVariables.get("fs" + i), // size + ((Double) rawVariables.get("fs" + i)).longValue(), // size (String) rawVariables.get("ft" + i)); // mime String id = (String) rawVariables.get("fi" + i); files[i] = file; diff --git a/server/src/com/vaadin/ui/Link.java b/server/src/com/vaadin/ui/Link.java index cf8e1a9693..e1a47777bd 100644 --- a/server/src/com/vaadin/ui/Link.java +++ b/server/src/com/vaadin/ui/Link.java @@ -16,13 +16,10 @@ package com.vaadin.ui; -import java.util.Map; - -import com.vaadin.server.PaintException; -import com.vaadin.server.PaintTarget; import com.vaadin.server.Resource; import com.vaadin.shared.ui.BorderStyle; import com.vaadin.shared.ui.link.LinkConstants; +import com.vaadin.shared.ui.link.LinkState; /** * Link is used to create external or internal URL links. @@ -31,7 +28,7 @@ import com.vaadin.shared.ui.link.LinkConstants; * @since 3.0 */ @SuppressWarnings("serial") -public class Link extends AbstractComponent implements LegacyComponent { +public class Link extends AbstractComponent { /** * @deprecated As of 7.0, use {@link BorderStyle#NONE} instead @@ -51,14 +48,6 @@ public class Link extends AbstractComponent implements LegacyComponent { @Deprecated public static final BorderStyle TARGET_BORDER_DEFAULT = BorderStyle.DEFAULT; - private String targetName; - - private BorderStyle targetBorder = BorderStyle.DEFAULT; - - private int targetWidth = -1; - - private int targetHeight = -1; - /** * Creates a new link. */ @@ -105,43 +94,14 @@ public class Link extends AbstractComponent implements LegacyComponent { setTargetBorder(border); } - /** - * 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 { - if (getResource() == null) { - return; - } - - // Target window name - final String name = getTargetName(); - if (name != null && name.length() > 0) { - target.addAttribute("name", name); - } - - // Target window size - if (getTargetWidth() >= 0) { - target.addAttribute("targetWidth", getTargetWidth()); - } - if (getTargetHeight() >= 0) { - target.addAttribute("targetHeight", getTargetHeight()); - } - - // Target window border - switch (getTargetBorder()) { - case MINIMAL: - target.addAttribute("border", "minimal"); - break; - case NONE: - target.addAttribute("border", "none"); - break; - } + protected LinkState getState() { + return (LinkState) super.getState(); + } + + @Override + protected LinkState getState(boolean markAsDirty) { + return (LinkState) super.getState(markAsDirty); } /** @@ -150,7 +110,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * @return the target window border. */ public BorderStyle getTargetBorder() { - return targetBorder; + return getState(false).targetBorder; } /** @@ -159,7 +119,8 @@ public class Link extends AbstractComponent implements LegacyComponent { * @return the target window height. */ public int getTargetHeight() { - return targetHeight < 0 ? -1 : targetHeight; + return getState(false).targetHeight < 0 ? -1 + : getState(false).targetHeight; } /** @@ -169,7 +130,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * @return the target window name. */ public String getTargetName() { - return targetName; + return getState(false).target; } /** @@ -178,7 +139,8 @@ public class Link extends AbstractComponent implements LegacyComponent { * @return the target window width. */ public int getTargetWidth() { - return targetWidth < 0 ? -1 : targetWidth; + return getState(false).targetWidth < 0 ? -1 + : getState(false).targetWidth; } /** @@ -188,8 +150,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * the targetBorder to set. */ public void setTargetBorder(BorderStyle targetBorder) { - this.targetBorder = targetBorder; - markAsDirty(); + getState().targetBorder = targetBorder; } /** @@ -199,8 +160,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * the targetHeight to set. */ public void setTargetHeight(int targetHeight) { - this.targetHeight = targetHeight; - markAsDirty(); + getState().targetHeight = targetHeight; } /** @@ -210,8 +170,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * the targetName to set. */ public void setTargetName(String targetName) { - this.targetName = targetName; - markAsDirty(); + getState().target = targetName; } /** @@ -221,8 +180,7 @@ public class Link extends AbstractComponent implements LegacyComponent { * the targetWidth to set. */ public void setTargetWidth(int targetWidth) { - this.targetWidth = targetWidth; - markAsDirty(); + getState().targetWidth = targetWidth; } /** @@ -244,8 +202,4 @@ public class Link extends AbstractComponent implements LegacyComponent { setResource(LinkConstants.HREF_RESOURCE, resource); } - @Override - public void changeVariables(Object source, Map<String, Object> variables) { - // TODO Remove once LegacyComponent is no longer implemented - } } diff --git a/server/src/com/vaadin/ui/Notification.java b/server/src/com/vaadin/ui/Notification.java index cf1d03ab5c..31fa265b02 100644 --- a/server/src/com/vaadin/ui/Notification.java +++ b/server/src/com/vaadin/ui/Notification.java @@ -21,6 +21,7 @@ import java.io.Serializable; import com.vaadin.server.Page; import com.vaadin.server.Resource; import com.vaadin.shared.Position; +import com.vaadin.shared.ui.ui.NotificationConfigurationBean.Role; /** * A notification message, used to display temporary messages to the user - for @@ -63,7 +64,7 @@ import com.vaadin.shared.Position; */ public class Notification implements Serializable { public enum Type { - HUMANIZED_MESSAGE, WARNING_MESSAGE, ERROR_MESSAGE, TRAY_NOTIFICATION; + HUMANIZED_MESSAGE, WARNING_MESSAGE, ERROR_MESSAGE, TRAY_NOTIFICATION, ASSISTIVE_NOTIFICATION; } @Deprecated @@ -190,21 +191,38 @@ public class Notification implements Serializable { case WARNING_MESSAGE: delayMsec = 1500; styleName = "warning"; + setNavigationConfiguration("Warning: ", "", Role.ALERT); break; case ERROR_MESSAGE: delayMsec = -1; styleName = "error"; + setNavigationConfiguration("Error: ", " - close with ESC", + Role.ALERT); break; case TRAY_NOTIFICATION: delayMsec = 3000; position = Position.BOTTOM_RIGHT; styleName = "tray"; - + setNavigationConfiguration("Info: ", "", Role.STATUS); + break; + case ASSISTIVE_NOTIFICATION: + delayMsec = 3000; + position = Position.ASSISTIVE; + styleName = "assistive"; + setNavigationConfiguration("Note: ", "", Role.ALERT); + break; case HUMANIZED_MESSAGE: default: + styleName = "humanized"; + setNavigationConfiguration("Info: ", "", Role.ALERT); break; } + } + private void setNavigationConfiguration(String prefix, String postfix, + Role ariaRole) { + UI.getCurrent().getNotificationConfiguration() + .setStyleConfiguration(styleName, prefix, postfix, ariaRole); } /** @@ -322,6 +340,132 @@ public class Notification implements Serializable { } /** + * Sets the accessibility prefix for a notification type. + * + * This prefix is read to assistive device users before the content of the + * notification, but not visible on the page. + * + * @param type + * Type of the notification + * @param prefix + * String that is placed before the notification content + */ + public void setAssistivePrefixForType(Type type, String prefix) { + UI.getCurrent().getNotificationConfiguration() + .setAssistivePrefixForStyle(getStyle(type), prefix); + } + + /** + * Gets the accessibility prefix for a notification type. + * + * This prefix is read to assistive device users before the content of the + * notification, but not visible on the page. + * + * @param type + * Type of the notification + * @return The accessibility prefix for the provided notification type + */ + public String getAssistivePrefixForType(Type type) { + return UI.getCurrent().getNotificationConfiguration() + .getAssistivePrefixForStyle(getStyle(type)); + } + + /** + * Sets the accessibility postfix for a notification type. + * + * This postfix is read to assistive device users after the content of the + * notification, but not visible on the page. + * + * @param type + * Type of the notification + * @param postfix + * String that is placed after the notification content + */ + public void setAssistivePostfixForType(Type type, String postfix) { + UI.getCurrent().getNotificationConfiguration() + .setAssistivePostfixForStyle(getStyle(type), postfix); + } + + /** + * Gets the accessibility postfix for a notification type. + * + * This postfix is read to assistive device users after the content of the + * notification, but not visible on the page. + * + * @param type + * Type of the notification + * @return The accessibility postfix for the provided notification type + */ + public String getAssistivePostfixForType(Type type) { + return UI.getCurrent().getNotificationConfiguration() + .getAssistivePostfixForStyle(getStyle(type)); + } + + /** + * Sets the WAI-ARIA role for a notification type. + * + * This role defines how an assistive device handles a notification. + * Available roles are alert and status (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>). + * + * The default role is alert. + * + * @param type + * Type of the notification + * @param role + * Role to set for the notification type + */ + public void setAssistiveRoleForType(Type type, Role role) { + UI.getCurrent().getNotificationConfiguration() + .setAssistiveRoleForStyle(getStyle(type), role); + } + + /** + * Gets the WAI-ARIA role for a notification type. + * + * This role defines how an assistive device handles a notification. + * Available roles are alert and status (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>) + * + * The default role is alert. + * + * @param type + * Type of the notification + * @return Role to set for the notification type + */ + public Role getAssistiveRoleForType(Type type) { + return UI.getCurrent().getNotificationConfiguration() + .getAssistiveRoleForStyle(getStyle(type)); + } + + private String getStyle(Type type) { + String style = ""; + + switch (type) { + case WARNING_MESSAGE: + style = "warning"; + break; + case ERROR_MESSAGE: + style = "error"; + break; + case TRAY_NOTIFICATION: + style = "tray"; + break; + case ASSISTIVE_NOTIFICATION: + style = "assistive"; + break; + case HUMANIZED_MESSAGE: + default: + style = "humanized"; + break; + } + + return style; + } + + /** * Sets whether html is allowed in the caption and description. If set to * true, the texts are passed to the browser as html and the developer is * responsible for ensuring no harmful html is used. If set to false, the @@ -414,4 +558,4 @@ public class Notification implements Serializable { public static void show(String caption, String description, Type type) { new Notification(caption, description, type).show(Page.getCurrent()); } -}
\ No newline at end of file +} diff --git a/server/src/com/vaadin/ui/NotificationConfiguration.java b/server/src/com/vaadin/ui/NotificationConfiguration.java new file mode 100644 index 0000000000..52d3e76d63 --- /dev/null +++ b/server/src/com/vaadin/ui/NotificationConfiguration.java @@ -0,0 +1,269 @@ +/* + * Copyright 2000-2013 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 com.vaadin.shared.ui.ui.NotificationConfigurationBean; +import com.vaadin.shared.ui.ui.NotificationConfigurationBean.Role; +import com.vaadin.shared.ui.ui.UIState.NotificationConfigurationState; + +/** + * Provides methods for configuring the notification. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public interface NotificationConfiguration extends Serializable { + public void setStyleConfiguration(String style, String prefix, + String postfix, Role ariaRole); + + /** + * Returns the complete configuration object for the given notification + * style. + * + * @param style + * String of the notification style to return + * @return The notification configuration object + */ + public NotificationConfigurationBean getStyleConfiguration(String style); + + /** + * Sets the accessibility prefix for the given notification style. + * + * This prefix is read to assistive device users in front of the content of + * the notification, but not visible on the page. + * + * @param style + * String of the notification style + * @param prefix + * String that is placed before the notification content + */ + public void setAssistivePrefixForStyle(String style, String prefix); + + /** + * Returns the accessibility prefix for the given notification style. + * + * This prefix is read to assistive device users in front of the content of + * the notification, but not visible on the page. + * + * @param style + * String of the notification style + * @return The prefix of the provided notification style + */ + public String getAssistivePrefixForStyle(String style); + + /** + * Sets the accessibility postfix for the given notification style. + * + * This postfix is read to assistive device users after the content of the + * notification, but not visible on the page. + * + * @param style + * String of the notification style + * @param postfix + * String that is placed after the notification content + */ + public void setAssistivePostfixForStyle(String style, String postfix); + + /** + * Returns the accessibility postfix for the given notification style. + * + * This postfix is read to assistive device users after the content of the + * notification, but not visible on the page. + * + * @param style + * String of the notification style + * @return The postfix of the provided notification style + */ + public String getAssistivePostfixForStyle(String style); + + /** + * Sets the WAI-ARIA role for a notification style. + * + * This role defines how an assistive device handles a notification. + * Available roles are alert, alertdialog and status (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>) + * + * The default role is alert. + * + * @param style + * String of the notification style + * @param role + * Role to set for the notification type + */ + public void setAssistiveRoleForStyle(String style, Role role); + + /** + * Returns the WAI-ARIA role for a notification style. + * + * This role defines how an assistive device handles a notification. + * Available roles are alert, alertdialog and status (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a> ) + * + * The default role is alert. + * + * @param style + * String of the notification style + * @return The current Role for the notification type + */ + public Role getAssistiveRoleForStyle(String style); +} + +class NotificationConfigurationImpl implements NotificationConfiguration { + + private UI ui; + + public NotificationConfigurationImpl(UI ui) { + this.ui = ui; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#setStyleConfiguration(java.lang + * .String, java.lang.String, java.lang.String, + * com.vaadin.ui.NotificationConfiguration.Role) + */ + @Override + public void setStyleConfiguration(String style, String prefix, + String postfix, Role ariaRole) { + getState().setup.put(style, new NotificationConfigurationBean(prefix, + postfix, ariaRole)); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#getStyleConfiguration(java.lang + * .String) + */ + @Override + public NotificationConfigurationBean getStyleConfiguration(String style) { + return getState(false).setup.get(style); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#setStylePrefix(java.lang.String, + * java.lang.String) + */ + @Override + public void setAssistivePrefixForStyle(String style, String prefix) { + getConfigurationBean(style).setAssistivePrefix(prefix); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#getStylePrefix(java.lang.String) + */ + @Override + public String getAssistivePrefixForStyle(String style) { + NotificationConfigurationBean styleSetup = getState().setup.get(style); + if (styleSetup != null) { + return styleSetup.getAssistivePrefix(); + } + + return null; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#setStylePostfix(com.vaadin.ui + * .Notification.Type, java.lang.String) + */ + @Override + public void setAssistivePostfixForStyle(String style, String postfix) { + getConfigurationBean(style).setAssistivePostfix(postfix); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.NotificationConfiguration#getStylePostfix(com.vaadin.ui + * .Notification.Type) + */ + @Override + public String getAssistivePostfixForStyle(String style) { + NotificationConfigurationBean styleSetup = getState().setup.get(style); + if (styleSetup != null) { + return styleSetup.getAssistivePostfix(); + } + + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.NotificationConfiguration#setStyleRole(com.vaadin.ui. + * Notification.Type, com.vaadin.ui.NotificationConfiguration.Role) + */ + @Override + public void setAssistiveRoleForStyle(String style, Role role) { + getConfigurationBean(style).setAssistiveRole(role); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.NotificationConfiguration#getStyleRole(com.vaadin.ui. + * Notification.Type) + */ + @Override + public Role getAssistiveRoleForStyle(String style) { + NotificationConfigurationBean styleSetup = getState().setup.get(style); + if (styleSetup != null) { + return styleSetup.getAssistiveRole(); + } + + return null; + } + + private NotificationConfigurationBean getConfigurationBean(String style) { + NotificationConfigurationBean styleSetup = getState().setup.get(style); + if (styleSetup == null) { + styleSetup = new NotificationConfigurationBean(); + getState().setup.put(style, styleSetup); + } + + return styleSetup; + } + + private NotificationConfigurationState getState() { + return ui.getState().notificationConfiguration; + } + + private NotificationConfigurationState getState(boolean markAsDirty) { + return ui.getState(markAsDirty).notificationConfiguration; + } + +} diff --git a/server/src/com/vaadin/ui/TabSheet.java b/server/src/com/vaadin/ui/TabSheet.java index 36022adb74..a1f9e9dd26 100644 --- a/server/src/com/vaadin/ui/TabSheet.java +++ b/server/src/com/vaadin/ui/TabSheet.java @@ -268,7 +268,34 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * @return the created {@link Tab} */ public Tab addTab(Component c, String caption, Resource icon) { - return addTab(c, caption, icon, components.size()); + return addTab(c, caption, icon, "", components.size()); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and icon alternate text and returns the corresponding (old) tab, + * preserving other tab metadata. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @param iconAltText + * the alternate text for the icon + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon, + String iconAltText) { + return addTab(c, caption, icon, iconAltText, components.size()); } /** @@ -294,12 +321,41 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * @return the created {@link Tab} */ public Tab addTab(Component c, String caption, Resource icon, int position) { + return addTab(c, caption, icon, "", position); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and icon alternate text and returns the corresponding (old) tab, + * preserving other tab metadata like the position. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @param iconAltText + * the alternate text for the icon + * @param position + * the position at where the the tab should be added. + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon, + String iconAltText, int position) { if (c == null) { return null; } else if (tabs.containsKey(c)) { Tab tab = tabs.get(c); tab.setCaption(caption); - tab.setIcon(icon); + tab.setIcon(icon, iconAltText); return tab; } else { components.add(position, c); @@ -371,13 +427,15 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, final Component c = i.next(); String caption = null; Resource icon = null; + String iconAltText = ""; if (TabSheet.class.isAssignableFrom(source.getClass())) { Tab tab = ((TabSheet) source).getTab(c); caption = tab.getCaption(); icon = tab.getIcon(); + iconAltText = tab.getIconAltText(); } source.removeComponent(c); - addTab(c, caption, icon); + addTab(c, caption, icon, iconAltText); } } @@ -429,9 +487,12 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, if (icon != null) { target.addAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON, icon); + target.addAttribute( + TabsheetBaseConstants.ATTRIBUTE_TAB_ICON_ALT, + tab.getIconAltText()); } final String caption = tab.getCaption(); - if (caption != null && caption.length() > 0) { + if (caption != null && !caption.isEmpty()) { target.addAttribute( TabsheetBaseConstants.ATTRIBUTE_TAB_CAPTION, caption); } @@ -449,10 +510,15 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, } final String styleName = tab.getStyleName(); - if (styleName != null && styleName.length() != 0) { + if (styleName != null && !styleName.isEmpty()) { target.addAttribute(TabsheetConstants.TAB_STYLE_NAME, styleName); } + final String id = tab.getId(); + if (id != null && !id.isEmpty()) { + target.addAttribute("id", id); + } + target.addAttribute("key", keyMapper.key(component)); if (component.equals(selected)) { target.addAttribute("selected", true); @@ -549,6 +615,11 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, // connector if (selected != null) { selected.markAsDirtyRecursive(); + + Tab tab = getTab(c); + if (tab != null && tab.getDefaultFocusComponent() != null) { + tab.getDefaultFocusComponent().focus(); + } } } @@ -889,6 +960,23 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, public void setClosable(boolean closable); /** + * Set the component that should automatically focused when the tab is + * selected. + * + * @param component + * the component to focus + */ + public void setDefaultFocusComponent(Focusable component); + + /** + * Get the component that should be automatically focused when the tab + * is selected. + * + * @return the focusable component + */ + public Focusable getDefaultFocusComponent(); + + /** * Returns the enabled status for the tab. A disabled tab is shown as * such in the tab bar and cannot be selected. * @@ -932,6 +1020,27 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, public void setIcon(Resource icon); /** + * Sets the icon and alt text for the tab. + * + * @param icon + * the icon to set + */ + public void setIcon(Resource icon, String iconAltText); + + /** + * Gets the icon alt text for the tab. + */ + public String getIconAltText(); + + /** + * Sets the icon alt text for the tab. + * + * @param iconAltText + * the icon to set + */ + public void setIconAltText(String iconAltText); + + /** * Gets the description for the tab. The description can be used to * briefly describe the state of the tab to the user, and is typically * shown as a tooltip when hovering over the tab. @@ -1015,6 +1124,23 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * @see #setStyleName(String) */ public String getStyleName(); + + /** + * Adds an unique id for component that is used in the client-side for + * testing purposes. Keeping identifiers unique is the responsibility of + * the programmer. + * + * @param id + * An alphanumeric id + */ + public void setId(String id); + + /** + * Gets currently set debug identifier + * + * @return current id, null if not set + */ + public String getId(); } /** @@ -1030,6 +1156,9 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, private String description = null; private ErrorMessage componentError = null; private String styleName; + private String id; + private String iconAltText = ""; + private Focusable defaultFocus; public TabSheetTabImpl(String caption, Resource icon) { if (caption == null) { @@ -1061,11 +1190,38 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public void setIcon(Resource icon) { + setIcon(icon, ""); + } + + @Override + public void setIcon(Resource icon, String iconAltText) { this.icon = icon; + this.iconAltText = iconAltText; markAsDirty(); } @Override + public String getIconAltText() { + return iconAltText; + } + + @Override + public void setIconAltText(String iconAltText) { + this.iconAltText = iconAltText; + markAsDirty(); + } + + @Override + public void setDefaultFocusComponent(Focusable defaultFocus) { + this.defaultFocus = defaultFocus; + } + + @Override + public Focusable getDefaultFocusComponent() { + return defaultFocus; + } + + @Override public boolean isEnabled() { return enabled; } @@ -1150,6 +1306,18 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, public String getStyleName() { return styleName; } + + @Override + public void setId(String id) { + this.id = id; + markAsDirty(); + + } + + @Override + public String getId() { + return id; + } } /** @@ -1309,7 +1477,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, */ private static void copyTabMetadata(Tab from, Tab to) { to.setCaption(from.getCaption()); - to.setIcon(from.getIcon()); + to.setIcon(from.getIcon(), from.getIconAltText()); to.setDescription(from.getDescription()); to.setVisible(from.isVisible()); to.setEnabled(from.isEnabled()); diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index 746fa194ac..a292e6b829 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -32,6 +32,9 @@ import com.vaadin.event.Action.Handler; import com.vaadin.event.ActionManager; import com.vaadin.event.MouseEvents.ClickEvent; import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.event.UIEvents.PollEvent; +import com.vaadin.event.UIEvents.PollListener; +import com.vaadin.event.UIEvents.PollNotifier; import com.vaadin.navigator.Navigator; import com.vaadin.server.ClientConnector; import com.vaadin.server.ComponentSizeValidator; @@ -77,7 +80,7 @@ import com.vaadin.util.CurrentInstance; * When a new UI instance is needed, typically because the user opens a URL in a * browser window which points to e.g. {@link VaadinServlet}, all * {@link UIProvider}s registered to the current {@link VaadinSession} are - * queried for the UI class that should be used. The selection is by defaylt + * queried for the UI class that should be used. The selection is by default * based on the <code>UI</code> init parameter from web.xml. * </p> * <p> @@ -95,7 +98,8 @@ import com.vaadin.util.CurrentInstance; * @since 7.0 */ public abstract class UI extends AbstractSingleComponentContainer implements - Action.Container, Action.Notifier, LegacyComponent, Focusable { + Action.Container, Action.Notifier, PollNotifier, LegacyComponent, + Focusable { /** * The application to which this UI belongs @@ -167,10 +171,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements @Override public void poll() { - /* - * No-op. This is only called to cause a server visit to check for - * changes. - */ + fireEvent(new PollEvent(UI.this)); } }; private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() { @@ -222,6 +223,9 @@ public abstract class UI extends AbstractSingleComponentContainer implements private PushConfiguration pushConfiguration = new PushConfigurationImpl( this); + private NotificationConfiguration notificationConfiguration = new NotificationConfigurationImpl( + this); + /** * Creates a new empty UI without a caption. The content of the UI must be * set by calling {@link #setContent(Component)} before using the UI. @@ -551,6 +555,8 @@ public abstract class UI extends AbstractSingleComponentContainer implements private LocaleService localeService = new LocaleService(this, getState(false).localeServiceState); + private String embedId; + /** * This method is used by Component.Focusable objects to request focus to * themselves. Focus renders must be handled at window level (instead of @@ -598,12 +604,19 @@ public abstract class UI extends AbstractSingleComponentContainer implements * the initialization request * @param uiId * the id of the new ui + * @param embedId + * the embed id of this UI, or <code>null</code> if no id is + * known + * + * @see #getUIId() + * @see #getEmbedId() */ - public void doInit(VaadinRequest request, int uiId) { + public void doInit(VaadinRequest request, int uiId, String embedId) { if (this.uiId != -1) { throw new IllegalStateException("UI id has already been defined"); } this.uiId = uiId; + this.embedId = embedId; // Actual theme - used for finding CustomLayout templates theme = request.getParameter("theme"); @@ -1334,6 +1347,15 @@ public abstract class UI extends AbstractSingleComponentContainer implements } /** + * Retrieves the object used for configuring notifications. + * + * @return The instance used for notification configuration + */ + public NotificationConfiguration getNotificationConfiguration() { + return notificationConfiguration; + } + + /** * Retrieves the object used for configuring the loading indicator. * * @return The instance used for configuring the loading indicator @@ -1469,6 +1491,17 @@ public abstract class UI extends AbstractSingleComponentContainer implements return getState(false).pollInterval; } + @Override + public void addPollListener(PollListener listener) { + addListener(EventId.POLL, PollEvent.class, listener, + PollListener.POLL_METHOD); + } + + @Override + public void removePollListener(PollListener listener) { + removeListener(EventId.POLL, PollEvent.class, listener); + } + /** * Retrieves the object used for configuring the push channel. * @@ -1518,4 +1551,18 @@ public abstract class UI extends AbstractSingleComponentContainer implements private static Logger getLogger() { return Logger.getLogger(UI.class.getName()); } + + /** + * Gets a string the uniquely distinguishes this UI instance based on where + * it is embedded. The embed identifier is based on the + * <code>window.name</code> DOM attribute of the browser window where the UI + * is displayed and the id of the div element where the UI is embedded. + * + * @since 7.2 + * @return the embed id for this UI, or <code>null</code> if no id known + */ + public String getEmbedId() { + return embedId; + } + } diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java index c173b401b9..d3afdaacf1 100644 --- a/server/src/com/vaadin/ui/Window.java +++ b/server/src/com/vaadin/ui/Window.java @@ -18,6 +18,9 @@ package com.vaadin.ui; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import com.vaadin.event.FieldEvents.BlurEvent; @@ -33,10 +36,12 @@ import com.vaadin.event.ShortcutAction.ModifierKey; import com.vaadin.event.ShortcutListener; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; +import com.vaadin.shared.Connector; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.window.WindowMode; import com.vaadin.shared.ui.window.WindowServerRpc; import com.vaadin.shared.ui.window.WindowState; +import com.vaadin.shared.ui.window.WindowState.WindowRole; import com.vaadin.util.ReflectTools; /** @@ -238,8 +243,6 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, // Don't do anything if not attached to a UI if (uI != null) { - // focus is restored to the parent window - uI.focus(); // window is removed from the UI uI.removeWindow(this); } @@ -644,7 +647,10 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, /** * Sets window modality. When a modal window is open, components outside - * that window it cannot be accessed. + * that window cannot be accessed. + * <p> + * Keyboard navigation is restricted by blocking the tab key at the top and + * bottom of the window by activating the tab stop function internally. * * @param modal * true if modality is to be turned on @@ -1005,4 +1011,194 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, protected WindowState getState(boolean markAsDirty) { return (WindowState) super.getState(markAsDirty); } + + /** + * Allows to specify which components contain the description for the + * window. Text contained in these components will be read by assistive + * devices when it is opened. + * + * @param connectors + * with the components to use as description + */ + public void setAssistiveDescription(Connector... connectors) { + if (connectors == null) { + throw new IllegalArgumentException( + "Parameter connectors must be non-null"); + } else { + getState().contentDescription = connectors; + } + } + + /** + * Gets the components that are used as assistive description. Text + * contained in these components will be read by assistive devices when the + * window is opened. + * + * @return list of previously set components + */ + public List<Connector> getAssistiveDescription() { + return Collections.unmodifiableList(Arrays + .asList(getState().contentDescription)); + } + + /** + * Sets the accessibility prefix for the window caption. + * + * This prefix is read to assistive device users before the window caption, + * but not visible on the page. + * + * @param prefix + * String that is placed before the window caption + */ + public void setAssistivePrefix(String prefix) { + getState().assistivePrefix = prefix; + } + + /** + * Gets the accessibility prefix for the window caption. + * + * This prefix is read to assistive device users before the window caption, + * but not visible on the page. + * + * @return The accessibility prefix + */ + public String getAssistivePrefix() { + return getState().assistivePrefix; + } + + /** + * Sets the accessibility postfix for the window caption. + * + * This postfix is read to assistive device users after the window caption, + * but not visible on the page. + * + * @param prefix + * String that is placed after the window caption + */ + public void setAssistivePostfix(String assistivePostfix) { + getState().assistivePostfix = assistivePostfix; + } + + /** + * Gets the accessibility postfix for the window caption. + * + * This postfix is read to assistive device users after the window caption, + * but not visible on the page. + * + * @return The accessibility postfix + */ + public String getAssistivePostfix() { + return getState().assistivePostfix; + } + + /** + * Sets the WAI-ARIA role the window. + * + * This role defines how an assistive device handles a window. Available + * roles are alertdialog and dialog (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>). + * + * The default role is dialog. + * + * @param role + * WAI-ARIA role to set for the window + */ + public void setAssistiveRole(WindowRole role) { + getState().role = role; + } + + /** + * Gets the WAI-ARIA role the window. + * + * This role defines how an assistive device handles a window. Available + * roles are alertdialog and dialog (@see <a + * href="http://www.w3.org/TR/2011/CR-wai-aria-20110118/roles">Roles + * Model</a>). + * + * @return WAI-ARIA role set for the window + */ + public WindowRole getAssistiveRole() { + return getState().role; + } + + /** + * Set if it should be prevented to set the focus to a component outside a + * non-modal window with the tab key. + * <p> + * This is meant to help users of assistive devices to not leaving the + * window unintentionally. + * <p> + * For modal windows, this function is activated automatically, while + * preserving the stored value of tabStop. + * + * @param tabStop + * true to keep the focus inside the window when reaching the top + * or bottom, false (default) to allow leaving the window + */ + public void setTabStopEnabled(boolean tabStop) { + getState().assistiveTabStop = tabStop; + } + + /** + * Get if it is prevented to leave a window with the tab key. + * + * @return true when the focus is limited to inside the window, false when + * focus can leave the window + */ + public boolean isTabStopEnabled() { + return getState().assistiveTabStop; + } + + /** + * Sets the message that is provided to users of assistive devices when the + * user reaches the top of the window when leaving a window with the tab key + * is prevented. + * <p> + * This message is not visible on the screen. + * + * @param topMessage + * String provided when the user navigates with Shift-Tab keys to + * the top of the window + */ + public void setTabStopTopAssistiveText(String topMessage) { + getState().assistiveTabStopTopText = topMessage; + } + + /** + * Sets the message that is provided to users of assistive devices when the + * user reaches the bottom of the window when leaving a window with the tab + * key is prevented. + * <p> + * This message is not visible on the screen. + * + * @param bottomMessage + * String provided when the user navigates with the Tab key to + * the bottom of the window + */ + public void setTabStopBottomAssistiveText(String bottomMessage) { + getState().assistiveTabStopBottomText = bottomMessage; + } + + /** + * Gets the message that is provided to users of assistive devices when the + * user reaches the top of the window when leaving a window with the tab key + * is prevented. + * + * @return the top message + */ + public String getTabStopTopAssistiveText() { + return getState().assistiveTabStopTopText; + } + + /** + * Gets the message that is provided to users of assistive devices when the + * user reaches the bottom of the window when leaving a window with the tab + * key is prevented. + * + * @return the bottom message + */ + public String getTabStopBottomAssistiveText() { + return getState().assistiveTabStopBottomText; + } } diff --git a/server/tests/src/com/vaadin/data/util/BeanContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanContainerTest.java index 9037e303a8..2dcbb4aed8 100644 --- a/server/tests/src/com/vaadin/data/util/BeanContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanContainerTest.java @@ -457,4 +457,28 @@ public class BeanContainerTest extends AbstractBeanContainerTest { .getValue()); } + public void testNestedContainerPropertyWithNullBean() { + BeanContainer<String, NestedMethodPropertyTest.Person> container = new BeanContainer<String, NestedMethodPropertyTest.Person>( + NestedMethodPropertyTest.Person.class); + container.setBeanIdProperty("name"); + + container.addBean(new NestedMethodPropertyTest.Person("John", null)); + assertTrue(container + .addNestedContainerProperty("address.postalCodeObject")); + assertTrue(container.addNestedContainerProperty("address.street", true)); + // the nested properties added with allowNullBean setting should return + // null + assertNull(container.getContainerProperty("John", "address.street") + .getValue()); + // nested properties added without allowNullBean setting should throw + // exception + try { + container.getContainerProperty("John", "address.postalCodeObject") + .getValue(); + fail(); + } catch (Exception e) { + // should throw exception + } + } + } diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java index 6b88eb336d..35f09fc8f3 100644 --- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java @@ -10,8 +10,15 @@ import java.util.Map; import junit.framework.Assert; +import org.easymock.Capture; +import org.easymock.EasyMock; + import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; +import com.vaadin.data.util.filter.Compare; /** * Test basic functionality of BeanItemContainer. @@ -714,4 +721,205 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest { .getValue()); } + public void testNestedContainerPropertyWithNullBean() { + BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>( + NestedMethodPropertyTest.Person.class); + NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person( + "John", null); + assertNotNull(container.addBean(john)); + assertTrue(container + .addNestedContainerProperty("address.postalCodeObject")); + assertTrue(container.addNestedContainerProperty("address.street", true)); + // the nested properties added with allowNullBean setting should return + // null + assertNull(container.getContainerProperty(john, "address.street") + .getValue()); + // nested properties added without allowNullBean setting should throw + // exception + try { + container.getContainerProperty(john, "address.postalCodeObject") + .getValue(); + fail(); + } catch (Exception e) { + // should throw exception + } + } + + public void testItemAddedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(bean); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_addItemAt_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAt(1, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAfter(bean, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_amountOfAddedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + + container.addAll(beans); + + assertEquals(2, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(1, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), bean); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + Person secondBean = new Person("John"); + container.addItem(secondBean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondBean); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + container.addItem(new Person("John")); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + BeanItemContainer<Person> container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } } diff --git a/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java b/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java index 640ede8743..d517322010 100644 --- a/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java +++ b/server/tests/src/com/vaadin/data/util/NestedMethodPropertyTest.java @@ -273,6 +273,23 @@ public class NestedMethodPropertyTest extends TestCase { Assert.assertEquals("Joonas", managerNameProperty.getValue()); } + public void testNullNestedPropertyWithAllowNullBeans() { + NestedMethodProperty<String> managerNameProperty = new NestedMethodProperty<String>( + vaadin, "manager.name", true); + NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>( + vaadin, "manager.address.street", true); + + joonas.setAddress(null); + // should return null + Assert.assertNull(streetProperty.getValue()); + + vaadin.setManager(null); + Assert.assertNull(managerNameProperty.getValue()); + vaadin.setManager(joonas); + Assert.assertEquals("Joonas", managerNameProperty.getValue()); + Assert.assertNull(streetProperty.getValue()); + } + public void testMultiLevelNestedPropertySetValue() { NestedMethodProperty<String> managerNameProperty = new NestedMethodProperty<String>( vaadin, "manager.name"); @@ -314,6 +331,20 @@ public class NestedMethodPropertyTest extends TestCase { Assert.assertEquals("Ruukinkatu 2-4", property2.getValue()); } + public void testSerializationWithNullBeansAllowed() throws IOException, + ClassNotFoundException { + vaadin.setManager(null); + NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>( + vaadin, "manager.address.street", true); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ObjectOutputStream(baos).writeObject(streetProperty); + @SuppressWarnings("unchecked") + NestedMethodProperty<String> property2 = (NestedMethodProperty<String>) new ObjectInputStream( + new ByteArrayInputStream(baos.toByteArray())).readObject(); + + Assert.assertNull(property2.getValue()); + } + public void testIsReadOnly() { NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>( vaadin, "manager.address.street"); diff --git a/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java b/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java index 14e70d76d4..0ae76430f6 100644 --- a/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java +++ b/server/tests/src/com/vaadin/data/util/PropertyDescriptorTest.java @@ -52,4 +52,20 @@ public class PropertyDescriptorTest extends TestCase { Property<?> property = pd2.createProperty(new Person("John", null)); Assert.assertEquals("John", property.getValue()); } + + public void testNestedPropertyDescriptorWithNullBeansAllowedSerialization() + throws Exception { + NestedPropertyDescriptor<Person> pd = new NestedPropertyDescriptor<Person>( + "address.street", Person.class, true); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new ObjectOutputStream(baos).writeObject(pd); + @SuppressWarnings("unchecked") + VaadinPropertyDescriptor<Person> pd2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream( + new ByteArrayInputStream(baos.toByteArray())).readObject(); + + Property<?> property = pd2.createProperty(new Person("John", null)); + Assert.assertNull(property.getValue()); + } + } diff --git a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java index 09e5a26c15..5c78965092 100644 --- a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java +++ b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java @@ -4,6 +4,12 @@ import java.util.List; import junit.framework.Assert; +import org.easymock.Capture; +import org.easymock.EasyMock; + +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; public class TestIndexedContainer extends AbstractInMemoryContainerTest { @@ -271,6 +277,113 @@ public class TestIndexedContainer extends AbstractInMemoryContainerTest { counter.assertNone(); } + public void testItemAddedEvent() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItem(); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_IndexOfAddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + container.addItem(); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItemAt(1); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + Object secondItemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondItemId); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + IndexedContainer container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } + // Ticket 8028 public void testGetItemIdsRangeIndexOutOfBounds() { IndexedContainer ic = new IndexedContainer(); diff --git a/server/tests/src/com/vaadin/server/VaadinSessionTest.java b/server/tests/src/com/vaadin/server/VaadinSessionTest.java index 68f198410c..51ae2a2d13 100644 --- a/server/tests/src/com/vaadin/server/VaadinSessionTest.java +++ b/server/tests/src/com/vaadin/server/VaadinSessionTest.java @@ -100,7 +100,7 @@ public class VaadinSessionTest { } }; - ui.doInit(vaadinRequest, session.getNextUIid()); + ui.doInit(vaadinRequest, session.getNextUIid(), null); ui.setSession(session); session.addUI(ui); diff --git a/server/tests/src/com/vaadin/tests/data/bean/BeanToValidate.java b/server/tests/src/com/vaadin/tests/data/bean/BeanToValidate.java index 416563baba..034609764f 100644 --- a/server/tests/src/com/vaadin/tests/data/bean/BeanToValidate.java +++ b/server/tests/src/com/vaadin/tests/data/bean/BeanToValidate.java @@ -4,6 +4,7 @@ import javax.validation.constraints.Digits; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; public class BeanToValidate { @@ -21,6 +22,10 @@ public class BeanToValidate { @Digits(integer = 3, fraction = 2) private String decimals; + @Pattern(regexp = "V*", message = "Must start with letter V") + @Size(min = 3, max = 6, message = "Must contain 3 - 6 letters") + private String nickname; + public String getFirstname() { return firstname; } @@ -53,4 +58,12 @@ public class BeanToValidate { this.decimals = decimals; } + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + } diff --git a/server/tests/src/com/vaadin/tests/data/bean/PersonWithBeanValidationAnnotations.java b/server/tests/src/com/vaadin/tests/data/bean/PersonWithBeanValidationAnnotations.java index 93b2273263..575730d946 100644 --- a/server/tests/src/com/vaadin/tests/data/bean/PersonWithBeanValidationAnnotations.java +++ b/server/tests/src/com/vaadin/tests/data/bean/PersonWithBeanValidationAnnotations.java @@ -8,12 +8,15 @@ import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Past; +import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; public class PersonWithBeanValidationAnnotations { @NotNull @Size(min = 5, max = 20) + @Pattern(regexp = "A.*") private String firstName; + @NotNull private String lastName; diff --git a/server/tests/src/com/vaadin/tests/data/converter/TestStringToBigDecimalConverter.java b/server/tests/src/com/vaadin/tests/data/converter/TestStringToBigDecimalConverter.java new file mode 100644 index 0000000000..5db33691b6 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/data/converter/TestStringToBigDecimalConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2013 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.data.converter; + +import java.math.BigDecimal; +import java.util.Locale; + +import junit.framework.TestCase; + +import com.vaadin.data.util.converter.StringToBigDecimalConverter; + +public class TestStringToBigDecimalConverter extends TestCase { + + StringToBigDecimalConverter converter = new StringToBigDecimalConverter(); + + public void testNullConversion() { + assertEquals(null, + converter.convertToModel(null, BigDecimal.class, null)); + } + + public void testEmptyStringConversion() { + assertEquals(null, converter.convertToModel("", BigDecimal.class, null)); + } + + public void testValueParsing() { + BigDecimal converted = converter.convertToModel("10", BigDecimal.class, + null); + BigDecimal expected = new BigDecimal(10); + assertEquals(expected, converted); + } + + public void testValueFormatting() { + BigDecimal bd = new BigDecimal(12.5); + String expected = "12,5"; + + String converted = converter.convertToPresentation(bd, String.class, + Locale.GERMAN); + assertEquals(expected, converted); + } +} diff --git a/server/tests/src/com/vaadin/tests/data/converter/TestStringToNumberConverter.java b/server/tests/src/com/vaadin/tests/data/converter/TestStringToNumberConverter.java deleted file mode 100644 index 66fc4f6532..0000000000 --- a/server/tests/src/com/vaadin/tests/data/converter/TestStringToNumberConverter.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.vaadin.tests.data.converter; - -import junit.framework.TestCase; - -import com.vaadin.data.util.converter.StringToNumberConverter; - -public class TestStringToNumberConverter extends TestCase { - - StringToNumberConverter converter = new StringToNumberConverter(); - - public void testNullConversion() { - assertEquals(null, converter.convertToModel(null, Number.class, null)); - } - - public void testEmptyStringConversion() { - assertEquals(null, converter.convertToModel("", Number.class, null)); - } - - public void testValueConversion() { - assertEquals(Long.valueOf(10), - converter.convertToModel("10", Number.class, null)); - assertEquals(10.5, converter.convertToModel("10.5", Number.class, null)); - } -} diff --git a/server/tests/src/com/vaadin/tests/server/component/abstractfield/AbsFieldValueConversions.java b/server/tests/src/com/vaadin/tests/server/component/abstractfield/AbsFieldValueConversions.java index a5e825bddb..85116dd152 100644 --- a/server/tests/src/com/vaadin/tests/server/component/abstractfield/AbsFieldValueConversions.java +++ b/server/tests/src/com/vaadin/tests/server/component/abstractfield/AbsFieldValueConversions.java @@ -205,14 +205,15 @@ public class AbsFieldValueConversions extends TestCase { } + // Now specific to Integer because StringToNumberConverter has been removed public static class NumberBean { - private Number number; + private Integer number; - public Number getNumber() { + public Integer getNumber() { return number; } - public void setNumber(Number number) { + public void setNumber(Integer number) { this.number = number; } @@ -239,7 +240,7 @@ public class AbsFieldValueConversions extends TestCase { tf.setPropertyDataSource(new MethodProperty<Number>(nb, "number")); Converter c2 = tf.getConverter(); assertTrue( - "StringToNumber converter is ok for integer types and should stay even though property is changed", + "StringToInteger converter is ok for integer types and should stay even though property is changed", c1 == c2); assertEquals(490, tf.getPropertyDataSource().getValue()); assertEquals("490", tf.getValue()); diff --git a/server/tests/src/com/vaadin/tests/server/validation/TestBeanValidation.java b/server/tests/src/com/vaadin/tests/server/validation/TestBeanValidation.java index e1d08a989b..1d1a3c297e 100644 --- a/server/tests/src/com/vaadin/tests/server/validation/TestBeanValidation.java +++ b/server/tests/src/com/vaadin/tests/server/validation/TestBeanValidation.java @@ -1,7 +1,6 @@ package com.vaadin.tests.server.validation; -import junit.framework.Assert; - +import org.junit.Assert; import org.junit.Test; import com.vaadin.data.Validator.InvalidValueException; @@ -59,6 +58,32 @@ public class TestBeanValidation { } @Test + public void testBeanValidationException_OneValidationError() { + InvalidValueException[] causes = null; + BeanValidator validator = new BeanValidator(BeanToValidate.class, + "lastname"); + try { + validator.validate(null); + } catch (InvalidValueException e) { + causes = e.getCauses(); + } + + Assert.assertEquals(1, causes.length); + } + + @Test + public void testBeanValidationsException_TwoValidationErrors() { + InvalidValueException[] causes = null; + BeanValidator validator = new BeanValidator(BeanToValidate.class, + "nickname"); + try { + validator.validate("A"); + } catch (InvalidValueException e) { + causes = e.getCauses(); + } + + Assert.assertEquals(2, causes.length); + } public void testBeanValidationNotAddedTwice() { // See ticket #11045 BeanFieldGroup<BeanToValidate> fieldGroup = new BeanFieldGroup<BeanToValidate>( |