diff options
Diffstat (limited to 'server')
73 files changed, 3507 insertions, 661 deletions
diff --git a/server/src/com/vaadin/annotations/Widgetset.java b/server/src/com/vaadin/annotations/Widgetset.java index 4cc81b4bd3..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 widget set 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 ef507c5f31..1e053d1091 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/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java index 23f2da53ce..7edcc9719c 100644 --- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.logging.Logger; import com.vaadin.data.Item; import com.vaadin.data.Property; @@ -55,9 +54,6 @@ import com.vaadin.util.ReflectTools; */ public class FieldGroup implements Serializable { - private static final Logger logger = Logger.getLogger(FieldGroup.class - .getName()); - private Item itemDataSource; private boolean buffered = true; @@ -1013,9 +1009,7 @@ public class FieldGroup implements Serializable { */ public Field<?> buildAndBind(String caption, Object propertyId) throws BindException { - Class<?> type = getPropertyType(propertyId); return buildAndBind(caption, propertyId, Field.class); - } /** diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index b19cdd980c..d94588bdc9 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. * 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/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/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/Constants.java b/server/src/com/vaadin/server/Constants.java index 430a5153c0..a39c39fa72 100644 --- a/server/src/com/vaadin/server/Constants.java +++ b/server/src/com/vaadin/server/Constants.java @@ -67,7 +67,7 @@ public interface Constants { // Keep the version number in sync with push/build.xml and other locations // listed in that file - static final String REQUIRED_ATMOSPHERE_RUNTIME_VERSION = "1.0.18.vaadin3"; + static final String REQUIRED_ATMOSPHERE_RUNTIME_VERSION = "2.0.3.vaadin1"; static final String INVALID_ATMOSPHERE_VERSION_WARNING = "\n" + "=================================================================\n" diff --git a/server/src/com/vaadin/server/DefaultErrorHandler.java b/server/src/com/vaadin/server/DefaultErrorHandler.java index 2f46354500..f8b684f1d1 100644 --- a/server/src/com/vaadin/server/DefaultErrorHandler.java +++ b/server/src/com/vaadin/server/DefaultErrorHandler.java @@ -16,11 +16,14 @@ package com.vaadin.server; +import java.lang.reflect.InvocationTargetException; import java.net.SocketException; import java.util.logging.Level; import java.util.logging.Logger; +import com.vaadin.event.ListenerMethod.MethodException; import com.vaadin.server.ClientConnector.ConnectorErrorEvent; +import com.vaadin.server.ServerRpcManager.RpcInvocationException; import com.vaadin.shared.Connector; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.Component; @@ -32,7 +35,7 @@ public class DefaultErrorHandler implements ErrorHandler { } public static void doDefault(ErrorEvent event) { - final Throwable t = event.getThrowable(); + Throwable t = event.getThrowable(); if (t instanceof SocketException) { // Most likely client browser closed socket getLogger().info( @@ -41,6 +44,8 @@ public class DefaultErrorHandler implements ErrorHandler { return; } + t = findRelevantThrowable(t); + // Finds the original source of the error/exception AbstractComponent component = findAbstractComponent(event); if (component != null) { @@ -54,6 +59,40 @@ public class DefaultErrorHandler implements ErrorHandler { getLogger().log(Level.SEVERE, "", t); } + /** + * Vaadin wraps exceptions in its own and due to reflection usage there + * might be also other irrelevant exceptions that make no sense for Vaadin + * users (~developers using Vaadin). This method tries to choose the + * relevant one to be reported. + * + * @since 7.2 + * @param t + * throwable given for default error handler + * @return the throwable that is relevant for Vaadin users + */ + private static Throwable findRelevantThrowable(Throwable t) { + try { + if ((t instanceof RpcInvocationException) + && (t.getCause() instanceof InvocationTargetException)) { + /* + * RpcInvocationException (that always wraps irrelevant + * java.lang.reflect.InvocationTargetException) might only be + * relevant for core Vaadin developers. + */ + return findRelevantThrowable(t.getCause().getCause()); + } else if (t instanceof MethodException) { + /* + * Method exception might only be relevant for core Vaadin + * developers. + */ + return t.getCause(); + } + } catch (Exception e) { + // NOP, just return the original one + } + return t; + } + private static Logger getLogger() { return Logger.getLogger(DefaultErrorHandler.class.getName()); } diff --git a/server/src/com/vaadin/server/FontAwesome.java b/server/src/com/vaadin/server/FontAwesome.java new file mode 100644 index 0000000000..a7f4c7b342 --- /dev/null +++ b/server/src/com/vaadin/server/FontAwesome.java @@ -0,0 +1,447 @@ +/* + * Copyright 2000-2014 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; + +/** + * FontAwesome set of font icons. + * <p> + * Each {@link FontIcon} comes from the FontAwesome font family, which is + * included in the theme.<br/> + * Consider this a starting point: it is unlikely an application needs exactly + * these icons, and all of them, so you might want to consider making a custom + * icon font - either to get other icons, or to minimize the size of the font. + * </p> + * + * @since 7.2 + * @author Vaadin Ltd + * @see http://fortawesome.github.io/Font-Awesome/ + */ +public enum FontAwesome implements FontIcon { + GLASS(0XF000), // + MUSIC(0XF001), // + SEARCH(0XF002), // + ENVELOPE_O(0XF003), // + HEART(0XF004), // + STAR(0XF005), // + STAR_O(0XF006), // + USER(0XF007), // + FILM(0XF008), // + TH_LARGE(0XF009), // + TH(0XF00A), // + TH_LIST(0XF00B), // + CHECK(0XF00C), // + TIMES(0XF00D), // + SEARCH_PLUS(0XF00E), // + SEARCH_MINUS(0XF010), // + POWER_OFF(0XF011), // + SIGNAL(0XF012), // + COG(0XF013), // + TRASH_O(0XF014), // + HOME(0XF015), // + FILE_O(0XF016), // + CLOCK_O(0XF017), // + ROAD(0XF018), // + DOWNLOAD(0XF019), // + ARROW_CIRCLE_O_DOWN(0XF01A), // + ARROW_CIRCLE_O_UP(0XF01B), // + INBOX(0XF01C), // + PLAY_CIRCLE_O(0XF01D), // + REPEAT(0XF01E), // + REFRESH(0XF021), // + LIST_ALT(0XF022), // + LOCK(0XF023), // + FLAG(0XF024), // + HEADPHONES(0XF025), // + VOLUME_OFF(0XF026), // + VOLUME_DOWN(0XF027), // + VOLUME_UP(0XF028), // + QRCODE(0XF029), // + BARCODE(0XF02A), // + TAG(0XF02B), // + TAGS(0XF02C), // + BOOK(0XF02D), // + BOOKMARK(0XF02E), // + PRINT(0XF02F), // + CAMERA(0XF030), // + FONT(0XF031), // + BOLD(0XF032), // + ITALIC(0XF033), // + TEXT_HEIGHT(0XF034), // + TEXT_WIDTH(0XF035), // + ALIGN_LEFT(0XF036), // + ALIGN_CENTER(0XF037), // + ALIGN_RIGHT(0XF038), // + ALIGN_JUSTIFY(0XF039), // + LIST(0XF03A), // + OUTDENT(0XF03B), // + INDENT(0XF03C), // + VIDEO_CAMERA(0XF03D), // + PICTURE_O(0XF03E), // + PENCIL(0XF040), // + MAP_MARKER(0XF041), // + ADJUST(0XF042), // + TINT(0XF043), // + PENCIL_SQUARE_O(0XF044), // + SHARE_SQUARE_O(0XF045), // + CHECK_SQUARE_O(0XF046), // + ARROWS(0XF047), // + STEP_BACKWARD(0XF048), // + FAST_BACKWARD(0XF049), // + BACKWARD(0XF04A), // + PLAY(0XF04B), // + PAUSE(0XF04C), // + STOP(0XF04D), // + FORWARD(0XF04E), // + FAST_FORWARD(0XF050), // + STEP_FORWARD(0XF051), // + EJECT(0XF052), // + CHEVRON_LEFT(0XF053), // + CHEVRON_RIGHT(0XF054), // + PLUS_CIRCLE(0XF055), // + MINUS_CIRCLE(0XF056), // + TIMES_CIRCLE(0XF057), // + CHECK_CIRCLE(0XF058), // + QUESTION_CIRCLE(0XF059), // + INFO_CIRCLE(0XF05A), // + CROSSHAIRS(0XF05B), // + TIMES_CIRCLE_O(0XF05C), // + CHECK_CIRCLE_O(0XF05D), // + BAN(0XF05E), // + ARROW_LEFT(0XF060), // + ARROW_RIGHT(0XF061), // + ARROW_UP(0XF062), // + ARROW_DOWN(0XF063), // + SHARE(0XF064), // + EXPAND(0XF065), // + COMPRESS(0XF066), // + PLUS(0XF067), // + MINUS(0XF068), // + ASTERISK(0XF069), // + EXCLAMATION_CIRCLE(0XF06A), // + GIFT(0XF06B), // + LEAF(0XF06C), // + FIRE(0XF06D), // + EYE(0XF06E), // + EYE_SLASH(0XF070), // + EXCLAMATION_TRIANGLE(0XF071), // + PLANE(0XF072), // + CALENDAR(0XF073), // + RANDOM(0XF074), // + COMMENT(0XF075), // + MAGNET(0XF076), // + CHEVRON_UP(0XF077), // + CHEVRON_DOWN(0XF078), // + RETWEET(0XF079), // + SHOPPING_CART(0XF07A), // + FOLDER(0XF07B), // + FOLDER_OPEN(0XF07C), // + ARROWS_V(0XF07D), // + ARROWS_H(0XF07E), // + BAR_CHART_O(0XF080), // + TWITTER_SQUARE(0XF081), // + FACEBOOK_SQUARE(0XF082), // + CAMERA_RETRO(0XF083), // + KEY(0XF084), // + COGS(0XF085), // + COMMENTS(0XF086), // + THUMBS_O_UP(0XF087), // + THUMBS_O_DOWN(0XF088), // + STAR_HALF(0XF089), // + HEART_O(0XF08A), // + SIGN_OUT(0XF08B), // + LINKEDIN_SQUARE(0XF08C), // + THUMB_TACK(0XF08D), // + EXTERNAL_LINK(0XF08E), // + SIGN_IN(0XF090), // + TROPHY(0XF091), // + GITHUB_SQUARE(0XF092), // + UPLOAD(0XF093), // + LEMON_O(0XF094), // + PHONE(0XF095), // + SQUARE_O(0XF096), // + BOOKMARK_O(0XF097), // + PHONE_SQUARE(0XF098), // + TWITTER(0XF099), // + FACEBOOK(0XF09A), // + GITHUB(0XF09B), // + UNLOCK(0XF09C), // + CREDIT_CARD(0XF09D), // + RSS(0XF09E), // + HDD_O(0XF0A0), // + BULLHORN(0XF0A1), // + BELL(0XF0F3), // + CERTIFICATE(0XF0A3), // + HAND_O_RIGHT(0XF0A4), // + HAND_O_LEFT(0XF0A5), // + HAND_O_UP(0XF0A6), // + HAND_O_DOWN(0XF0A7), // + ARROW_CIRCLE_LEFT(0XF0A8), // + ARROW_CIRCLE_RIGHT(0XF0A9), // + ARROW_CIRCLE_UP(0XF0AA), // + ARROW_CIRCLE_DOWN(0XF0AB), // + GLOBE(0XF0AC), // + WRENCH(0XF0AD), // + TASKS(0XF0AE), // + FILTER(0XF0B0), // + BRIEFCASE(0XF0B1), // + ARROWS_ALT(0XF0B2), // + USERS(0XF0C0), // + LINK(0XF0C1), // + CLOUD(0XF0C2), // + FLASK(0XF0C3), // + SCISSORS(0XF0C4), // + FILES_O(0XF0C5), // + PAPERCLIP(0XF0C6), // + FLOPPY_O(0XF0C7), // + SQUARE(0XF0C8), // + BARS(0XF0C9), // + LIST_UL(0XF0CA), // + LIST_OL(0XF0CB), // + STRIKETHROUGH(0XF0CC), // + UNDERLINE(0XF0CD), // + TABLE(0XF0CE), // + MAGIC(0XF0D0), // + TRUCK(0XF0D1), // + PINTEREST(0XF0D2), // + PINTEREST_SQUARE(0XF0D3), // + GOOGLE_PLUS_SQUARE(0XF0D4), // + GOOGLE_PLUS(0XF0D5), // + MONEY(0XF0D6), // + CARET_DOWN(0XF0D7), // + CARET_UP(0XF0D8), // + CARET_LEFT(0XF0D9), // + CARET_RIGHT(0XF0DA), // + COLUMNS(0XF0DB), // + SORT(0XF0DC), // + SORT_ASC(0XF0DD), // + SORT_DESC(0XF0DE), // + ENVELOPE(0XF0E0), // + LINKEDIN(0XF0E1), // + UNDO(0XF0E2), // + GAVEL(0XF0E3), // + TACHOMETER(0XF0E4), // + COMMENT_O(0XF0E5), // + COMMENTS_O(0XF0E6), // + BOLT(0XF0E7), // + SITEMAP(0XF0E8), // + UMBRELLA(0XF0E9), // + CLIPBOARD(0XF0EA), // + LIGHTBULB_O(0XF0EB), // + EXCHANGE(0XF0EC), // + CLOUD_DOWNLOAD(0XF0ED), // + CLOUD_UPLOAD(0XF0EE), // + USER_MD(0XF0F0), // + STETHOSCOPE(0XF0F1), // + SUITCASE(0XF0F2), // + BELL_O(0XF0A2), // + COFFEE(0XF0F4), // + CUTLERY(0XF0F5), // + FILE_TEXT_O(0XF0F6), // + BUILDING_O(0XF0F7), // + HOSPITAL_O(0XF0F8), // + AMBULANCE(0XF0F9), // + MEDKIT(0XF0FA), // + FIGHTER_JET(0XF0FB), // + BEER(0XF0FC), // + H_SQUARE(0XF0FD), // + PLUS_SQUARE(0XF0FE), // + ANGLE_DOUBLE_LEFT(0XF100), // + ANGLE_DOUBLE_RIGHT(0XF101), // + ANGLE_DOUBLE_UP(0XF102), // + ANGLE_DOUBLE_DOWN(0XF103), // + ANGLE_LEFT(0XF104), // + ANGLE_RIGHT(0XF105), // + ANGLE_UP(0XF106), // + ANGLE_DOWN(0XF107), // + DESKTOP(0XF108), // + LAPTOP(0XF109), // + TABLET(0XF10A), // + MOBILE(0XF10B), // + CIRCLE_O(0XF10C), // + QUOTE_LEFT(0XF10D), // + QUOTE_RIGHT(0XF10E), // + SPINNER(0XF110), // + CIRCLE(0XF111), // + REPLY(0XF112), // + GITHUB_ALT(0XF113), // + FOLDER_O(0XF114), // + FOLDER_OPEN_O(0XF115), // + SMILE_O(0XF118), // + FROWN_O(0XF119), // + MEH_O(0XF11A), // + GAMEPAD(0XF11B), // + KEYBOARD_O(0XF11C), // + FLAG_O(0XF11D), // + FLAG_CHECKERED(0XF11E), // + TERMINAL(0XF120), // + CODE(0XF121), // + REPLY_ALL(0XF122), // + MAIL_REPLY_ALL(0XF122), // + STAR_HALF_O(0XF123), // + LOCATION_ARROW(0XF124), // + CROP(0XF125), // + CODE_FORK(0XF126), // + CHAIN_BROKEN(0XF127), // + QUESTION(0XF128), // + INFO(0XF129), // + EXCLAMATION(0XF12A), // + SUPERSCRIPT(0XF12B), // + SUBSCRIPT(0XF12C), // + ERASER(0XF12D), // + PUZZLE_PIECE(0XF12E), // + MICROPHONE(0XF130), // + MICROPHONE_SLASH(0XF131), // + SHIELD(0XF132), // + CALENDAR_O(0XF133), // + FIRE_EXTINGUISHER(0XF134), // + ROCKET(0XF135), // + MAXCDN(0XF136), // + CHEVRON_CIRCLE_LEFT(0XF137), // + CHEVRON_CIRCLE_RIGHT(0XF138), // + CHEVRON_CIRCLE_UP(0XF139), // + CHEVRON_CIRCLE_DOWN(0XF13A), // + HTML5(0XF13B), // + CSS3(0XF13C), // + ANCHOR(0XF13D), // + UNLOCK_ALT(0XF13E), // + BULLSEYE(0XF140), // + ELLIPSIS_H(0XF141), // + ELLIPSIS_V(0XF142), // + RSS_SQUARE(0XF143), // + PLAY_CIRCLE(0XF144), // + TICKET(0XF145), // + MINUS_SQUARE(0XF146), // + MINUS_SQUARE_O(0XF147), // + LEVEL_UP(0XF148), // + LEVEL_DOWN(0XF149), // + CHECK_SQUARE(0XF14A), // + PENCIL_SQUARE(0XF14B), // + EXTERNAL_LINK_SQUARE(0XF14C), // + SHARE_SQUARE(0XF14D), // + COMPASS(0XF14E), // + CARET_SQUARE_O_DOWN(0XF150), // + CARET_SQUARE_O_UP(0XF151), // + CARET_SQUARE_O_RIGHT(0XF152), // + EUR(0XF153), // + GBP(0XF154), // + USD(0XF155), // + INR(0XF156), // + JPY(0XF157), // + RUB(0XF158), // + KRW(0XF159), // + BTC(0XF15A), // + FILE(0XF15B), // + FILE_TEXT(0XF15C), // + SORT_ALPHA_ASC(0XF15D), // + SORT_ALPHA_DESC(0XF15E), // + SORT_AMOUNT_ASC(0XF160), // + SORT_AMOUNT_DESC(0XF161), // + SORT_NUMERIC_ASC(0XF162), // + SORT_NUMERIC_DESC(0XF163), // + THUMBS_UP(0XF164), // + THUMBS_DOWN(0XF165), // + YOUTUBE_SQUARE(0XF166), // + YOUTUBE(0XF167), // + XING(0XF168), // + XING_SQUARE(0XF169), // + YOUTUBE_PLAY(0XF16A), // + DROPBOX(0XF16B), // + STACK_OVERFLOW(0XF16C), // + INSTAGRAM(0XF16D), // + FLICKR(0XF16E), // + ADN(0XF170), // + BITBUCKET(0XF171), // + BITBUCKET_SQUARE(0XF172), // + TUMBLR(0XF173), // + TUMBLR_SQUARE(0XF174), // + LONG_ARROW_DOWN(0XF175), // + LONG_ARROW_UP(0XF176), // + LONG_ARROW_LEFT(0XF177), // + LONG_ARROW_RIGHT(0XF178), // + APPLE(0XF179), // + WINDOWS(0XF17A), // + ANDROID(0XF17B), // + LINUX(0XF17C), // + DRIBBBLE(0XF17D), // + SKYPE(0XF17E), // + FOURSQUARE(0XF180), // + TRELLO(0XF181), // + FEMALE(0XF182), // + MALE(0XF183), // + GITTIP(0XF184), // + SUN_O(0XF185), // + MOON_O(0XF186), // + ARCHIVE(0XF187), // + BUG(0XF188), // + VK(0XF189), // + WEIBO(0XF18A), // + RENREN(0XF18B), // + PAGELINES(0XF18C), // + STACK_EXCHANGE(0XF18D), // + ARROW_CIRCLE_O_RIGHT(0XF18E), // + ARROW_CIRCLE_O_LEFT(0XF190), // + CARET_SQUARE_O_LEFT(0XF191), // + DOT_CIRCLE_O(0XF192), // + WHEELCHAIR(0XF193), // + VIMEO_SQUARE(0XF194), // + TRY(0XF195), // + PLUS_SQUARE_O(0XF196); + + private static final String fontFamily = "FontAwesome"; + private int codepoint; + + FontAwesome(int codepoint) { + this.codepoint = codepoint; + } + + /** + * Unsupported: {@link FontIcon} does not have a MIME type and is not a + * {@link Resource} that can be used in a context where a MIME type would be + * needed. + */ + @Override + public String getMIMEType() { + throw new UnsupportedOperationException(FontIcon.class.getSimpleName() + + " should not be used where a MIME type is needed."); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.FontIcon#getFontFamily() + */ + @Override + public String getFontFamily() { + return fontFamily; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.FontIcon#getCodepoint() + */ + @Override + public int getCodepoint() { + return codepoint; + } + + @Override + public String getHtml() { + return "<span class=\"v-icon\" style=\"font-family: " + fontFamily + + ";\">&#x" + Integer.toHexString(codepoint) + ";</span>"; + } + +} diff --git a/server/src/com/vaadin/server/FontIcon.java b/server/src/com/vaadin/server/FontIcon.java new file mode 100644 index 0000000000..45279f2c44 --- /dev/null +++ b/server/src/com/vaadin/server/FontIcon.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2014 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 com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.ui.Label; + +/** + * A font icon is a type of icon that is made by displaying one character from a + * specially constructed font containing icons ("icon font"). + * <p> + * {@link FontIcon} is a custom resource type which uses the URI scheme + * <code>fonticon://<fontfamily>/<codepoint></code> to reference a + * specific icon from a specific icon font. <br/> + * </p> + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface FontIcon extends Resource { + /** + * Returns the name (font family) of the font from which this icon comes. + * The name is used to apply the correct font where the icon is used. + * + * @since 7.2 + * @return + */ + public String getFontFamily(); + + /** + * Returns the unicode codepoint (character location) for this icon within + * the font given in {@link #getFontFamily()}. + * <p> + * For example, 0x0021 would in a regular font be the codepoint for the + * exclamation-point character.<br/> + * When constructing icon fonts, it might be a good idea to use the + * codepoints in the "Private use area", from 0xE000 0xF8FF. + * </p> + * + * @since 7.2 + * @return + */ + public int getCodepoint(); + + /** + * Returns HTML that can be used to display the icon in places where HTML + * can be used, such as a {@link Label} with {@link ContentMode#HTML}. + * + * + * @since 7.2 + * @return HTML needed to display icon + */ + public String getHtml(); +} diff --git a/server/src/com/vaadin/server/JsonCodec.java b/server/src/com/vaadin/server/JsonCodec.java index d533ed99f3..d05922b40d 100644 --- a/server/src/com/vaadin/server/JsonCodec.java +++ b/server/src/com/vaadin/server/JsonCodec.java @@ -31,6 +31,7 @@ import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -45,6 +46,8 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import com.vaadin.server.communication.DateSerializer; +import com.vaadin.server.communication.JSONSerializer; import com.vaadin.shared.Connector; import com.vaadin.shared.JsonConstants; import com.vaadin.shared.communication.UidlValue; @@ -176,6 +179,11 @@ public class JsonCodec implements Serializable { */ private static Map<String, Class<?>> transportTypeToType = new HashMap<String, Class<?>>(); + private static Map<Class<?>, JSONSerializer<?>> customSerializers = new HashMap<Class<?>, JSONSerializer<?>>(); + static { + customSerializers.put(Date.class, new DateSerializer()); + } + static { registerType(String.class, JsonConstants.VTYPE_STRING); registerType(Connector.class, JsonConstants.VTYPE_CONNECTOR); @@ -283,6 +291,9 @@ public class JsonCodec implements Serializable { Class<?> classForType = getClassForType(targetType); return decodeEnum(classForType.asSubclass(Enum.class), (String) value); + } else if (customSerializers.containsKey(getClassForType(targetType))) { + return customSerializers.get(getClassForType(targetType)) + .deserialize(targetType, value, connectorTracker); } else { return decodeObject(targetType, (JSONObject) value, connectorTracker); @@ -606,7 +617,7 @@ public class JsonCodec implements Serializable { return decodedObject; } catch (Exception e) { - throw new JSONException(e); + throw new JSONException(e.getMessage()); } } @@ -676,6 +687,10 @@ public class JsonCodec implements Serializable { return encodeEnum((Enum<?>) value, connectorTracker); } else if (value instanceof JSONArray || value instanceof JSONObject) { return new EncodeResult(value); + } else if (customSerializers.containsKey(value.getClass())) { + JSONSerializer serializer = customSerializers.get(value.getClass()); + return new EncodeResult(serializer.serialize(value, + connectorTracker)); } else if (valueType instanceof Class<?>) { // Any object that we do not know how to encode we encode by looping // through fields @@ -750,7 +765,7 @@ public class JsonCodec implements Serializable { } } catch (Exception e) { // TODO: Should exceptions be handled in a different way? - throw new JSONException(e); + throw new JSONException(e.getMessage()); } return new EncodeResult(encoded, diff); } diff --git a/server/src/com/vaadin/server/LegacyCommunicationManager.java b/server/src/com/vaadin/server/LegacyCommunicationManager.java index ad662cf6df..8d61968b47 100644 --- a/server/src/com/vaadin/server/LegacyCommunicationManager.java +++ b/server/src/com/vaadin/server/LegacyCommunicationManager.java @@ -316,8 +316,6 @@ public class LegacyCommunicationManager implements Serializable { private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>(); private int nextTypeKey = 0; - private BootstrapHandler bootstrapHandler; - /** * @deprecated As of 7.1. Will be removed in the future. */ diff --git a/server/src/com/vaadin/server/Page.java b/server/src/com/vaadin/server/Page.java index 037d8e8352..19b84381d5 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. * @@ -1119,12 +1133,15 @@ public class Page implements Serializable { /** * Sets the page title. The page title is displayed by the browser e.g. as * the title of the browser window or as the title of the tab. + * <p> + * If the title is set to null, it will not left as-is. Set to empty string + * to clear the title. * * @param title - * the new page title to set + * the 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/ResourceReference.java b/server/src/com/vaadin/server/ResourceReference.java index 6747dd2b74..4bc8febd72 100644 --- a/server/src/com/vaadin/server/ResourceReference.java +++ b/server/src/com/vaadin/server/ResourceReference.java @@ -67,6 +67,13 @@ public class ResourceReference extends URLReference { final String uri = "theme://" + ((ThemeResource) resource).getResourceId(); return uri; + } else if (resource instanceof FontIcon) { + // fonticon://[font-family]/[codepoint] + final FontIcon icon = (FontIcon) resource; + final String uri = ApplicationConstants.FONTICON_PROTOCOL_PREFIX + + urlEncode(icon.getFontFamily()) + "/" + + Integer.toHexString(icon.getCodepoint()); + return uri; } else { throw new RuntimeException(getClass().getSimpleName() + " does not support resources of type: " 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..6cf30e85e9 100644 --- a/server/src/com/vaadin/server/VaadinPortlet.java +++ b/server/src/com/vaadin/server/VaadinPortlet.java @@ -365,7 +365,6 @@ public class VaadinPortlet extends GenericPortlet implements Constants, if (request instanceof RenderRequest) { return RequestType.RENDER; } else if (request instanceof ResourceRequest) { - ResourceRequest resourceRequest = (ResourceRequest) request; if (ServletPortletHelper.isUIDLRequest(vaadinRequest)) { return RequestType.UIDL; } else if (PortletUIInitHandler.isUIInitRequest(vaadinRequest)) { @@ -503,6 +502,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 eda794438f..c926efc67c 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}. */ @@ -1568,9 +1574,9 @@ public abstract class VaadinService implements Serializable { meta.put("appError", appError); JSONObject json = new JSONObject(); - json.put("changes", Collections.EMPTY_LIST); - json.put("resources", Collections.EMPTY_MAP); - json.put("locales", Collections.EMPTY_LIST); + json.put("changes", new JSONObject()); + json.put("resources", new JSONObject()); + json.put("locales", new JSONObject()); json.put("meta", meta); returnString = json.toString(); } catch (JSONException e) { @@ -1690,23 +1696,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 @@ -1729,6 +1718,8 @@ public abstract class VaadinService implements Serializable { } catch (InterruptedException e) { // Just ignore } + + return future; } /** @@ -1775,4 +1766,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..265d18b859 100644 --- a/server/src/com/vaadin/server/VaadinSession.java +++ b/server/src/com/vaadin/server/VaadinSession.java @@ -22,12 +22,15 @@ import java.io.Serializable; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Queue; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; @@ -43,7 +46,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 +207,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(); @@ -424,6 +426,32 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } /** + * Retrieves all {@link VaadinSession}s which are stored in the given HTTP + * session + * + * @since 7.2 + * @param httpSession + * the HTTP session + * @return the found VaadinSessions + */ + public static Collection<VaadinSession> getAllSessions( + HttpSession httpSession) { + Set<VaadinSession> sessions = new HashSet<VaadinSession>(); + Enumeration<String> attributeNames = httpSession.getAttributeNames(); + + while (attributeNames.hasMoreElements()) { + String attributeName = attributeNames.nextElement(); + if (attributeName.startsWith(VaadinSession.class.getName() + ".")) { + Object value = httpSession.getAttribute(attributeName); + if (value instanceof VaadinSession) { + sessions.add((VaadinSession) value); + } + } + } + return sessions; + } + + /** * Removes this VaadinSession from the HTTP session. * * @param service @@ -828,10 +856,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 +969,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 +992,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 +1116,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 +1132,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 +1357,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/AtmospherePushConnection.java b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java index b9d4955b12..56dd576403 100644 --- a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java +++ b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java @@ -22,22 +22,16 @@ import java.io.Serializable; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.logging.Level; -import java.util.logging.Logger; import org.atmosphere.cpr.AtmosphereResource; import org.atmosphere.cpr.AtmosphereResource.TRANSPORT; -import org.json.JSONException; import com.vaadin.shared.communication.PushConstants; import com.vaadin.ui.UI; /** - * {@link PushConnection} implementation using the Atmosphere push support that - * is by default included in Vaadin. + * A {@link PushConnection} implementation using the Atmosphere push support + * that is by default included in Vaadin. * * @author Vaadin Ltd * @since 7.1 @@ -92,55 +86,84 @@ public class AtmospherePushConnection implements PushConnection { } } + protected enum State { + /** + * Not connected. Trying to push will set the connection state to + * PUSH_PENDING or RESPONSE_PENDING and defer sending the message until + * a connection is established. + */ + DISCONNECTED, + + /** + * Not connected. An asynchronous push is pending the opening of the + * connection. + */ + PUSH_PENDING, + + /** + * Not connected. A response to a client request is pending the opening + * of the connection. + */ + RESPONSE_PENDING, + + /** + * Connected. Messages can be sent through the connection. + */ + CONNECTED; + } + + private State state = State.DISCONNECTED; private UI ui; private AtmosphereResource resource; - private Future<String> outgoingMessage; private FragmentedMessage incomingMessage; - public AtmospherePushConnection(UI ui, AtmosphereResource resource) { + public AtmospherePushConnection(UI ui) { this.ui = ui; - this.resource = resource; } @Override public void push() { - assert isConnected(); - try { - push(true); - } catch (IOException e) { - // TODO Error handling - throw new RuntimeException("Push failed", e); - } + push(true); } /** - * Pushes pending state changes and client RPC calls to the client. + * Pushes pending state changes and client RPC calls to the client. If + * {@code isConnected()} is false, defers the push until a connection is + * established. * * @param async * True if this push asynchronously originates from the server, * false if it is a response to a client request. - * @throws IOException */ - protected void push(boolean async) throws IOException { - Writer writer = new StringWriter(); - try { - new UidlWriter().write(getUI(), writer, false, async); - } catch (JSONException e) { - throw new IOException("Error writing UIDL", e); + public void push(boolean async) { + if (!isConnected()) { + if (async && state != State.RESPONSE_PENDING) { + state = State.PUSH_PENDING; + } else { + state = State.RESPONSE_PENDING; + } + } else { + try { + Writer writer = new StringWriter(); + new UidlWriter().write(getUI(), writer, false, async); + sendMessage("for(;;);[{" + writer.toString() + "}]"); + } catch (Exception e) { + throw new RuntimeException("Push failed", e); + } } - sendMessage("for(;;);[{" + writer.toString() + "}]"); } /** - * Sends the given message to the current client. + * Sends the given message to the current client. Cannot be called if + * {@isConnected()} is false. * * @param message * The message to send */ void sendMessage(String message) { + assert (isConnected()); // "Broadcast" the changes to the single client only - outgoingMessage = getResource().getBroadcaster().broadcast(message, - getResource()); + getResource().getBroadcaster().broadcast(message, getResource()); } /** @@ -157,7 +180,7 @@ public class AtmospherePushConnection implements PushConnection { */ protected Reader receiveMessage(Reader reader) throws IOException { - if (resource.transport() != TRANSPORT.WEBSOCKET) { + if (resource == null || resource.transport() != TRANSPORT.WEBSOCKET) { return reader; } @@ -179,9 +202,37 @@ public class AtmospherePushConnection implements PushConnection { @Override public boolean isConnected() { - return resource != null - && resource.getBroadcaster().getAtmosphereResources() - .contains(resource); + assert (state == State.CONNECTED) ^ (resource == null); + return state == State.CONNECTED; + } + + /** + * Associates this {@code AtmospherePushConnection} with the given + * {@AtmosphereResource} representing an established + * push connection. If already connected, calls {@link #disconnect()} first. + * If there is a deferred push, carries it out via the new connection. + * + * @since 7.2 + */ + public void connect(AtmosphereResource resource) { + + assert resource != null; + assert resource != this.resource; + + if (isConnected()) { + disconnect(); + } + + this.resource = resource; + State oldState = state; + state = State.CONNECTED; + + if (oldState == State.PUSH_PENDING + || oldState == State.RESPONSE_PENDING) { + // Sending a "response" message (async=false) also takes care of a + // pending push, but not vice versa + push(oldState == State.PUSH_PENDING); + } } /** @@ -202,33 +253,8 @@ public class AtmospherePushConnection implements PushConnection { @Override public void disconnect() { assert isConnected(); - - if (outgoingMessage != null) { - // Wait for the last message to be sent before closing the - // connection (assumes that futures are completed in order) - try { - outgoingMessage.get(1000, TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - getLogger() - .log(Level.INFO, - "Timeout waiting for messages to be sent to client before disconnect"); - } catch (Exception e) { - getLogger() - .log(Level.INFO, - "Error waiting for messages to be sent to client before disconnect"); - } - outgoingMessage = null; - } - resource.resume(); resource = null; - } - - /** - * @since - * @return - */ - private static Logger getLogger() { - return Logger.getLogger(AtmospherePushConnection.class.getName()); + state = State.DISCONNECTED; } } diff --git a/server/src/com/vaadin/server/communication/DateSerializer.java b/server/src/com/vaadin/server/communication/DateSerializer.java new file mode 100644 index 0000000000..9179eb922b --- /dev/null +++ b/server/src/com/vaadin/server/communication/DateSerializer.java @@ -0,0 +1,42 @@ +/* + * 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.communication; + +import java.lang.reflect.Type; +import java.util.Date; + +import com.vaadin.ui.ConnectorTracker; + +/** + * Server side serializer/deserializer for java.util.Date + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class DateSerializer implements JSONSerializer<Date> { + + @Override + public Date deserialize(Type type, Object jsonValue, + ConnectorTracker connectorTracker) { + return new Date(Long.valueOf(String.valueOf(jsonValue))); + } + + @Override + public Object serialize(Date value, ConnectorTracker connectorTracker) { + return value.getTime(); + } + +} 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/JSONSerializer.java b/server/src/com/vaadin/server/communication/JSONSerializer.java new file mode 100644 index 0000000000..fe609c70b6 --- /dev/null +++ b/server/src/com/vaadin/server/communication/JSONSerializer.java @@ -0,0 +1,72 @@ +/* + * 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.communication; + +import java.lang.reflect.Type; + +import com.vaadin.ui.ConnectorTracker; + +/** + * Implementors of this interface knows how to serialize an Object of a given + * type to JSON and how to deserialize the JSON back into an object. + * <p> + * The {@link #serialize(Object, ConnectorTracker)} and + * {@link #deserialize(Type, Object, ConnectorTracker)} methods must be + * symmetric so they can be chained and produce the original result (or an equal + * result). + * <p> + * Each {@link JSONSerializer} implementation can handle an object of a single + * type. + * <p> + * This is the server side interface, see + * com.vaadin.client.communication.JSONSerializer for the client side interface. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface JSONSerializer<T> { + /** + * Creates and deserializes an object received from the client. Must be + * compatible with {@link #serialize(Object, ConnectorTracker)} and also + * with the client side com.vaadin.client.communication.JSONSerializer. + * <p> + * The json parameter is of type Object as org.json JSON classes have no + * other common super class + * + * @param type + * The expected return type + * @param jsonValue + * the value from the JSON + * @param connectorTracker + * the connector tracker instance for the UI + * @return A deserialized object + */ + T deserialize(Type type, Object jsonValue, ConnectorTracker connectorTracker); + + /** + * Serialize the given object into JSON. Must be compatible with + * {@link #deserialize(Object, connectorTracker)} and the client side + * com.vaadin.client.communication.JSONSerializer + * + * @param value + * The object to serialize + * @param connectorTracker + * The connector tracker instance for the UI + * @return A JSON serialized version of the object + */ + Object serialize(T value, ConnectorTracker connectorTracker); + +} diff --git a/server/src/com/vaadin/server/communication/PushConnection.java b/server/src/com/vaadin/server/communication/PushConnection.java index 7f78d1d48e..cab3c94824 100644 --- a/server/src/com/vaadin/server/communication/PushConnection.java +++ b/server/src/com/vaadin/server/communication/PushConnection.java @@ -20,7 +20,12 @@ import com.vaadin.ui.UI; /** * Represents a bidirectional ("push") connection between a single UI and its - * client-side. + * client-side. A single {@code PushConnection} instance is bound to a UI as + * long as push is enabled in that UI, even if the actual connection is + * momentarily dropped either due to a network failure or as a normal part of + * the transport mechanism. + * <p> + * This interface is an internal API, only meant to be used by the framework. * * @author Vaadin Ltd * @since 7.1 @@ -28,9 +33,10 @@ import com.vaadin.ui.UI; public interface PushConnection { /** - * Pushes pending state changes and client RPC calls to the client. Cannot - * be called if {@link #isConnected()} is false. It is NOT safe to invoke - * this method if not holding the session lock. + * Pushes pending state changes and client RPC calls to the client. Can be + * called even if {@link #isConnected()} is false; the push will be deferred + * until a connection is available. It is NOT safe to invoke this method if + * not holding the session lock. * <p> * This is internal API; please use {@link UI#push()} instead. */ diff --git a/server/src/com/vaadin/server/communication/PushHandler.java b/server/src/com/vaadin/server/communication/PushHandler.java index 09428e47a9..99aff3780f 100644 --- a/server/src/com/vaadin/server/communication/PushHandler.java +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -19,7 +19,6 @@ package com.vaadin.server.communication; import java.io.IOException; import java.io.Reader; import java.io.Writer; -import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -43,7 +42,6 @@ import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinServletRequest; import com.vaadin.server.VaadinServletService; import com.vaadin.server.VaadinSession; -import com.vaadin.server.WebBrowser; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; @@ -75,8 +73,8 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter @Override public void run(AtmosphereResource resource, UI ui) throws IOException { getLogger().log(Level.FINER, - "New push connection with transport {0}", - resource.transport()); + "New push connection for resource {0} with transport {1}", + new Object[] { resource.uuid(), resource.transport() }); resource.addEventListener(PushHandler.this); @@ -84,14 +82,6 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter VaadinSession session = ui.getSession(); if (resource.transport() == TRANSPORT.STREAMING) { - // IE8 requires a longer padding to work properly if the - // initial message is small (#11573). Chrome does not work - // without the original padding... - WebBrowser browser = session.getBrowser(); - if (browser.isIE() && browser.getBrowserMajorVersion() == 8) { - resource.padding(LONG_PADDING); - } - // Must ensure that the streaming response contains // "Connection: close", otherwise iOS 6 will wait for the // response to this request before sending another request to @@ -115,10 +105,9 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter resource.suspend(); - AtmospherePushConnection connection = new AtmospherePushConnection( - ui, resource); - - ui.setPushConnection(connection); + AtmospherePushConnection connection = getConnectionForUI(ui); + assert (connection != null); + connection.connect(resource); } }; @@ -182,11 +171,11 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter @Override public void run(AtmosphereResource resource, UI ui) throws IOException { PushMode pushMode = ui.getPushConfiguration().getPushMode(); - AtmospherePushConnection pushConnection = getConnectionForUI(ui); + AtmospherePushConnection connection = getConnectionForUI(ui); String id = resource.uuid(); - if (pushConnection == null) { + if (connection == null) { getLogger() .log(Level.WARNING, "Could not find push connection to close: {0} with transport {1}", @@ -209,19 +198,11 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter "Connection unexpectedly closed for resource {0} with transport {1}", new Object[] { id, resource.transport() }); } - ui.setPushConnection(null); + connection.disconnect(); } } }; - private static final String LONG_PADDING; - - static { - char[] array = new char[4096]; - Arrays.fill(array, '-'); - LONG_PADDING = String.copyValueOf(array); - - } private VaadinServletService service; public PushHandler(VaadinServletService service) { @@ -351,10 +332,10 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter private static AtmospherePushConnection getConnectionForUI(UI ui) { PushConnection pushConnection = ui.getPushConnection(); if (pushConnection instanceof AtmospherePushConnection) { - assert pushConnection.isConnected(); return (AtmospherePushConnection) pushConnection; + } else { + return null; } - return null; } @Override @@ -391,7 +372,7 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter break; case JSONP: case LONG_POLLING: - resource.resume(); + disconnect(event); break; default: getLogger().log(Level.SEVERE, "Unknown transport {0}", @@ -415,13 +396,6 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter } @Override - public void onResume(AtmosphereResourceEvent event) { - // Log event on trace level - super.onResume(event); - disconnect(event); - } - - @Override public void destroy() { } @@ -444,8 +418,8 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter */ private static void sendRefreshAndDisconnect(AtmosphereResource resource) throws IOException { - AtmospherePushConnection connection = new AtmospherePushConnection( - null, resource); + AtmospherePushConnection connection = new AtmospherePushConnection(null); + connection.connect(resource); try { connection.sendMessage(VaadinService .createCriticalNotificationJSON(null, null, null, null)); diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java index 272dd8e05c..aec3aa54c0 100644 --- a/server/src/com/vaadin/server/communication/PushRequestHandler.java +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -29,6 +29,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; @@ -75,6 +77,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 ea25777525..ce9cec5e2a 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 implements Serializable { + + 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/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java index 61bcf00ad8..33aa689a88 100644 --- a/server/src/com/vaadin/ui/AbstractComponent.java +++ b/server/src/com/vaadin/ui/AbstractComponent.java @@ -31,13 +31,13 @@ import com.vaadin.event.ConnectorActionManager; import com.vaadin.event.ShortcutListener; import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.ComponentSizeValidator; -import com.vaadin.server.ErrorHandler; import com.vaadin.server.ErrorMessage; import com.vaadin.server.Resource; import com.vaadin.server.VaadinSession; import com.vaadin.shared.AbstractComponentState; import com.vaadin.shared.ComponentConstants; import com.vaadin.shared.ui.ComponentStateUtil; +import com.vaadin.ui.Field.ValueChangeEvent; import com.vaadin.util.ReflectTools; /** @@ -85,8 +85,6 @@ public abstract class AbstractComponent extends AbstractClientConnector private static final Pattern sizePattern = Pattern .compile("^(-?\\d+(\\.\\d+)?)(%|px|em|rem|ex|in|cm|mm|pt|pc)?$"); - private ErrorHandler errorHandler = null; - /** * Keeps track of the Actions added to this component; the actual * handling/notifying is delegated, usually to the containing window. @@ -97,6 +95,8 @@ public abstract class AbstractComponent extends AbstractClientConnector private HasComponents parent; + private Boolean explicitImmediateValue; + /* Constructor */ /** @@ -360,25 +360,29 @@ public abstract class AbstractComponent extends AbstractClientConnector } } - /* - * Tests if the component is in the immediate mode. Don't add a JavaDoc - * comment here, we use the default documentation from implemented - * interface. - */ public boolean isImmediate() { - return getState(false).immediate; + if (explicitImmediateValue != null) { + return explicitImmediateValue; + } else if (hasListeners(ValueChangeEvent.class)) { + /* + * Automatic immediate for fields that developers are interested + * about. + */ + return true; + } else { + return false; + } } /** - * Sets the component's immediate mode to the specified status. This method - * will trigger a {@link RepaintRequestEvent}. + * Sets the component's immediate mode to the specified status. * * @param immediate * the boolean value specifying if the component should be in the * immediate mode after the call. - * @see Component#isImmediate() */ public void setImmediate(boolean immediate) { + explicitImmediateValue = immediate; getState().immediate = immediate; } @@ -675,6 +679,8 @@ public abstract class AbstractComponent extends AbstractClientConnector } else { getState().errorMessage = null; } + + getState().immediate = isImmediate(); } /* General event framework */ diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java index b96e331889..300e130c4e 100644 --- a/server/src/com/vaadin/ui/AbstractField.java +++ b/server/src/com/vaadin/ui/AbstractField.java @@ -1086,6 +1086,8 @@ public abstract class AbstractField<T> extends AbstractComponent implements public void addValueChangeListener(Property.ValueChangeListener listener) { addListener(AbstractField.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); + // ensure "automatic immediate handling" works + markAsDirty(); } /** @@ -1107,6 +1109,8 @@ public abstract class AbstractField<T> extends AbstractComponent implements public void removeValueChangeListener(Property.ValueChangeListener listener) { removeListener(AbstractField.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); + // ensure "automatic immediate handling" works + markAsDirty(); } /** diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java index 556b16943f..a32d40b11d 100644 --- a/server/src/com/vaadin/ui/AbstractSelect.java +++ b/server/src/com/vaadin/ui/AbstractSelect.java @@ -878,6 +878,37 @@ public abstract class AbstractSelect extends AbstractField<Object> implements return retval; } + /** + * Adds given items with given item ids to container. + * + * @since 7.2 + * @param itemId + * item identifiers to be added to underlying container + * @throws UnsupportedOperationException + * if the underlying container don't support adding items with + * identifiers + */ + public void addItems(Object... itemId) throws UnsupportedOperationException { + for (Object id : itemId) { + addItem(id); + } + } + + /** + * Adds given items with given item ids to container. + * + * @since 7.2 + * @param itemIds + * item identifiers to be added to underlying container + * @throws UnsupportedOperationException + * if the underlying container don't support adding items with + * identifiers + */ + public void addItems(Collection<Object> itemIds) + throws UnsupportedOperationException { + addItems(itemIds.toArray()); + } + /* * (non-Javadoc) * diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java index 1bcf802f12..765c805d37 100644 --- a/server/src/com/vaadin/ui/Button.java +++ b/server/src/com/vaadin/ui/Button.java @@ -100,6 +100,31 @@ public class Button extends AbstractComponent implements } /** + * Creates a new push button with the given icon. + * + * @param icon + * the icon + */ + public Button(Resource icon) { + this(); + setIcon(icon); + } + + /** + * Creates a new push button with the given caption and icon. + * + * @param caption + * the caption + * @param icon + * the icon + */ + public Button(String caption, Resource icon) { + this(); + setCaption(caption); + setIcon(icon); + } + + /** * Creates a new push button with a click listener. * * @param caption diff --git a/server/src/com/vaadin/ui/ComboBox.java b/server/src/com/vaadin/ui/ComboBox.java index 88e895df82..5fb2f81011 100644 --- a/server/src/com/vaadin/ui/ComboBox.java +++ b/server/src/com/vaadin/ui/ComboBox.java @@ -56,8 +56,6 @@ public class ComboBox extends AbstractSelect implements */ protected int pageLength = 10; - private int columns = 0; - // Current page when the user is 'paging' trough options private int currentPage = -1; 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/Label.java b/server/src/com/vaadin/ui/Label.java index d7cee2a80d..71d54fc29a 100644 --- a/server/src/com/vaadin/ui/Label.java +++ b/server/src/com/vaadin/ui/Label.java @@ -18,7 +18,6 @@ package com.vaadin.ui; import java.lang.reflect.Method; import java.util.Locale; -import java.util.logging.Logger; import com.vaadin.data.Property; import com.vaadin.data.util.AbstractProperty; @@ -56,9 +55,6 @@ public class Label extends AbstractComponent implements Property<String>, Property.Viewer, Property.ValueChangeListener, Property.ValueChangeNotifier, Comparable<Label> { - private static final Logger logger = Logger - .getLogger(Label.class.getName()); - /** * @deprecated As of 7.0, use {@link ContentMode#TEXT} instead */ @@ -190,7 +186,8 @@ public class Label extends AbstractComponent implements Property<String>, /** * Set the value of the label. Value of the label is the XML contents of the - * label. + * label. Since Vaadin 7.2, changing the value of Label instance with that + * method will fire ValueChangeEvent. * * @param newStringValue * the New value of the label. @@ -198,7 +195,13 @@ public class Label extends AbstractComponent implements Property<String>, @Override public void setValue(String newStringValue) { if (getPropertyDataSource() == null) { - getState().text = newStringValue; + + LabelState state = (LabelState) getState(false); + String oldTextValue = state.text; + if (!SharedUtil.equals(oldTextValue, newStringValue)) { + getState().text = newStringValue; + fireValueChange(); + } } else { throw new IllegalStateException( "Label is only a Property.Viewer and cannot update its data source"); @@ -227,7 +230,8 @@ public class Label extends AbstractComponent implements Property<String>, } /** - * Sets the property as data-source for viewing. + * Sets the property as data-source for viewing. Since Vaadin 7.2 a + * ValueChangeEvent is fired if the new value is different from previous. * * @param newDataSource * the new data source Property @@ -257,7 +261,7 @@ public class Label extends AbstractComponent implements Property<String>, if (dataSource != null) { // Update the value from the data source. If data source was set to // null, retain the old value - getState().text = getDataSourceValue(); + updateValueFromDataSource(); } // Listens the new data source if possible @@ -408,7 +412,8 @@ public class Label extends AbstractComponent implements Property<String>, private void updateValueFromDataSource() { // Update the internal value from the data source String newConvertedValue = getDataSourceValue(); - if (!SharedUtil.equals(newConvertedValue, getState().text)) { + if (!SharedUtil.equals(newConvertedValue, + ((LabelState) getState(false)).text)) { getState().text = newConvertedValue; fireValueChange(); } 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/PushConfiguration.java b/server/src/com/vaadin/ui/PushConfiguration.java index a592b39bef..49738c5aff 100644 --- a/server/src/com/vaadin/ui/PushConfiguration.java +++ b/server/src/com/vaadin/ui/PushConfiguration.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; import com.vaadin.server.VaadinSession; +import com.vaadin.server.communication.AtmospherePushConnection; import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.Transport; import com.vaadin.shared.ui.ui.UIState.PushConfigurationState; @@ -170,20 +171,32 @@ class PushConfigurationImpl implements PushConfiguration { throw new IllegalArgumentException("Push mode cannot be null"); } - if (pushMode.isEnabled()) { - VaadinSession session = ui.getSession(); - if (session != null && !session.getService().ensurePushAvailable()) { - throw new IllegalStateException( - "Push is not available. See previous log messages for more information."); - } + VaadinSession session = ui.getSession(); + + if (session == null) { + throw new UIDetachedException( + "Cannot set the push mode for a detached UI"); + } + + assert session.hasLock(); + + if (pushMode.isEnabled() && !session.getService().ensurePushAvailable()) { + throw new IllegalStateException( + "Push is not available. See previous log messages for more information."); } - /* - * Client-side will open a new connection or disconnect the old - * connection, so there's nothing more to do on the server at this - * point. - */ - getState().mode = pushMode; + PushMode oldMode = getState().mode; + if (oldMode != pushMode) { + getState().mode = pushMode; + + if (!oldMode.isEnabled() && pushMode.isEnabled()) { + // The push connection is initially in a disconnected state; + // the client will establish the connection + ui.setPushConnection(new AtmospherePushConnection(ui)); + } + // Nothing to do here if disabling push; + // the client will close the connection + } } /* @@ -274,9 +287,8 @@ class PushConfigurationImpl implements PushConfiguration { @Override public Collection<String> getParameterNames() { - return Collections - .unmodifiableCollection(ui.getState(false).pushConfiguration.parameters - .keySet()); + return Collections.unmodifiableCollection(getState(false).parameters + .keySet()); } } 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/Table.java b/server/src/com/vaadin/ui/Table.java index 32ed738697..06e82dedcb 100644 --- a/server/src/com/vaadin/ui/Table.java +++ b/server/src/com/vaadin/ui/Table.java @@ -2165,7 +2165,6 @@ public class Table extends AbstractSelect implements Action.Container, // more efficient implementation for containers supporting access by // index - Container.Indexed indexed = ((Container.Indexed) items); List<?> itemIds = getItemIds(firstIndex, rows); for (int i = 0; i < rows && i < itemIds.size(); i++) { Object id = itemIds.get(i); diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index 746fa194ac..e688c06061 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; @@ -52,6 +55,7 @@ import com.vaadin.server.communication.PushConnection; import com.vaadin.shared.Connector; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.DebugWindowClientRpc; import com.vaadin.shared.ui.ui.DebugWindowServerRpc; import com.vaadin.shared.ui.ui.ScrollClientRpc; @@ -77,7 +81,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 +99,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 +172,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 +224,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. @@ -418,8 +423,9 @@ public abstract class UI extends AbstractSingleComponentContainer implements } else { if (session == null) { detach(); - // Close the push connection when UI is detached. Otherwise the + // Disable push when the UI is detached. Otherwise the // push connection and possibly VaadinSession will live on. + getPushConfiguration().setPushMode(PushMode.DISABLED); setPushConnection(null); } this.session = session; @@ -546,11 +552,11 @@ public abstract class UI extends AbstractSingleComponentContainer implements private transient PushConnection pushConnection = null; - private boolean hasPendingPush = false; - 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 @@ -1346,6 +1368,9 @@ public abstract class UI extends AbstractSingleComponentContainer implements * Pushes the pending changes and client RPC invocations of this UI to the * client-side. * <p> + * If push is enabled, but the push connection is not currently open, the + * push will be done when the connection is established. + * <p> * As with all UI methods, the session must be locked when calling this * method. It is also recommended that {@link UI#getCurrent()} is set up to * return this UI since writing the response may invoke logic in any @@ -1363,79 +1388,73 @@ public abstract class UI extends AbstractSingleComponentContainer implements */ public void push() { VaadinSession session = getSession(); - if (session != null) { - assert session.hasLock(); - - /* - * Purge the pending access queue as it might mark a connector as - * dirty when the push would otherwise be ignored because there are - * no changes to push. - */ - session.getService().runPendingAccessTasks(session); - - if (!getConnectorTracker().hasDirtyConnectors()) { - // Do not push if there is nothing to push - return; - } - if (!getPushConfiguration().getPushMode().isEnabled()) { - throw new IllegalStateException("Push not enabled"); - } + if (session == null) { + throw new UIDetachedException("Cannot push a detached UI"); + } + assert session.hasLock(); - if (pushConnection == null) { - hasPendingPush = true; - } else { - pushConnection.push(); - } - } else { - throw new UIDetachedException("Trying to push a detached UI"); + if (!getPushConfiguration().getPushMode().isEnabled()) { + throw new IllegalStateException("Push not enabled"); } + assert pushConnection != null; + + /* + * Purge the pending access queue as it might mark a connector as dirty + * when the push would otherwise be ignored because there are no changes + * to push. + */ + session.getService().runPendingAccessTasks(session); + + if (!getConnectorTracker().hasDirtyConnectors()) { + // Do not push if there is nothing to push + return; + } + + pushConnection.push(); } /** * Returns the internal push connection object used by this UI. This method - * should only be called by the framework. If the returned PushConnection is - * not null, it is guaranteed to have {@code isConnected() == true}. + * should only be called by the framework. * <p> * This method is not intended to be overridden. If it is overridden, care * should be taken since this method might be called in situations where * {@link UI#getCurrent()} does not return this UI. * - * @return the push connection used by this UI, <code>null</code> if there - * is no active push connection. + * @return the push connection used by this UI, or {@code null} if push is + * not available. */ public PushConnection getPushConnection() { - assert (pushConnection == null || pushConnection.isConnected()); + assert !(getPushConfiguration().getPushMode().isEnabled() && pushConnection == null); return pushConnection; } /** * Sets the internal push connection object used by this UI. This method - * should only be called by the framework. If {@pushConnection} is not null, - * its {@code isConnected()} must be true. + * should only be called by the framework. + * <p> + * The {@code pushConnection} argument must be non-null if and only if + * {@code getPushConfiguration().getPushMode().isEnabled()}. * * @param pushConnection * the push connection to use for this UI */ public void setPushConnection(PushConnection pushConnection) { - // If pushMode is disabled then there should never be a pushConnection - assert (pushConnection == null || getPushConfiguration().getPushMode() - .isEnabled()); - assert (pushConnection == null || pushConnection.isConnected()); + // If pushMode is disabled then there should never be a pushConnection; + // if enabled there should always be + assert (pushConnection == null) + ^ getPushConfiguration().getPushMode().isEnabled(); if (pushConnection == this.pushConnection) { return; } - if (this.pushConnection != null) { + if (this.pushConnection != null && this.pushConnection.isConnected()) { this.pushConnection.disconnect(); } this.pushConnection = pushConnection; - if (pushConnection != null && hasPendingPush) { - hasPendingPush = false; - pushConnection.push(); - } } /** @@ -1469,6 +1488,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 +1548,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/Upload.java b/server/src/com/vaadin/ui/Upload.java index 98f5d2ded9..c8d9f3ff09 100644 --- a/server/src/com/vaadin/ui/Upload.java +++ b/server/src/com/vaadin/ui/Upload.java @@ -28,7 +28,10 @@ import com.vaadin.server.NoOutputStreamException; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.server.StreamVariable.StreamingProgressEvent; +import com.vaadin.shared.EventId; import com.vaadin.shared.ui.upload.UploadClientRpc; +import com.vaadin.shared.ui.upload.UploadServerRpc; +import com.vaadin.util.ReflectTools; /** * Component for uploading files from client to server. @@ -113,9 +116,16 @@ public class Upload extends AbstractComponent implements Component.Focusable, * The receiver must be set before performing an upload. */ public Upload() { + registerRpc(new UploadServerRpc() { + @Override + public void change(String filename) { + fireEvent(new ChangeEvent(Upload.this, filename)); + } + }); } public Upload(String caption, Receiver uploadReceiver) { + this(); setCaption(caption); receiver = uploadReceiver; } @@ -486,6 +496,42 @@ public class Upload extends AbstractComponent implements Component.Focusable, } /** + * Upload.ChangeEvent event is sent when the value (filename) of the upload + * changes. + * + * @since 7.2 + */ + public static class ChangeEvent extends Component.Event { + + private final String filename; + + public ChangeEvent(Upload source, String filename) { + super(source); + this.filename = filename; + } + + /** + * Uploads where the event occurred. + * + * @return the Source of the event. + */ + @Override + public Upload getSource() { + return (Upload) super.getSource(); + } + + /** + * Gets the file name. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + } + + /** * Receives the events when the upload starts. * * @author Vaadin Ltd. @@ -554,6 +600,25 @@ public class Upload extends AbstractComponent implements Component.Focusable, } /** + * Listener for {@link ChangeEvent} + * + * @since 7.2 + */ + public interface ChangeListener extends Serializable { + + Method FILENAME_CHANGED = ReflectTools.findMethod(ChangeListener.class, + "filenameChanged", ChangeEvent.class); + + /** + * A file has been selected but upload has not yet started. + * + * @param event + * the change event + */ + public void filenameChanged(ChangeEvent event); + } + + /** * Adds the upload started event listener. * * @param listener @@ -740,6 +805,27 @@ public class Upload extends AbstractComponent implements Component.Focusable, } /** + * Adds a filename change event listener + * + * @param listener + * the Listener to add + */ + public void addChangeListener(ChangeListener listener) { + super.addListener(EventId.CHANGE, ChangeEvent.class, listener, + ChangeListener.FILENAME_CHANGED); + } + + /** + * Removes a filename change event listener + * + * @param listener + * the listener to be removed + */ + public void removeChangeListener(ChangeListener listener) { + super.removeListener(EventId.CHANGE, ChangeEvent.class, listener); + } + + /** * @deprecated As of 7.0, replaced by * {@link #removeProgressListener(ProgressListener)} **/ @@ -1040,7 +1126,11 @@ public class Upload extends AbstractComponent implements Component.Focusable, @Override public OutputStream getOutputStream() { - OutputStream receiveUpload = receiver.receiveUpload( + if (getReceiver() == null) { + throw new IllegalStateException( + "Upload cannot be performed without a receiver set"); + } + OutputStream receiveUpload = getReceiver().receiveUpload( lastStartedEvent.getFileName(), lastStartedEvent.getMimeType()); lastStartedEvent = null; 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 684ab5d6bc..a6c3bb0b8b 100644 --- a/server/tests/src/com/vaadin/data/util/BeanContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanContainerTest.java @@ -466,6 +466,8 @@ public class BeanContainerTest extends AbstractBeanContainerTest { assertTrue(container .addNestedContainerProperty("address.postalCodeObject")); assertTrue(container.addNestedContainerProperty("address.street")); + // the nested properties added with allowNullBean setting should return + // null assertNull(container.getContainerProperty("John", "address.street") .getValue()); } diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java index 767a9e2e4d..b9633753b4 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. @@ -723,7 +730,186 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest { assertTrue(container .addNestedContainerProperty("address.postalCodeObject")); assertTrue(container.addNestedContainerProperty("address.street")); + // the nested properties should return null assertNull(container.getContainerProperty(john, "address.street") .getValue()); } + + 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/TestIndexedContainer.java b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java index 09e5a26c15..5da0bdc8a2 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); + + 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/data/util/sqlcontainer/SQLContainerTest.java b/server/tests/src/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java index 844ef705b1..4c132eba30 100644 --- a/server/tests/src/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java @@ -1917,7 +1917,6 @@ public class SQLContainerTest { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( @@ -1990,7 +1989,6 @@ public class SQLContainerTest { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( @@ -2134,7 +2132,6 @@ public class SQLContainerTest { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( @@ -2213,7 +2210,6 @@ public class SQLContainerTest { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( @@ -2292,7 +2288,6 @@ public class SQLContainerTest { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( diff --git a/server/tests/src/com/vaadin/data/util/sqlcontainer/TicketTests.java b/server/tests/src/com/vaadin/data/util/sqlcontainer/TicketTests.java index 8e8c83d234..110225e206 100644 --- a/server/tests/src/com/vaadin/data/util/sqlcontainer/TicketTests.java +++ b/server/tests/src/com/vaadin/data/util/sqlcontainer/TicketTests.java @@ -88,7 +88,6 @@ public class TicketTests { EasyMock.expect(delegate.getCountStatement()) .andAnswer(new IAnswer<StatementHelper>() { @Override - @SuppressWarnings("deprecation") public StatementHelper answer() throws Throwable { StatementHelper sh = new StatementHelper(); StringBuffer query = new StringBuffer( 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/TestClassesSerializable.java b/server/tests/src/com/vaadin/tests/server/TestClassesSerializable.java index e5420b8921..0a36c7f7ce 100644 --- a/server/tests/src/com/vaadin/tests/server/TestClassesSerializable.java +++ b/server/tests/src/com/vaadin/tests/server/TestClassesSerializable.java @@ -42,6 +42,7 @@ public class TestClassesSerializable extends TestCase { "com\\.vaadin\\.event\\.FieldEvents", // "com\\.vaadin\\.event\\.LayoutEvents", // "com\\.vaadin\\.event\\.MouseEvents", // + "com\\.vaadin\\.event\\.UIEvents", // "com\\.vaadin\\.server\\.VaadinPortlet", // "com\\.vaadin\\.server\\.MockServletConfig", // "com\\.vaadin\\.server\\.MockServletContext", // @@ -49,6 +50,8 @@ public class TestClassesSerializable extends TestCase { "com\\.vaadin\\.server\\.communication\\.FileUploadHandler\\$SimpleMultiPartInputStream", // "com\\.vaadin\\.server\\.communication\\.PushRequestHandler.*", "com\\.vaadin\\.server\\.communication\\.PushHandler.*", // PushHandler + "com\\.vaadin\\.server\\.communication\\.DateSerializer", // + "com\\.vaadin\\.server\\.communication\\.JSONSerializer", // // and its inner classes do not need to be serializable "com\\.vaadin\\.util\\.SerializerHelper", // fully static // class level filtering, also affecting nested classes and 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/component/abstractselect/TestVarargsItemAddition.java b/server/tests/src/com/vaadin/tests/server/component/abstractselect/TestVarargsItemAddition.java new file mode 100644 index 0000000000..5575b8fd3d --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/abstractselect/TestVarargsItemAddition.java @@ -0,0 +1,26 @@ +package com.vaadin.tests.server.component.abstractselect; + +import java.util.Collection; + +import junit.framework.TestCase; + +import org.junit.Assert; + +import com.vaadin.ui.OptionGroup; + +public class TestVarargsItemAddition extends TestCase { + + public void itemAddition() throws Exception { + + OptionGroup optionGroup = new OptionGroup(); + + optionGroup.addItems("foo", "bar", "car"); + + Collection<?> itemIds = optionGroup.getItemIds(); + Assert.assertEquals(3, itemIds.size()); + Assert.assertTrue(itemIds.contains("foo")); + Assert.assertTrue(itemIds.contains("bar")); + Assert.assertTrue(itemIds.contains("car")); + + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/label/LabelListeners.java b/server/tests/src/com/vaadin/tests/server/component/label/LabelListeners.java index 3ed79f5010..9bb4c53ba2 100644 --- a/server/tests/src/com/vaadin/tests/server/component/label/LabelListeners.java +++ b/server/tests/src/com/vaadin/tests/server/component/label/LabelListeners.java @@ -1,5 +1,14 @@ package com.vaadin.tests.server.component.label; +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import org.easymock.EasyMock; + +import com.vaadin.data.Property; import com.vaadin.data.Property.ValueChangeListener; import com.vaadin.tests.server.component.AbstractListenerMethodsTest; import com.vaadin.ui.Label; @@ -10,4 +19,74 @@ public class LabelListeners extends AbstractListenerMethodsTest { testListenerAddGetRemove(Label.class, ValueChangeEvent.class, ValueChangeListener.class); } + + public void testValueChangeFiredWhenSettingValue() { + Label underTest = new Label(); + + // setup the mock listener + ValueChangeListener mockListener = createStrictMock(ValueChangeListener.class); + // record + mockListener + .valueChange(anyObject(com.vaadin.data.Property.ValueChangeEvent.class)); + + // test + underTest.addValueChangeListener(mockListener); + + replay(mockListener); + underTest.setValue("A new value"); + + verify(mockListener); + + } + + public void testValueChangeFiredWhenSettingPropertyDataSource() { + // setup + Label underTest = new Label(); + + Property mockProperty = EasyMock.createMock(Property.class); + + ValueChangeListener mockListener = createStrictMock(ValueChangeListener.class); + // record + mockListener + .valueChange(anyObject(com.vaadin.data.Property.ValueChangeEvent.class)); + + expect(mockProperty.getType()).andReturn(String.class).atLeastOnce(); + expect(mockProperty.getValue()).andReturn("Any").atLeastOnce(); + + // test + + replay(mockListener, mockProperty); + underTest.addValueChangeListener(mockListener); + underTest.setPropertyDataSource(mockProperty); + + verify(mockListener); + + } + + public void testValueChangeNotFiredWhenNotSettingValue() { + Label underTest = new Label(); + // setup the mock listener + ValueChangeListener mockListener = createStrictMock(ValueChangeListener.class); + // record: nothing to record + + // test + underTest.addValueChangeListener(mockListener); + replay(mockListener); + verify(mockListener); + } + + public void testNoValueChangeFiredWhenSettingPropertyDataSourceToNull() { + Label underTest = new Label(); + // setup the mock Listener + ValueChangeListener mockListener = createStrictMock(ValueChangeListener.class); + // record: nothing to record + + // test + underTest.addValueChangeListener(mockListener); + underTest.setPropertyDataSource(null); + + replay(mockListener); + verify(mockListener); + } + } 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>( |