diff options
Diffstat (limited to 'server/src/com/vaadin')
75 files changed, 3595 insertions, 1048 deletions
diff --git a/server/src/com/vaadin/annotations/PreserveOnRefresh.java b/server/src/com/vaadin/annotations/PreserveOnRefresh.java index d6216d1ee0..7f4ef3ffe5 100644 --- a/server/src/com/vaadin/annotations/PreserveOnRefresh.java +++ b/server/src/com/vaadin/annotations/PreserveOnRefresh.java @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import com.vaadin.server.UIProvider; +import com.vaadin.ui.UI; /** * Marks a UI that should be retained when the user refreshed the browser @@ -30,6 +31,10 @@ import com.vaadin.server.UIProvider; * adding this annotation to a UI class, the framework will instead reuse the * current UI instance when a reload is detected. * <p> + * Whenever a request is received that reloads a preserved UI, the UI's + * {@link UI#refresh(com.vaadin.server.VaadinRequest) refresh} method is invoked + * by the framework. + * <p> * By using * {@link UIProvider#isPreservedOnRefresh(com.vaadin.server.UICreateEvent)}, the * decision can also be made dynamically based on other parameters than only diff --git a/server/src/com/vaadin/annotations/Widgetset.java b/server/src/com/vaadin/annotations/Widgetset.java index 2047fa377d..c6ef6a7194 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/fieldgroup/BeanFieldGroup.java b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java index ff4ecc8426..e5d53b759d 100644 --- a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java @@ -225,4 +225,58 @@ public class BeanFieldGroup<T> extends FieldGroup { } return beanValidationImplementationAvailable; } + + /** + * Convenience method to bind Fields from a given "field container" to a + * given bean with buffering disabled. + * <p> + * The returned {@link BeanFieldGroup} can be used for further + * configuration. + * + * @see #bindFieldsBuffered(Object, Object) + * @see #bindMemberFields(Object) + * @since 7.2 + * @param bean + * the bean to be bound + * @param objectWithMemberFields + * the class that contains {@link Field}s for bean properties + * @return the bean field group used to make binding + */ + public static <T> BeanFieldGroup<T> bindFieldsUnbuffered(T bean, + Object objectWithMemberFields) { + return createAndBindFields(bean, objectWithMemberFields, false); + } + + /** + * Convenience method to bind Fields from a given "field container" to a + * given bean with buffering enabled. + * <p> + * The returned {@link BeanFieldGroup} can be used for further + * configuration. + * + * @see #bindFieldsUnbuffered(Object, Object) + * @see #bindMemberFields(Object) + * @since 7.2 + * @param bean + * the bean to be bound + * @param objectWithMemberFields + * the class that contains {@link Field}s for bean properties + * @return the bean field group used to make binding + */ + public static <T> BeanFieldGroup<T> bindFieldsBuffered(T bean, + Object objectWithMemberFields) { + return createAndBindFields(bean, objectWithMemberFields, true); + } + + private static <T> BeanFieldGroup<T> createAndBindFields(T bean, + Object objectWithMemberFields, boolean buffered) { + @SuppressWarnings("unchecked") + BeanFieldGroup<T> beanFieldGroup = new BeanFieldGroup<T>( + (Class<T>) bean.getClass()); + beanFieldGroup.setItemDataSource(bean); + beanFieldGroup.setBuffered(buffered); + beanFieldGroup.bindMemberFields(objectWithMemberFields); + return beanFieldGroup; + } + } diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java index 4772dce5e6..e647bdbf6d 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; @@ -1019,9 +1015,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/ContainerHierarchicalWrapper.java b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java index 038b036f4e..eafd3573bc 100644 --- a/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java +++ b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java @@ -95,7 +95,7 @@ public class ContainerHierarchicalWrapper implements Container.Hierarchical, } return 0; } - }; + } /** * Constructs a new hierarchical wrapper for an existing Container. Works diff --git a/server/src/com/vaadin/data/util/ListSet.java b/server/src/com/vaadin/data/util/ListSet.java index c4201692d4..ccc9e0dbfd 100644 --- a/server/src/com/vaadin/data/util/ListSet.java +++ b/server/src/com/vaadin/data/util/ListSet.java @@ -82,7 +82,7 @@ public class ListSet<E> extends ArrayList<E> { } else { return false; } - }; + } /** * Works as java.util.ArrayList#add(int, java.lang.Object) but returns diff --git a/server/src/com/vaadin/data/util/MethodProperty.java b/server/src/com/vaadin/data/util/MethodProperty.java index 5b9f3c90fd..5ec8ebffe0 100644 --- a/server/src/com/vaadin/data/util/MethodProperty.java +++ b/server/src/com/vaadin/data/util/MethodProperty.java @@ -19,6 +19,7 @@ package com.vaadin.data.util; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; @@ -85,6 +86,10 @@ public class MethodProperty<T> extends AbstractProperty<T> { */ private transient Class<? extends T> type; + private static final Object[] DEFAULT_GET_ARGS = new Object[0]; + + private static final Object[] DEFAULT_SET_ARGS = new Object[1]; + /* Special serialization to handle method references */ private void writeObject(java.io.ObjectOutputStream out) throws IOException { out.defaultWriteObject(); @@ -108,7 +113,7 @@ public class MethodProperty<T> extends AbstractProperty<T> { out.writeObject(null); out.writeObject(null); } - }; + } /* Special serialization to handle method references */ private void readObject(java.io.ObjectInputStream in) throws IOException, @@ -120,8 +125,9 @@ public class MethodProperty<T> extends AbstractProperty<T> { Class<T> class1 = (Class<T>) SerializerHelper.readClass(in); type = class1; instance = in.readObject(); - setArgs = (Object[]) in.readObject(); - getArgs = (Object[]) in.readObject(); + Object[] setArgs = (Object[]) in.readObject(); + Object[] getArgs = (Object[]) in.readObject(); + setArguments(getArgs, setArgs, setArgumentIndex); String name = (String) in.readObject(); Class<?>[] paramTypes = SerializerHelper.readClassArray(in); if (name != null) { @@ -142,7 +148,7 @@ public class MethodProperty<T> extends AbstractProperty<T> { } catch (NoSuchMethodException e) { getLogger().log(Level.SEVERE, "Internal deserialization error", e); } - }; + } /** * <p> @@ -219,7 +225,7 @@ public class MethodProperty<T> extends AbstractProperty<T> { type = (Class<T>) returnType; } - setArguments(new Object[] {}, new Object[] { null }, 0); + setArguments(DEFAULT_GET_ARGS, DEFAULT_SET_ARGS, 0); this.instance = instance; } @@ -627,13 +633,15 @@ public class MethodProperty<T> extends AbstractProperty<T> { */ public void setArguments(Object[] getArgs, Object[] setArgs, int setArgumentIndex) { - this.getArgs = new Object[getArgs.length]; - for (int i = 0; i < getArgs.length; i++) { - this.getArgs[i] = getArgs[i]; + if (getArgs.length == 0) { + this.getArgs = DEFAULT_GET_ARGS; + } else { + this.getArgs = Arrays.copyOf(getArgs, getArgs.length); } - this.setArgs = new Object[setArgs.length]; - for (int i = 0; i < setArgs.length; i++) { - this.setArgs[i] = setArgs[i]; + if (Arrays.equals(setArgs, DEFAULT_SET_ARGS)) { + this.setArgs = DEFAULT_SET_ARGS; + } else { + this.setArgs = Arrays.copyOf(setArgs, setArgs.length); } this.setArgumentIndex = setArgumentIndex; } diff --git a/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java index c0cdffe2e7..04a6ab1cc9 100644 --- a/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java +++ b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java @@ -122,7 +122,7 @@ public class MethodPropertyDescriptor<BT> implements } catch (NoSuchMethodException e) { getLogger().log(Level.SEVERE, "Internal deserialization error", e); } - }; + } @Override public String getName() { diff --git a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java index b97a5b9047..fdf858a528 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 6d33000160..0000000000 --- a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.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/util/filter/Compare.java b/server/src/com/vaadin/data/util/filter/Compare.java index 5a82392f77..1fcbe85580 100644 --- a/server/src/com/vaadin/data/util/filter/Compare.java +++ b/server/src/com/vaadin/data/util/filter/Compare.java @@ -37,7 +37,7 @@ public abstract class Compare implements Filter { public enum Operation { EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL - }; + } private final Object propertyId; private final Operation operation; diff --git a/server/src/com/vaadin/data/util/filter/Like.java b/server/src/com/vaadin/data/util/filter/Like.java index 656d3d5c32..9b7b2af292 100644 --- a/server/src/com/vaadin/data/util/filter/Like.java +++ b/server/src/com/vaadin/data/util/filter/Like.java @@ -23,11 +23,11 @@ public class Like implements Filter { private final String value; private boolean caseSensitive; - public Like(String propertyId, String value) { + public Like(Object propertyId, String value) { this(propertyId, value, true); } - public Like(String propertyId, String value, boolean caseSensitive) { + public Like(Object propertyId, String value, boolean caseSensitive) { this.propertyId = propertyId; this.value = value; setCaseSensitive(caseSensitive); diff --git a/server/src/com/vaadin/data/validator/BeanValidator.java b/server/src/com/vaadin/data/validator/BeanValidator.java index b8c4e1b493..b25f7e687c 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/FieldEvents.java b/server/src/com/vaadin/event/FieldEvents.java index 7164044cf3..364ac76ffd 100644 --- a/server/src/com/vaadin/event/FieldEvents.java +++ b/server/src/com/vaadin/event/FieldEvents.java @@ -325,6 +325,6 @@ public interface FieldEvents { public void focus() { fireEvent(new FocusEvent(component)); } - }; + } } diff --git a/server/src/com/vaadin/event/ListenerMethod.java b/server/src/com/vaadin/event/ListenerMethod.java index 1f1bcd619d..3311ed705e 100644 --- a/server/src/com/vaadin/event/ListenerMethod.java +++ b/server/src/com/vaadin/event/ListenerMethod.java @@ -98,7 +98,7 @@ public class ListenerMethod implements EventListener, Serializable { throw e; } - }; + } /* Special serialization to handle method references */ private void readObject(java.io.ObjectInputStream in) throws IOException, @@ -113,7 +113,7 @@ public class ListenerMethod implements EventListener, Serializable { } catch (SecurityException e) { getLogger().log(Level.SEVERE, "Internal deserialization error", e); } - }; + } private static Method findHighestMethod(Class<?> cls, String method, Class<?>[] paramTypes) { 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/navigator/Navigator.java b/server/src/com/vaadin/navigator/Navigator.java index 2f0e91e9e6..80dad2244e 100644 --- a/server/src/com/vaadin/navigator/Navigator.java +++ b/server/src/com/vaadin/navigator/Navigator.java @@ -794,7 +794,7 @@ public class Navigator implements Serializable { @Override public View getView(String viewName) { return view; - }; + } @Override public String getViewName(String navigationState) { diff --git a/server/src/com/vaadin/server/AbstractClientConnector.java b/server/src/com/vaadin/server/AbstractClientConnector.java index 1a8a5697ee..1e58fb30c0 100644 --- a/server/src/com/vaadin/server/AbstractClientConnector.java +++ b/server/src/com/vaadin/server/AbstractClientConnector.java @@ -132,13 +132,22 @@ public abstract class AbstractClientConnector implements ClientConnector, /* Documentation copied from interface */ @Override public void markAsDirty() { - assert getSession() == null || getSession().hasLock() : "Session must be locked when markAsDirty() is called"; + assert getSession() == null || getSession().hasLock() : buildLockAssertMessage("markAsDirty()"); UI uI = getUI(); if (uI != null) { uI.getConnectorTracker().markDirty(this); } } + private String buildLockAssertMessage(String method) { + if (VaadinService.isOtherSessionLocked(getSession())) { + return "The session of this connecor is not locked, but there is another session that is locked. " + + "This might be caused by accidentally using a connector that belongs to another session."; + } else { + return "Session must be locked when " + method + " is called"; + } + } + /** * Registers an RPC interface implementation for this component. * @@ -217,7 +226,7 @@ public abstract class AbstractClientConnector implements ClientConnector, * @see #getState() */ protected SharedState getState(boolean markAsDirty) { - assert getSession() == null || getSession().hasLock() : "Session must be locked when getState() is called"; + assert getSession() == null || getSession().hasLock() : buildLockAssertMessage("getState()"); if (null == sharedState) { sharedState = createState(); diff --git a/server/src/com/vaadin/server/BootstrapHandler.java b/server/src/com/vaadin/server/BootstrapHandler.java index a0acea6976..3c75764075 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 { List<UIProvider> uiProviders = session.getUIProviders(); @@ -281,7 +282,7 @@ public abstract class BootstrapHandler extends SynchronizedRequestHandler { * Enable Chrome Frame in all versions of IE if installed. */ head.appendElement("meta").attr("http-equiv", "X-UA-Compatible") - .attr("content", "IE=10;chrome=1"); + .attr("content", "IE=11;chrome=1"); String title = response.getUIProvider().getPageTitle( new UICreateEvent(context.getRequest(), context.getUIClass())); diff --git a/server/src/com/vaadin/server/ConnectorResourceHandler.java b/server/src/com/vaadin/server/ConnectorResourceHandler.java index d50e0e80f2..6c486a2d65 100644 --- a/server/src/com/vaadin/server/ConnectorResourceHandler.java +++ b/server/src/com/vaadin/server/ConnectorResourceHandler.java @@ -87,6 +87,14 @@ public class ConnectorResourceHandler implements RequestHandler { + connector.getConnectorId() + ") did not handle connector request for " + key); } + } catch (Exception e) { + session.lock(); + try { + session.getCommunicationManager() + .handleConnectorRelatedException(connector, e); + } finally { + session.unlock(); + } } finally { CurrentInstance.restoreInstances(oldInstances); } diff --git a/server/src/com/vaadin/server/Constants.java b/server/src/com/vaadin/server/Constants.java index c2ed91f612..39329c32ce 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.1.2.vaadin2"; static final String INVALID_ATMOSPHERE_VERSION_WARNING = "\n" + "=================================================================\n" diff --git a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java index e5150fef68..e72b411720 100644 --- a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java @@ -142,16 +142,25 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { pkgName = pkg.getName(); } else { final String className = systemPropertyBaseClass.getName(); - pkgName = new String(className.toCharArray(), 0, - className.lastIndexOf('.')); + int index = className.lastIndexOf('.'); + if (index >= 0) { + pkgName = className.substring(0, index); + } else { + pkgName = null; + } + } + if (pkgName == null) { + pkgName = ""; + } else { + pkgName += '.'; } - val = System.getProperty(pkgName + "." + parameterName); + val = System.getProperty(pkgName + parameterName); if (val != null) { return val; } // Try lowercased system properties - val = System.getProperty(pkgName + "." + parameterName.toLowerCase()); + val = System.getProperty(pkgName + parameterName.toLowerCase()); return val; } diff --git a/server/src/com/vaadin/server/DefaultErrorHandler.java b/server/src/com/vaadin/server/DefaultErrorHandler.java index 127aaf0fc0..bbb15b4d62 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/DragAndDropService.java b/server/src/com/vaadin/server/DragAndDropService.java index fc4d692ce8..087a670b5b 100644 --- a/server/src/com/vaadin/server/DragAndDropService.java +++ b/server/src/com/vaadin/server/DragAndDropService.java @@ -50,14 +50,14 @@ public class DragAndDropService implements VariableOwner, ClientConnector { private DragAndDropEvent dragEvent; - private final LegacyCommunicationManager manager; + private final VaadinSession session; private AcceptCriterion acceptCriterion; private ErrorHandler errorHandler; - public DragAndDropService(LegacyCommunicationManager manager) { - this.manager = manager; + public DragAndDropService(VaadinSession session) { + this.session = session; } @Override @@ -233,8 +233,8 @@ public class DragAndDropService implements VariableOwner, ClientConnector { outWriter.write(", \"dd\":"); - JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager, - outWriter, false); + JsonPaintTarget jsonPaintTarget = new JsonPaintTarget( + session.getCommunicationManager(), outWriter, false); jsonPaintTarget.startTag("dd"); jsonPaintTarget.addAttribute("visitId", lastVisitId); if (acceptCriterion != null) { 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 73611ffc8d..eb11cde343 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; @@ -59,6 +62,25 @@ import com.vaadin.ui.ConnectorTracker; */ public class JsonCodec implements Serializable { + /* Immutable Encode Result representing null */ + private static final EncodeResult ENCODE_RESULT_NULL = new EncodeResult( + JSONObject.NULL); + + /* Immutable empty JSONArray */ + private static final JSONArray EMPTY_JSON_ARRAY = new JSONArray() { + @Override + public JSONArray put(Object value) { + throw new UnsupportedOperationException( + "Immutable empty JSONArray."); + }; + + @Override + public JSONArray put(int index, Object value) { + throw new UnsupportedOperationException( + "Immutable empty JSONArray."); + }; + }; + public static interface BeanProperty extends Serializable { public Object getValue(Object bean) throws Exception; @@ -176,6 +198,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 +310,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 +636,7 @@ public class JsonCodec implements Serializable { return decodedObject; } catch (Exception e) { - throw new JSONException(e); + throw new JSONException(e.getMessage()); } } @@ -624,7 +654,7 @@ public class JsonCodec implements Serializable { } if (null == value) { - return encodeNull(); + return ENCODE_RESULT_NULL; } if (value instanceof String[]) { @@ -669,13 +699,17 @@ public class JsonCodec implements Serializable { if (value instanceof Component && !(LegacyCommunicationManager .isComponentVisibleToClient((Component) value))) { - return encodeNull(); + return ENCODE_RESULT_NULL; } return new EncodeResult(connector.getConnectorId()); } else if (value instanceof Enum) { 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 @@ -686,10 +720,6 @@ public class JsonCodec implements Serializable { } } - private static EncodeResult encodeNull() { - return new EncodeResult(JSONObject.NULL); - } - public static Collection<BeanProperty> getProperties(Class<?> type) throws IntrospectionException { Collection<BeanProperty> cachedProperties = typePropertyCache.get(type); @@ -750,7 +780,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); } @@ -766,14 +796,17 @@ public class JsonCodec implements Serializable { if (fieldValue == JSONObject.NULL) { fieldValue = null; } - if (referenceValue == JSONObject.NULL) { - referenceValue = null; - } if (fieldValue == referenceValue) { return true; } else if (fieldValue == null || referenceValue == null) { return false; + } else if (fieldValue instanceof Integer + && referenceValue instanceof Integer) { + return ((Integer) fieldValue).equals(referenceValue); + } else if (fieldValue instanceof Boolean + && referenceValue instanceof Boolean) { + return ((Boolean) fieldValue).equals(referenceValue); } else { return fieldValue.toString().equals(referenceValue.toString()); } @@ -834,7 +867,7 @@ public class JsonCodec implements Serializable { if (map.isEmpty()) { // Client -> server encodes empty map as an empty array because of // #8906. Do the same for server -> client to maintain symmetry. - return new JSONArray(); + return EMPTY_JSON_ARRAY; } if (keyType == String.class) { diff --git a/server/src/com/vaadin/server/LegacyCommunicationManager.java b/server/src/com/vaadin/server/LegacyCommunicationManager.java index ee5ecd471a..0dda5661bd 100644 --- a/server/src/com/vaadin/server/LegacyCommunicationManager.java +++ b/server/src/com/vaadin/server/LegacyCommunicationManager.java @@ -62,9 +62,6 @@ public class LegacyCommunicationManager implements Serializable { */ private final VaadinSession session; - // TODO Move to VaadinSession (#11409) - private DragAndDropService dragAndDropService; - // TODO Refactor (#11412) private String requestThemeName; @@ -277,28 +274,14 @@ public class LegacyCommunicationManager implements Serializable { } /** - * @deprecated As of 7.1. See #11411. + * @deprecated As of 7.1. In 7.2 and later, use + * {@link ConnectorTracker#getConnector(String) + * uI.getConnectorTracker().getConnector(connectorId)} instead. + * See ticket #11411. */ @Deprecated public ClientConnector getConnector(UI uI, String connectorId) { - ClientConnector c = uI.getConnectorTracker().getConnector(connectorId); - if (c == null - && connectorId.equals(getDragAndDropService().getConnectorId())) { - return getDragAndDropService(); - } - - return c; - } - - /** - * @deprecated As of 7.1. See #11409. - */ - @Deprecated - public DragAndDropService getDragAndDropService() { - if (dragAndDropService == null) { - dragAndDropService = new DragAndDropService(this); - } - return dragAndDropService; + return uI.getConnectorTracker().getConnector(connectorId); } /** @@ -316,8 +299,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 375f589eb8..d58ba548e3 100644 --- a/server/src/com/vaadin/server/Page.java +++ b/server/src/com/vaadin/server/Page.java @@ -32,6 +32,7 @@ import com.vaadin.shared.ui.ui.PageClientRpc; import com.vaadin.shared.ui.ui.PageState; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIState; +import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.JavaScript; import com.vaadin.ui.LegacyWindow; import com.vaadin.ui.Link; @@ -476,6 +477,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; @@ -633,10 +636,14 @@ public class Page implements Serializable { } public void init(VaadinRequest request) { + // NOTE: UI.refresh makes assumptions about the semantics of this method. + // It should be kept in sync if this method is changed. + // Extract special parameter sent by vaadinBootstrap.js 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,28 +669,62 @@ public class Page implements Serializable { } /** - * Updates the internal state with the given values. Does not resize the - * Page or browser window. + * 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; + } + + /** + * For internal use only. Updates the internal state with the given values. + * Does not resize the Page or browser window. + * + * @deprecated As of 7.2, use + * {@link #updateBrowserWindowSize(int, int, boolean)} instead. + * * @param width - * The new width + * the new browser window width * @param height - * The new height + * the new browse window height */ + @Deprecated public void updateBrowserWindowSize(int width, int height) { - boolean fireEvent = false; + updateBrowserWindowSize(width, height, true); + } + + /** + * For internal use only. Updates the internal state with the given values. + * Does not resize the Page or browser window. + * + * @since 7.2 + * + * @param width + * the new browser window width + * @param height + * the new browser window height + * @param fireEvents + * whether to fire {@link BrowserWindowResizeEvent} if the size + * changes + */ + public void updateBrowserWindowSize(int width, int height, + boolean fireEvents) { + boolean sizeChanged = false; if (width != browserWindowWidth) { browserWindowWidth = width; - fireEvent = true; + sizeChanged = true; } if (height != browserWindowHeight) { browserWindowHeight = height; - fireEvent = true; + sizeChanged = true; } - if (fireEvent) { + if (fireEvents && sizeChanged) { fireEvent(new BrowserWindowResizeEvent(this, browserWindowWidth, browserWindowHeight)); } @@ -854,18 +895,33 @@ public class Page implements Serializable { /** * Navigates this page to the given URI. The contents of this page in the * browser is replaced with whatever is returned for the given URI. + * <p> + * This method should not be used to start downloads, as the client side + * will assume the browser will navigate away when opening the URI. Use one + * of the {@code Page.open} methods or {@code FileDownloader} instead. + * + * @see #open(String, String) + * @see FileDownloader * * @param uri * the URI to show */ public void setLocation(String uri) { - openList.add(new OpenResource(uri, null, -1, -1, BORDER_DEFAULT, false)); + openList.add(new OpenResource(uri, "_self", -1, -1, BORDER_DEFAULT, + false)); uI.markAsDirty(); } /** * Navigates this page to the given URI. The contents of this page in the * browser is replaced with whatever is returned for the given URI. + * <p> + * This method should not be used to start downloads, as the client side + * will assume the browser will navigate away when opening the URI. Use one + * of the {@code Page.open} methods or {@code FileDownloader} instead. + * + * @see #open(String, String) + * @see FileDownloader * * @param uri * the URI to show @@ -888,14 +944,37 @@ public class Page implements Serializable { /** * For internal use only. Used to update the server-side location when the * client-side location changes. + * + * @deprecated As of 7.2, use {@link #updateLocation(String, boolean)} + * instead. + * + * @param location + * the new location URI */ + @Deprecated public void updateLocation(String location) { + updateLocation(location, true); + } + + /** + * For internal use only. Used to update the server-side location when the + * client-side location changes. + * + * @since 7.2 + * + * @param location + * the new location URI + * @param fireEvents + * whether to fire {@link UriFragmentChangedEvent} if the URI + * fragment changes + */ + public void updateLocation(String location, boolean fireEvents) { try { String oldUriFragment = this.location.getFragment(); this.location = new URI(location); String newUriFragment = this.location.getFragment(); - if (newUriFragment == null ? oldUriFragment != null - : !newUriFragment.equals(oldUriFragment)) { + if (fireEvents + && !SharedUtil.equals(oldUriFragment, newUriFragment)) { fireEvent(new UriFragmentChangedEvent(this, newUriFragment)); } } catch (URISyntaxException e) { @@ -1119,12 +1198,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 f0c532b8e0..31dfa41ef9 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/Responsive.java b/server/src/com/vaadin/server/Responsive.java new file mode 100644 index 0000000000..d69c204c94 --- /dev/null +++ b/server/src/com/vaadin/server/Responsive.java @@ -0,0 +1,161 @@ +/* + * 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 com.vaadin.ui.Component; + +/** + * An extension providing responsive layout capabilities to any Vaadin + * component. The Responsive extension allows specifying different CSS rules for + * different dimensions of extended components. This allows creating + * applications that provide an optimal viewing experience – easy reading and + * navigation with a minimum of resizing, panning, and scrolling – across a wide + * range of devices (from mobile phones to desktop computer monitors). + * <p> + * NOTE! You should always specify a relative (%) size for the extended + * component, doing otherwise will prevent the Responsive extension from + * working, as the component will not dynamically resize. + * </p> + * <p> + * All configuration of the visual breakpoints (ranges) for the component are + * done with CSS. Pixels (px) are the only supported unit. Fractional pixels are + * not supported. + * </p> + * <p> + * <i>Dynamic style injections (e.g. through + * <code>Page.getCurrent().getStyles().add(...)</code>) or any other style + * updates after the initial page load are not supported at the moment.</i> + * </p> + * + * + * <p> + * Example: + * + * <b>Java</b> + * + * <pre> + * CssLayout layout = new CssLayout(); + * layout.setStyleName("responsive"); + * layout.setSizeFull(); + * Responsive.makeResponsive(layout); + * </pre> + * + * <b>SCSS</b> + * + * <pre> + * .v-csslayout.responsive { + * &[width-range~="0-300px"] { + * // Styles for the layout when its width is between 0 and 300 pixels + * } + * &[width-range~="301-500px"] { + * // Styles for the layout when its width is between 301 and 500 pixels + * } + * &[width-range~="501px-"] { + * // Styles for the layout when its width is over 500 pixels + * } + * &[height-range~="0-300px"] { + * // Styles for the layout when its height is between 0 and 300 pixels + * } + * &[height-range~="301-500px"] { + * // Styles for the layout when its height is between 301 and 500 pixels + * } + * &[height-range~="501-"] { + * // Styles for the layout when its height is over 500 pixels + * } + * } + * </pre> + * + * <b>CSS</b> + * + * <pre> + * .v-csslayout.responsive[width-range~="0-300px"] { + * // Styles for the layout when its width is between 0 and 300 pixels + * } + * .v-csslayout.responsive[width-range~="301-500px"] { + * // Styles for the layout when its width is between 301 and 500 pixels + * } + * .v-csslayout.responsive[width-range~="501-"] { + * // Styles for the layout when its width is over 500 pixels + * } + * + * .v-csslayout.responsive[height-range~="0-300px"] { + * // Styles for the layout when its height is between 0 and 300 pixels + * } + * .v-csslayout.responsive[height-range~="301-500px"] { + * // Styles for the layout when its height is between 301 and 500 pixels + * } + * .v-csslayout.responsive[height-range~="501px-"] { + * // Styles for the layout when its height is over 500 pixels + * } + * </pre> + * + * </p> + * <p> + * <b>Note:</b> <i>The defined ranges are applied on a global context, so even + * if you would write your CSS to target only a given context, the ranges would + * be applied to all other instances with the same style name.</i> + * </p> + * <p> + * E.g. this would affect all CssLayout instances in the application, even + * though the CSS implies it would only affect CssLayout instances inside a + * parent with a style name "foobar": + * </p> + * + * <pre> + * .foobar .v-csslayout[width-range~="0px-100px"] { + * // These properties will affect all responsive CssLayout instances + * } + * </pre> + * + * <p> + * To scope the ranges, use an additional style name for the target component, + * and add that to your CSS selector: + * </p> + * + * <pre> + * .v-csslayout.mystyle[width-range="0px-100px"] { + * // These properties will only affect responsive CssLayout instances with an additional style name of 'mystyle' + * } + * </pre> + * + * @author Vaadin Ltd + * @since 7.2 + */ +public class Responsive extends AbstractExtension { + + /** + * Creates a new instance, which can be used to extend a component. + */ + protected Responsive() { + } + + /** + * Enable responsive width and height range styling for the target component + * or UI instance. + * + * @param target + * The component which should be able to respond to width and/or + * height changes. + */ + public static void makeResponsive(Component... components) { + for (Component c : components) { + if (c instanceof AbstractClientConnector) { + new Responsive().extend((AbstractClientConnector) c); + } + } + } +} 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..1549d82d51 --- /dev/null +++ b/server/src/com/vaadin/server/ServiceDestroyListener.java @@ -0,0 +1,39 @@ +/* + * 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 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 59de4fc52b..56abfbb8f3 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 bd63796448..51e9da5800 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/UIProvider.java b/server/src/com/vaadin/server/UIProvider.java index fd010ac48e..d3d834cad7 100644 --- a/server/src/com/vaadin/server/UIProvider.java +++ b/server/src/com/vaadin/server/UIProvider.java @@ -129,6 +129,11 @@ public abstract class UIProvider implements Serializable { * detect that the application is opened in a browser window where it has * previously been open. The framework attempts to discover this by checking * the value of window.name in the browser. + * <p> + * Whenever a preserved UI is reused, its + * {@link UI#refresh(com.vaadin.server.VaadinRequest) refresh} method is + * invoked by the framework first. + * * * @param event * the UI create event with information about the UI and the diff --git a/server/src/com/vaadin/server/VaadinPortlet.java b/server/src/com/vaadin/server/VaadinPortlet.java index 0c31407b76..77a1b18d10 100644 --- a/server/src/com/vaadin/server/VaadinPortlet.java +++ b/server/src/com/vaadin/server/VaadinPortlet.java @@ -29,6 +29,7 @@ import javax.portlet.ActionResponse; import javax.portlet.EventRequest; import javax.portlet.EventResponse; import javax.portlet.GenericPortlet; +import javax.portlet.PortalContext; import javax.portlet.PortletConfig; import javax.portlet.PortletContext; import javax.portlet.PortletException; @@ -51,61 +52,83 @@ import com.vaadin.util.CurrentInstance; * Portlet 2.0 base class. This replaces the servlet in servlet/portlet 1.0 * deployments and handles various portlet requests from the browser. * - * TODO Document me! - * - * @author peholmst + * @author Vaadin Ltd */ public class VaadinPortlet extends GenericPortlet implements Constants, Serializable { /** - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * Base class for portlet requests that need access to HTTP servlet + * requests. */ - @Deprecated - public static final String RESOURCE_URL_ID = "APP"; - - public static class VaadinHttpAndPortletRequest extends + public static abstract class VaadinHttpAndPortletRequest extends VaadinPortletRequest { + /** + * Constructs a new {@link VaadinHttpAndPortletRequest}. + * + * @since 7.2 + * @param request + * {@link PortletRequest} to be wrapped + * @param vaadinService + * {@link VaadinPortletService} associated with this request + */ public VaadinHttpAndPortletRequest(PortletRequest request, - HttpServletRequest originalRequest, VaadinPortletService vaadinService) { super(request, vaadinService); - this.originalRequest = originalRequest; } - private final HttpServletRequest originalRequest; + private HttpServletRequest originalRequest; + + /** + * Returns the original HTTP servlet request for this portlet request. + * + * @since 7.2 + * @param request + * {@link PortletRequest} used to + * @return the original HTTP servlet request + */ + protected abstract HttpServletRequest getServletRequest( + PortletRequest request); + + private HttpServletRequest getOriginalRequest() { + if (originalRequest == null) { + PortletRequest request = getRequest(); + originalRequest = getServletRequest(request); + } + + return originalRequest; + } @Override public String getParameter(String name) { String parameter = super.getParameter(name); if (parameter == null) { - parameter = originalRequest.getParameter(name); + parameter = getOriginalRequest().getParameter(name); } return parameter; } @Override public String getRemoteAddr() { - return originalRequest.getRemoteAddr(); + return getOriginalRequest().getRemoteAddr(); } @Override public String getRemoteHost() { - return originalRequest.getRemoteHost(); + return getOriginalRequest().getRemoteHost(); } @Override public int getRemotePort() { - return originalRequest.getRemotePort(); + return getOriginalRequest().getRemotePort(); } @Override public String getHeader(String name) { String header = super.getHeader(name); if (header == null) { - header = originalRequest.getHeader(name); + header = getOriginalRequest().getHeader(name); } return header; } @@ -114,7 +137,7 @@ public class VaadinPortlet extends GenericPortlet implements Constants, public Enumeration<String> getHeaderNames() { Enumeration<String> headerNames = super.getHeaderNames(); if (headerNames == null) { - headerNames = originalRequest.getHeaderNames(); + headerNames = getOriginalRequest().getHeaderNames(); } return headerNames; } @@ -123,7 +146,7 @@ public class VaadinPortlet extends GenericPortlet implements Constants, public Enumeration<String> getHeaders(String name) { Enumeration<String> headers = super.getHeaders(name); if (headers == null) { - headers = originalRequest.getHeaders(name); + headers = getOriginalRequest().getHeaders(name); } return headers; } @@ -132,64 +155,21 @@ public class VaadinPortlet extends GenericPortlet implements Constants, public Map<String, String[]> getParameterMap() { Map<String, String[]> parameterMap = super.getParameterMap(); if (parameterMap == null) { - parameterMap = originalRequest.getParameterMap(); + parameterMap = getOriginalRequest().getParameterMap(); } return parameterMap; } } - public static class VaadinGateinRequest extends VaadinHttpAndPortletRequest { - public VaadinGateinRequest(PortletRequest request, - VaadinPortletService vaadinService) { - super(request, getOriginalRequest(request), vaadinService); - } - - private static final HttpServletRequest getOriginalRequest( - PortletRequest request) { - try { - Method getRealReq = request.getClass().getMethod( - "getRealRequest"); - HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq - .invoke(request); - return origRequest; - } catch (Exception e) { - throw new IllegalStateException("GateIn request not detected", - e); - } - } - } - - // Intentionally internal, will be refactored out in 7.2. - static class WebSpherePortalRequest extends VaadinHttpAndPortletRequest { - - public WebSpherePortalRequest(PortletRequest request, - VaadinPortletService vaadinService) { - super(request, getServletRequest(request), vaadinService); - } - - private static HttpServletRequest getServletRequest( - PortletRequest request) { - try { - Class<?> portletUtils = Class - .forName("com.ibm.ws.portletcontainer.portlet.PortletUtils"); - Method getHttpServletRequest = portletUtils.getMethod( - "getHttpServletRequest", PortletRequest.class); - - return (HttpServletRequest) getHttpServletRequest.invoke(null, - request); - } catch (Exception e) { - throw new IllegalStateException( - "WebSphere Portal request not detected."); - } - } - } - + /** + * Portlet request for Liferay. + */ public static class VaadinLiferayRequest extends VaadinHttpAndPortletRequest { public VaadinLiferayRequest(PortletRequest request, VaadinPortletService vaadinService) { - super(request, getOriginalRequest(request), vaadinService); + super(request, vaadinService); } @Override @@ -219,7 +199,7 @@ public class VaadinPortlet extends GenericPortlet implements Constants, * @throws Exception * @return return value of the invoked method */ - private static Object invokeStaticLiferayMethod(String className, + private Object invokeStaticLiferayMethod(String className, String methodName, Object argument, String parameterClassName) throws Exception { Thread currentThread = Thread.currentThread(); @@ -251,8 +231,8 @@ public class VaadinPortlet extends GenericPortlet implements Constants, } } - private static HttpServletRequest getOriginalRequest( - PortletRequest request) { + @Override + protected HttpServletRequest getServletRequest(PortletRequest request) { try { // httpRequest = PortalUtil.getHttpServletRequest(request); HttpServletRequest httpRequest = (HttpServletRequest) invokeStaticLiferayMethod( @@ -272,10 +252,68 @@ public class VaadinPortlet extends GenericPortlet implements Constants, e); } } + } + + /** + * Portlet request for GateIn. + */ + public static class VaadinGateInRequest extends VaadinHttpAndPortletRequest { + public VaadinGateInRequest(PortletRequest request, + VaadinPortletService vaadinService) { + super(request, vaadinService); + } + + @Override + protected HttpServletRequest getServletRequest(PortletRequest request) { + try { + Method getRealReq = request.getClass().getMethod( + "getRealRequest"); + HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq + .invoke(request); + return origRequest; + } catch (Exception e) { + throw new IllegalStateException("GateIn request not detected", + e); + } + } + } + /** + * Portlet request for WebSphere Portal. + */ + public static class VaadinWebSpherePortalRequest extends + VaadinHttpAndPortletRequest { + + public VaadinWebSpherePortalRequest(PortletRequest request, + VaadinPortletService vaadinService) { + super(request, vaadinService); + } + + @Override + protected HttpServletRequest getServletRequest(PortletRequest request) { + try { + Class<?> portletUtils = Class + .forName("com.ibm.ws.portletcontainer.portlet.PortletUtils"); + Method getHttpServletRequest = portletUtils.getMethod( + "getHttpServletRequest", PortletRequest.class); + + return (HttpServletRequest) getHttpServletRequest.invoke(null, + request); + } catch (Exception e) { + throw new IllegalStateException( + "WebSphere Portal request not detected."); + } + } } /** + * @deprecated As of 7.0. Will likely change or be removed in a future + * version + */ + @Deprecated + public static final String RESOURCE_URL_ID = "APP"; + + /** * This portlet parameter is used to add styles to the main element. E.g * "height:500px" generates a style="height:500px" to the main element. * @@ -390,7 +428,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)) { @@ -444,50 +481,26 @@ public class VaadinPortlet extends GenericPortlet implements Constants, * * @param request * The original PortletRequest - * @return A wrapped version of the PorletRequest + * @return A wrapped version of the PortletRequest */ protected VaadinPortletRequest createVaadinRequest(PortletRequest request) { - if (isLiferay(request)) { - return new VaadinLiferayRequest(request, getService()); - } else if (isGateIn(request)) { - return new VaadinGateinRequest(request, getService()); - } else if (isWebSphere(request)) { - return new WebSpherePortalRequest(request, getService()); - } else { + PortalContext portalContext = request.getPortalContext(); + String portalInfo = portalContext.getPortalInfo().toLowerCase().trim(); + VaadinPortletService service = getService(); - return new VaadinPortletRequest(request, getService()); + if (portalInfo.contains("gatein")) { + return new VaadinGateInRequest(request, service); } - } - /** - * Returns true if the portlet request is from Liferay. - * - * @param request - * @return True if Liferay, false otherwise - */ - private static boolean isLiferay(PortletRequest request) { - String portalInfo = request.getPortalContext().getPortalInfo() - .toLowerCase(); - return portalInfo.contains("liferay"); - } - - /** - * Returns true if the portlet request if from GateIn - * - * @param request - * @return True if GateIn, false otherwise - */ - private static boolean isGateIn(PortletRequest request) { - String portalInfo = request.getPortalContext().getPortalInfo() - .toLowerCase(); - return portalInfo.contains("gatein"); - } + if (portalInfo.contains("liferay")) { + return new VaadinLiferayRequest(request, service); + } - private static boolean isWebSphere(PortletRequest request) { - String portalInfo = request.getPortalContext().getPortalInfo() - .toLowerCase(); + if (portalInfo.contains("websphere portal")) { + return new VaadinWebSpherePortalRequest(request, service); + } - return portalInfo.contains("websphere portal"); + return new VaadinPortletRequest(request, service); } private VaadinPortletResponse createVaadinResponse(PortletResponse response) { @@ -538,6 +551,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 131d84baa6..f2181c7c6f 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; @@ -51,6 +53,7 @@ import org.json.JSONObject; import com.vaadin.annotations.PreserveOnRefresh; import com.vaadin.event.EventRouter; import com.vaadin.server.VaadinSession.FutureAccess; +import com.vaadin.server.VaadinSession.State; import com.vaadin.server.communication.FileUploadHandler; import com.vaadin.server.communication.HeartbeatHandler; import com.vaadin.server.communication.PublishedFileHandler; @@ -97,6 +100,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}. */ @@ -440,6 +447,12 @@ public abstract class VaadinService implements Serializable { session.accessSynchronously(new Runnable() { @Override public void run() { + if (session.getState() == State.CLOSED) { + return; + } + if (session.getState() == State.OPEN) { + closeSession(session); + } ArrayList<UI> uis = new ArrayList<UI>(session.getUIs()); for (final UI ui : uis) { ui.accessSynchronously(new Runnable() { @@ -463,6 +476,8 @@ public abstract class VaadinService implements Serializable { // destroy listeners eventRouter.fireEvent(new SessionDestroyEvent( VaadinService.this, session), session.getErrorHandler()); + + session.setState(State.CLOSED); } }); } @@ -1121,7 +1136,7 @@ public abstract class VaadinService implements Serializable { closeInactiveUIs(session); removeClosedUIs(session); } else { - if (!session.isClosing()) { + if (session.getState() == State.OPEN) { closeSession(session); if (session.getSession() != null) { getLogger().log(Level.FINE, "Closing inactive session {0}", @@ -1273,7 +1288,7 @@ public abstract class VaadinService implements Serializable { * @return true if the session is active, false if it could be closed. */ private boolean isSessionActive(VaadinSession session) { - if (session.isClosing() || session.getSession() == null) { + if (session.getState() != State.OPEN || session.getSession() == null) { return false; } else { long now = System.currentTimeMillis(); @@ -1571,9 +1586,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) { @@ -1629,15 +1644,33 @@ public abstract class VaadinService implements Serializable { * if the current thread holds the lock for another session */ public static void verifyNoOtherSessionLocked(VaadinSession session) { - VaadinSession otherSession = VaadinSession.getCurrent(); - if (otherSession != null && otherSession != session - && otherSession.hasLock()) { + if (isOtherSessionLocked(session)) { throw new IllegalStateException( "Can't access session while another session is locked by the same thread. This restriction is intended to help avoid deadlocks."); } } /** + * Checks whether there might be some {@link VaadinSession} other than the + * provided one for which the current thread holds a lock. This method might + * not detect all cases where some other session is locked, but it should + * cover the most typical situations. + * + * @since 7.2 + * @param session + * the session that is expected to be locked + * @return <code>true</code> if another session is also locked by the + * current thread; <code>false</code> if no such session was found + */ + public static boolean isOtherSessionLocked(VaadinSession session) { + VaadinSession otherSession = VaadinSession.getCurrent(); + if (otherSession == null || otherSession == session) { + return false; + } + return otherSession.hasLock(); + } + + /** * Verifies that the given CSRF token (aka double submit cookie) is valid * for the given session. This is used to protect against Cross Site Request * Forgery attacks. @@ -1778,4 +1811,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 15f243d6f7..81c3f374ea 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; @@ -259,14 +258,22 @@ public class VaadinServlet extends HttpServlet implements Constants { */ protected boolean handleContextRootWithoutSlash(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Query parameters like "?a=b" are handled by the servlet container but + // path parameter (e.g. ;jsessionid=) needs to be handled here + String location = request.getRequestURI(); + + String lastPathParameter = getLastPathParameter(location); + location = location.substring(0, + location.length() - lastPathParameter.length()); + if ((request.getPathInfo() == null || "/".equals(request.getPathInfo())) && "".equals(request.getServletPath()) - && !request.getRequestURI().endsWith("/")) { + && !location.endsWith("/")) { /* * Path info is for the root but request URI doesn't end with a * slash -> redirect to the same URI but with an ending slash. */ - String location = request.getRequestURI() + "/"; + location = location + "/" + lastPathParameter; String queryString = request.getQueryString(); if (queryString != null) { location += '?' + queryString; @@ -278,6 +285,40 @@ public class VaadinServlet extends HttpServlet implements Constants { } } + /** + * Finds any path parameter added to the last part of the uri. A path + * parameter is any string separated by ";" from the path and ends in / or + * at the end of the string. + * <p> + * For example the uri http://myhost.com/foo;a=1/bar;b=1 contains two path + * parameters, {@literal a=1} related to {@literal /foo} and {@literal b=1} + * related to /bar. + * <p> + * For http://myhost.com/foo;a=1/bar;b=1 this method will return ;b=1 + * + * @since 7.2 + * @param uri + * a URI + * @return the last path parameter of the uri including the semicolon or an + * empty string. Never null. + */ + protected static String getLastPathParameter(String uri) { + int lastPathStart = uri.lastIndexOf('/'); + if (lastPathStart == -1) { + return ""; + } + + int semicolonPos = uri.indexOf(';', lastPathStart); + if (semicolonPos < 0) { + // No path parameter for the last part + return ""; + } else { + // This includes the semicolon. + String semicolonString = uri.substring(semicolonPos); + return semicolonString; + } + } + private VaadinServletResponse createVaadinResponse( HttpServletResponse response) { return new VaadinServletResponse(response, getService()); @@ -670,21 +711,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 +724,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. * @@ -826,7 +894,7 @@ public class VaadinServlet extends HttpServlet implements Constants { // cache it response.setHeader("Cache-Control", "no-cache"); final String mimetype = getService().getMimeType(filename); - writeResponse(response, mimetype, scss.toString()); + writeResponse(response, mimetype, scss.printState()); return true; } @@ -982,20 +1050,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 +1131,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 a2f8febbca..bbabd881f8 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; @@ -165,6 +167,33 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } /** + * The lifecycle state of a VaadinSession. + * + * @since 7.2 + */ + public enum State { + /** + * The session is active and accepting client requests. + */ + OPEN, + /** + * The {@link VaadinSession#close() close} method has been called; the + * session will be closed as soon as the current request ends. + */ + CLOSING, + /** + * The session is closed; all the {@link UI}s have been removed and + * {@link SessionDestroyListener}s have been called. + */ + CLOSED; + + private boolean isValidChange(State newState) { + return (this == OPEN && newState == CLOSING) + || (this == CLOSING && newState == CLOSED); + } + } + + /** * The name of the parameter that is by default used in e.g. web.xml to * define the name of the default {@link UI} class. */ @@ -205,7 +234,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(); @@ -213,6 +242,8 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { protected WebBrowser browser = new WebBrowser(); + private DragAndDropService dragAndDropService; + private LegacyCommunicationManager communicationManager; private long cumulativeRequestDuration = 0; @@ -221,7 +252,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { private long lastRequestTimestamp = System.currentTimeMillis(); - private boolean closing = false; + private State state = State.OPEN; private transient WrappedSession session; @@ -275,24 +306,20 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } else if (VaadinService.getCurrentRequest() != null && getCurrent() == this) { assert hasLock(); - /* - * Ignore if the session is being moved to a different backing - * session or if GAEVaadinServlet is doing its normal cleanup. - */ + // Ignore if the session is being moved to a different backing + // session or if GAEVaadinServlet is doing its normal cleanup. if (getAttribute(VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE) == Boolean.TRUE) { return; } // There is still a request in progress for this session. The // session will be destroyed after the response has been written. - if (!isClosing()) { + if (getState() == State.OPEN) { close(); } } else { - /* - * We are not in a request related to this session so we can - * immediately destroy it - */ + // We are not in a request related to this session so we can destroy + // it as soon as we acquire the lock. service.fireSessionDestroy(this); } session = null; @@ -393,6 +420,13 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { return communicationManager; } + public DragAndDropService getDragAndDropService() { + if (dragAndDropService == null) { + dragAndDropService = new DragAndDropService(this); + } + return dragAndDropService; + } + /** * Loads the VaadinSession for the given service and WrappedSession from the * HTTP session. @@ -424,6 +458,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 +888,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); + } } /** @@ -1099,20 +1162,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 +1178,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(); + } + } } /** @@ -1186,19 +1249,52 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ public void close() { assert hasLock(); - closing = true; + state = State.CLOSING; } /** - * Returns whether this session is marked to be closed. + * Returns whether this session is marked to be closed. Note that this + * method also returns true if the session is actually already closed. * * @see #close() * + * @deprecated As of 7.2, use + * <code>{@link #getState() getState() != State.OPEN}</code> + * instead. + * * @return true if this session is marked to be closed, false otherwise */ + @Deprecated public boolean isClosing() { assert hasLock(); - return closing; + return state == State.CLOSING || state == State.CLOSED; + } + + /** + * Returns the lifecycle state of this session. + * + * @since 7.2 + * @return the current state + */ + public State getState() { + assert hasLock(); + return state; + } + + /** + * Sets the lifecycle state of this session. The allowed transitions are + * OPEN to CLOSING and CLOSING to CLOSED. + * + * @since 7.2 + * @param state + * the new state + */ + protected void setState(State state) { + assert hasLock(); + assert this.state.isValidChange(state) : "Invalid session state change " + + this.state + "->" + state; + + this.state = state; } private static final Logger getLogger() { @@ -1340,4 +1436,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/WrappedHttpSession.java b/server/src/com/vaadin/server/WrappedHttpSession.java index a2cc7d001b..137ccf2cdd 100644 --- a/server/src/com/vaadin/server/WrappedHttpSession.java +++ b/server/src/com/vaadin/server/WrappedHttpSession.java @@ -85,6 +85,20 @@ public class WrappedHttpSession implements WrappedSession { @Override public void invalidate() { + if (session == null) { + throw new IllegalStateException( + "Session is null and cannot be invalidated"); + } + + if (session.getClass().getName() + .equals("org.atmosphere.util.FakeHttpSession")) { + throw new UnsupportedOperationException( + "FakeHttpSession cannot be invalidated. " + + "This typically means you are using websockets together with Tomcat 7. " + + "Because Tomcat 7 does not support sharing the HTTP session between standard HTTP requests and websockets, a copy of the session is used for websockets. " + + "Invalidating this session does not have the desired effect. " + + "To resolve this, upgrade to Tomcat 8 or use another transport mechanism than websockets."); + } session.invalidate(); } diff --git a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java index 11563b0c7c..4727720f4b 100644 --- a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java +++ b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java @@ -30,14 +30,13 @@ 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,52 +91,83 @@ 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; + private Future<Object> outgoingMessage; - 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()); @@ -157,7 +187,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 +209,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); + } } /** @@ -229,15 +287,12 @@ public class AtmospherePushConnection implements PushConnection { outgoingMessage = null; } - resource.resume(); resource = null; + state = State.DISCONNECTED; } - /** - * @since - * @return - */ private static Logger getLogger() { return Logger.getLogger(AtmospherePushConnection.class.getName()); } + } 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..429941abfd --- /dev/null +++ b/server/src/com/vaadin/server/communication/DateSerializer.java @@ -0,0 +1,42 @@ +/* + * 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.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 38b78de02c..22c6a76106 100644 --- a/server/src/com/vaadin/server/communication/FileUploadHandler.java +++ b/server/src/com/vaadin/server/communication/FileUploadHandler.java @@ -271,8 +271,7 @@ public class FileUploadHandler implements RequestHandler { return true; } - source = session.getCommunicationManager().getConnector(uI, - connectorId); + source = uI.getConnectorTracker().getConnector(connectorId); } finally { session.unlock(); } @@ -287,7 +286,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; } @@ -339,7 +338,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; @@ -406,9 +405,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(); @@ -477,7 +489,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 @@ -507,7 +519,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( @@ -515,7 +527,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 3ef7974d94..c6711d1d18 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 3f76eafe35..52efcfcd89 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 dd494c06ca..c6126f9d21 100644 --- a/server/src/com/vaadin/server/communication/PushHandler.java +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -18,8 +18,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; @@ -29,6 +27,7 @@ import org.atmosphere.cpr.AtmosphereResource; import org.atmosphere.cpr.AtmosphereResource.TRANSPORT; import org.atmosphere.cpr.AtmosphereResourceEvent; import org.atmosphere.cpr.AtmosphereResourceEventListenerAdapter; +import org.atmosphere.handler.AbstractReflectorAtmosphereHandler; import org.json.JSONException; import com.vaadin.server.ErrorEvent; @@ -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; @@ -54,8 +52,31 @@ import com.vaadin.ui.UI; * @author Vaadin Ltd * @since 7.1 */ -public class PushHandler extends AtmosphereResourceEventListenerAdapter - implements AtmosphereHandler { +public class PushHandler extends AtmosphereResourceEventListenerAdapter { + + AtmosphereHandler handler = new AbstractReflectorAtmosphereHandler() { + + @Override + public void onStateChange(AtmosphereResourceEvent event) + throws IOException { + super.onStateChange(event); + if (event.isCancelled() || event.isResumedOnTimeout()) { + disconnect(event); + } + } + + @Override + public void onRequest(AtmosphereResource resource) { + AtmosphereRequest req = resource.getRequest(); + + if (req.getMethod().equalsIgnoreCase("GET")) { + callWithUi(resource, establishCallback); + } else if (req.getMethod().equalsIgnoreCase("POST")) { + callWithUi(resource, receiveCallback); + } + } + + }; /** * Callback interface used internally to process an event with the @@ -75,8 +96,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 +105,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 +128,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); } }; @@ -174,14 +186,46 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter } }; - private static final String LONG_PADDING; + /** + * Callback used when a connection is closed, either deliberately or because + * an error occurred. + */ + private final PushEventCallback disconnectCallback = new PushEventCallback() { + @Override + public void run(AtmosphereResource resource, UI ui) throws IOException { + PushMode pushMode = ui.getPushConfiguration().getPushMode(); + AtmospherePushConnection connection = getConnectionForUI(ui); - static { - char[] array = new char[4096]; - Arrays.fill(array, '-'); - LONG_PADDING = String.copyValueOf(array); + String id = resource.uuid(); + + if (connection == null) { + getLogger() + .log(Level.WARNING, + "Could not find push connection to close: {0} with transport {1}", + new Object[] { id, resource.transport() }); + } else { + if (!pushMode.isEnabled()) { + /* + * The client is expected to close the connection after push + * mode has been set to disabled. + */ + getLogger().log(Level.FINER, + "Connection closed for resource {0}", id); + } else { + /* + * Unexpected cancel, e.g. if the user closes the browser + * tab. + */ + getLogger() + .log(Level.FINER, + "Connection unexpectedly closed for resource {0} with transport {1}", + new Object[] { id, resource.transport() }); + } + connection.disconnect(); + } + } + }; - } private VaadinServletService service; public PushHandler(VaadinServletService service) { @@ -297,66 +341,12 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter } } - @Override - public void onRequest(AtmosphereResource resource) { - AtmosphereRequest req = resource.getRequest(); - - if (req.getMethod().equalsIgnoreCase("GET")) { - callWithUi(resource, establishCallback); - } else if (req.getMethod().equalsIgnoreCase("POST")) { - callWithUi(resource, receiveCallback); - } - } - private static AtmospherePushConnection getConnectionForUI(UI ui) { PushConnection pushConnection = ui.getPushConnection(); if (pushConnection instanceof AtmospherePushConnection) { - assert pushConnection.isConnected(); return (AtmospherePushConnection) pushConnection; - } - return null; - } - - @Override - public void onStateChange(AtmosphereResourceEvent event) throws IOException { - AtmosphereResource resource = event.getResource(); - - String id = resource.uuid(); - if (event.isCancelled() || event.isResumedOnTimeout()) { - getLogger().log(Level.FINER, - "Cancelled connection for resource {0}", id); - disconnect(event); - } else if (event.isResuming()) { - // A connection that was suspended earlier was resumed (committed to - // the client.) Should only happen if the transport is JSONP or - // long-polling. - getLogger().log(Level.FINER, "Resuming request for resource {0}", - id); } else { - // A message was broadcast to this resource and should be sent to - // the client. We don't do any actual broadcasting, in the sense of - // sending to multiple recipients; any UIDL message is specific to a - // single client. - getLogger().log(Level.FINER, "Writing message to resource {0}", id); - - Writer writer = resource.getResponse().getWriter(); - writer.write(event.getMessage().toString()); - - switch (resource.transport()) { - case WEBSOCKET: - break; - case SSE: - case STREAMING: - writer.flush(); - break; - case JSONP: - case LONG_POLLING: - resource.resume(); - break; - default: - getLogger().log(Level.SEVERE, "Unknown transport {0}", - resource.transport()); - } + return null; } } @@ -374,17 +364,6 @@ public class PushHandler extends AtmosphereResourceEventListenerAdapter disconnect(event); } - @Override - public void onResume(AtmosphereResourceEvent event) { - // Log event on trace level - super.onResume(event); - disconnect(event); - } - - @Override - public void destroy() { - } - private void disconnect(AtmosphereResourceEvent event) { // We don't want to use callWithUi here, as it assumes there's a client // request active and does requestStart and requestEnd among other @@ -480,8 +459,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 67f7575a87..db14e73c1a 100644 --- a/server/src/com/vaadin/server/communication/PushRequestHandler.java +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -21,6 +21,7 @@ import java.io.IOException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; +import org.atmosphere.cache.UUIDBroadcasterCache; import org.atmosphere.client.TrackMessageSizeInterceptor; import org.atmosphere.cpr.ApplicationConfig; import org.atmosphere.cpr.AtmosphereFramework; @@ -29,6 +30,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; @@ -54,6 +57,12 @@ public class PushRequestHandler implements RequestHandler, private AtmosphereFramework atmosphere; private PushHandler pushHandler; + /** + * Atmosphere 2.x has a race condition when AtmosphereFramework init(config) + * is run from two threads at once. See http://dev.vaadin.com/ticket/13528 + */ + private static Object atmosphereInitRaceConditionWorkaroundLock = new Object(); + public PushRequestHandler(VaadinServletService service) throws ServiceException { @@ -75,36 +84,49 @@ public class PushRequestHandler implements RequestHandler, } }; - pushHandler = new PushHandler(service); - atmosphere.addAtmosphereHandler("/*", pushHandler); - atmosphere.addInitParameter(ApplicationConfig.PROPERTY_SESSION_SUPPORT, - "true"); - atmosphere.addInitParameter(ApplicationConfig.MESSAGE_DELIMITER, - String.valueOf(PushConstants.MESSAGE_DELIMITER)); - - final String bufferSize = String - .valueOf(PushConstants.WEBSOCKET_BUFFER_SIZE); - atmosphere.addInitParameter(ApplicationConfig.WEBSOCKET_BUFFER_SIZE, - bufferSize); - atmosphere.addInitParameter(ApplicationConfig.WEBSOCKET_MAXTEXTSIZE, - bufferSize); - atmosphere.addInitParameter(ApplicationConfig.WEBSOCKET_MAXBINARYSIZE, - bufferSize); - - // Disable Atmosphere's message about commercial support - atmosphere.addInitParameter("org.atmosphere.cpr.showSupportMessage", - "false"); - - try { - atmosphere.init(config); - - // Ensure the client-side knows how to split the message stream - // into individual messages when using certain transports - AtmosphereInterceptor trackMessageSize = new TrackMessageSizeInterceptor(); - trackMessageSize.configure(atmosphere.getAtmosphereConfig()); - atmosphere.interceptor(trackMessageSize); - } catch (ServletException e) { - throw new ServiceException("Atmosphere init failed", e); + service.addServiceDestroyListener(new ServiceDestroyListener() { + @Override + public void serviceDestroy(ServiceDestroyEvent event) { + destroy(); + } + }); + + synchronized (atmosphereInitRaceConditionWorkaroundLock) { + pushHandler = new PushHandler(service); + atmosphere.addAtmosphereHandler("/*", pushHandler.handler); + atmosphere.addInitParameter(ApplicationConfig.BROADCASTER_CACHE, + UUIDBroadcasterCache.class.getName()); + atmosphere.addInitParameter( + ApplicationConfig.PROPERTY_SESSION_SUPPORT, "true"); + atmosphere.addInitParameter(ApplicationConfig.MESSAGE_DELIMITER, + String.valueOf(PushConstants.MESSAGE_DELIMITER)); + + final String bufferSize = String + .valueOf(PushConstants.WEBSOCKET_BUFFER_SIZE); + atmosphere.addInitParameter( + ApplicationConfig.WEBSOCKET_BUFFER_SIZE, bufferSize); + atmosphere.addInitParameter( + ApplicationConfig.WEBSOCKET_MAXTEXTSIZE, bufferSize); + atmosphere.addInitParameter( + ApplicationConfig.WEBSOCKET_MAXBINARYSIZE, bufferSize); + atmosphere.addInitParameter( + ApplicationConfig.PROPERTY_ALLOW_SESSION_TIMEOUT_REMOVAL, + "false"); + // Disable Atmosphere's message about commercial support + atmosphere.addInitParameter( + "org.atmosphere.cpr.showSupportMessage", "false"); + + try { + atmosphere.init(config); + + // Ensure the client-side knows how to split the message stream + // into individual messages when using certain transports + AtmosphereInterceptor trackMessageSize = new TrackMessageSizeInterceptor(); + trackMessageSize.configure(atmosphere.getAtmosphereConfig()); + atmosphere.interceptor(trackMessageSize); + } catch (ServletException e) { + throw new ServiceException("Atmosphere init failed", e); + } } } diff --git a/server/src/com/vaadin/server/communication/ServerRpcHandler.java b/server/src/com/vaadin/server/communication/ServerRpcHandler.java index d875347633..89d87567d7 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)); + } + + /** + * 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; + } - public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + /** + * 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,57 +150,65 @@ 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(); try { + ConnectorTracker connectorTracker = uI.getConnectorTracker(); + 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()); + final ClientConnector connector = connectorTracker + .getConnector(invocation.getConnectorId()); if (connector != null && connector.isConnectorEnabled()) { enabledConnectors.add(connector); @@ -150,8 +218,8 @@ public class ServerRpcHandler implements Serializable { for (int i = 0; i < invocations.size(); i++) { MethodInvocation invocation = invocations.get(i); - final ClientConnector connector = manager.getConnector(uI, - invocation.getConnectorId()); + final ClientConnector connector = connectorTracker + .getConnector(invocation.getConnectorId()); if (connector == null) { getLogger() .log(Level.WARNING, @@ -243,21 +311,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 +336,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 +351,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 +360,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 +471,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 f71dcc91ee..cf0de8e9ee 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 - - // Special parameter sent by vaadinBootstrap.js - String windowName = request.getParameter("v-wn"); - - Map<String, Integer> retainOnRefreshUIs = session - .getPreserveOnRefreshUIs(); - if (windowName != null && !retainOnRefreshUIs.isEmpty()) { - // Check for a known UI + // Check for an existing UI based on embed id - Integer retainedUIId = retainOnRefreshUIs.get(windowName); + String embedId = getEmbedId(request); - 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}. * @@ -248,12 +266,7 @@ public abstract class UIInitHandler extends SynchronizedRequestHandler { */ private void reinitUI(UI ui, VaadinRequest request) { UI.setCurrent(ui); - - // Fire fragment change if the fragment has changed - String location = request.getParameter("v-loc"); - if (location != null) { - ui.getPage().updateLocation(location); - } + ui.doRefresh(request); } /** diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java index 4e71685805..0d8ddb7bc7 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 af035e619c..00522e2aa5 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, @@ -281,9 +285,7 @@ public class UidlWriter implements Serializable { + new JSONArray(styleDependencies).toString()); } - if (manager.getDragAndDropService() != null) { - manager.getDragAndDropService().printJSONResponse(writer); - } + session.getDragAndDropService().printJSONResponse(writer); for (ClientConnector connector : dirtyVisibleConnectors) { uiConnectorTracker.markClientSideInitialized(connector); diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java index 1215891056..d0406c3eb7 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 dbe3a386e6..47ac953319 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/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java index 59d53f77ab..039c87333e 100644 --- a/server/src/com/vaadin/ui/AbstractOrderedLayout.java +++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -213,8 +213,12 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements if (oldLocation == -1) { addComponent(newComponent); } else if (newLocation == -1) { + Alignment alignment = getComponentAlignment(oldComponent); + float expandRatio = getExpandRatio(oldComponent); + removeComponent(oldComponent); addComponent(newComponent, oldLocation); + applyLayoutSettings(newComponent, alignment, expandRatio); } else { // Both old and new are in the layout if (oldLocation > newLocation) { @@ -444,4 +448,10 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements defaultComponentAlignment = defaultAlignment; } + private void applyLayoutSettings(Component target, Alignment alignment, + float expandRatio) { + setComponentAlignment(target, alignment); + setExpandRatio(target, expandRatio); + } + } diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java index ec11895762..c6abb8e685 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/AbstractSingleComponentContainer.java b/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java index 0a606183a6..de1bb29846 100644 --- a/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java +++ b/server/src/com/vaadin/ui/AbstractSingleComponentContainer.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.Iterator; import com.vaadin.server.ComponentSizeValidator; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; /** * Abstract base class for component containers that have only one child @@ -150,6 +152,19 @@ public abstract class AbstractSingleComponentContainer extends // TODO move utility method elsewhere? public static void removeFromParent(Component content) throws IllegalArgumentException { + // Verify the appropriate session is locked + UI parentUI = content.getUI(); + if (parentUI != null) { + VaadinSession parentSession = parentUI.getSession(); + if (parentSession != null && !parentSession.hasLock()) { + String message = "Cannot remove from parent when the session is not locked."; + if (VaadinService.isOtherSessionLocked(parentSession)) { + message += " Furthermore, there is another locked session, indicating that the component might be about to be moved from one session to another."; + } + throw new IllegalStateException(message); + } + } + HasComponents parent = content.getParent(); if (parent instanceof ComponentContainer) { // If the component already has a parent, try to remove it diff --git a/server/src/com/vaadin/ui/Accordion.java b/server/src/com/vaadin/ui/Accordion.java index 8ecf33f291..1d53937d24 100644 --- a/server/src/com/vaadin/ui/Accordion.java +++ b/server/src/com/vaadin/ui/Accordion.java @@ -15,6 +15,8 @@ */ package com.vaadin.ui; +import com.vaadin.shared.ui.accordion.AccordionState; + /** * An accordion is a component similar to a {@link TabSheet}, but with a * vertical orientation and the selected component presented between tabs. @@ -46,4 +48,14 @@ public class Accordion extends TabSheet { addComponents(components); } + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.TabSheet#getState() + */ + @Override + protected AccordionState getState() { + return (AccordionState) super.getState(); + } + } diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java index bec39c3efe..5a5d03a3ee 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 071f215d1e..048726dc84 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; @@ -105,22 +103,30 @@ public class ComboBox extends AbstractSelect implements private boolean textInputAllowed = true; public ComboBox() { - setNewItemsAllowed(false); + initDefaults(); } public ComboBox(String caption, Collection<?> options) { super(caption, options); - setNewItemsAllowed(false); + initDefaults(); } public ComboBox(String caption, Container dataSource) { super(caption, dataSource); - setNewItemsAllowed(false); + initDefaults(); } public ComboBox(String caption) { super(caption); + initDefaults(); + } + + /** + * Initialize the ComboBox with default settings + */ + private void initDefaults() { setNewItemsAllowed(false); + setImmediate(true); } /** diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java index b3a343804a..e10b5e1cd9 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 c0b60e276d..ce8c452c2c 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; @@ -34,6 +35,7 @@ import org.json.JSONObject; import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.ClientConnector; +import com.vaadin.server.DragAndDropService; import com.vaadin.server.GlobalResourceHandler; import com.vaadin.server.LegacyCommunicationManager; import com.vaadin.server.StreamVariable; @@ -81,6 +83,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 +182,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)) { @@ -251,8 +272,16 @@ public class ConnectorTracker implements Serializable { // Ignore connectors that have been unregistered but not yet cleaned up if (unregisteredConnectors.contains(connector)) { return null; + } else if (connector != null) { + return connector; + } else { + DragAndDropService service = uI.getSession() + .getDragAndDropService(); + if (connectorId.equals(service.getConnectorId())) { + return service; + } } - return connector; + return null; } /** @@ -570,12 +599,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 +622,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; } @@ -601,7 +644,7 @@ public class ConnectorTracker implements Serializable { stringDiffStates.put(key, diffStates.get(key).toString()); } out.writeObject(stringDiffStates); - }; + } /* Special serialization to JSONObjects which are not serializable */ private void readObject(java.io.ObjectInputStream in) throws IOException, @@ -732,4 +775,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 224c0dc941..cb94a774a5 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 71298289f4..b4685adcea 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 841d7efe50..2731e93ef2 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 46803802d6..aaf25a9e9d 100644 --- a/server/src/com/vaadin/ui/Notification.java +++ b/server/src/com/vaadin/ui/Notification.java @@ -63,7 +63,27 @@ import com.vaadin.shared.Position; */ public class Notification implements Serializable { public enum Type { - HUMANIZED_MESSAGE, WARNING_MESSAGE, ERROR_MESSAGE, TRAY_NOTIFICATION; + HUMANIZED_MESSAGE("humanized"), WARNING_MESSAGE("warning"), ERROR_MESSAGE( + "error"), TRAY_NOTIFICATION("tray"), + /** + * @since 7.2 + */ + ASSISTIVE_NOTIFICATION("assistive"); + + private String style; + + Type(String style) { + this.style = style; + } + + /** + * @since 7.2 + * + * @return the style name for this notification type. + */ + public String getStyle() { + return style; + } } @Deprecated @@ -186,25 +206,26 @@ public class Notification implements Serializable { } private void setType(Type type) { + styleName = type.getStyle(); switch (type) { case WARNING_MESSAGE: delayMsec = 1500; - styleName = "warning"; break; case ERROR_MESSAGE: delayMsec = -1; - styleName = "error"; break; case TRAY_NOTIFICATION: delayMsec = 3000; position = Position.BOTTOM_RIGHT; - styleName = "tray"; - + break; + case ASSISTIVE_NOTIFICATION: + delayMsec = 3000; + position = Position.ASSISTIVE; + break; case HUMANIZED_MESSAGE: default: break; } - } /** diff --git a/server/src/com/vaadin/ui/NotificationConfiguration.java b/server/src/com/vaadin/ui/NotificationConfiguration.java new file mode 100644 index 0000000000..e6d19f84f7 --- /dev/null +++ b/server/src/com/vaadin/ui/NotificationConfiguration.java @@ -0,0 +1,185 @@ +/* + * 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.NotificationRole; +import com.vaadin.shared.ui.ui.UIState.NotificationTypeConfiguration; +import com.vaadin.ui.Notification.Type; + +/** + * Provides methods for configuring the notification. + * + * @author Vaadin Ltd + * @since 7.2 + */ +public interface NotificationConfiguration extends Serializable { + /** + * Sets the accessibility prefix for a notification type. + * <p> + * 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 setAssistivePrefix(Type type, String prefix); + + /** + * Gets the accessibility prefix for a notification type. + * <p> + * 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 getAssistivePrefix(Type type); + + /** + * Sets the accessibility postfix for a notification type. + * <p> + * 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 setAssistivePostfix(Type type, String postfix); + + /** + * Gets the accessibility postfix for a notification type. + * <p> + * 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 getAssistivePostfix(Type type); + + /** + * Sets the WAI-ARIA role for a notification type. + * <p> + * 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 setAssistiveRole(Type type, NotificationRole role); + + /** + * Gets the WAI-ARIA role for a notification type. + * <p> + * 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>) + * <p> + * The default role is alert. + * + * @param type + * type of the notification + * @return role to set for the notification type + */ + public NotificationRole getAssistiveRole(Type type); +} + +class NotificationConfigurationImpl implements NotificationConfiguration { + + private UI ui; + + public NotificationConfigurationImpl(UI ui) { + this.ui = ui; + } + + @Override + public void setAssistivePrefix(Type type, String prefix) { + getConfigurationBean(type).prefix = prefix; + } + + @Override + public String getAssistivePrefix(Type type) { + NotificationTypeConfiguration styleSetup = getTypeConf(type); + if (styleSetup != null) { + return styleSetup.prefix; + } + + return null; + } + + @Override + public void setAssistivePostfix(Type type, String postfix) { + getConfigurationBean(type).postfix = postfix; + } + + @Override + public String getAssistivePostfix(Type type) { + NotificationTypeConfiguration styleSetup = getTypeConf(type); + if (styleSetup != null) { + return styleSetup.postfix; + } + + return null; + } + + @Override + public void setAssistiveRole(Type type, NotificationRole role) { + getConfigurationBean(type).notificationRole = role; + } + + @Override + public NotificationRole getAssistiveRole(Type type) { + NotificationTypeConfiguration styleSetup = getTypeConf(type); + if (styleSetup != null) { + return styleSetup.notificationRole; + } + + return null; + } + + private NotificationTypeConfiguration getConfigurationBean(Type type) { + NotificationTypeConfiguration styleSetup = getTypeConf(type); + if (styleSetup == null) { + styleSetup = new NotificationTypeConfiguration(); + ui.getState().notificationConfigurations.put(type.getStyle(), styleSetup); + } + + return styleSetup; + } + + private NotificationTypeConfiguration getTypeConf(Type type) { + return ui.getState().notificationConfigurations.get(type.getStyle()); + } +} diff --git a/server/src/com/vaadin/ui/PushConfiguration.java b/server/src/com/vaadin/ui/PushConfiguration.java index e68c72429b..84f59d0313 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 e417e36ddf..2fdb3b40a7 100644 --- a/server/src/com/vaadin/ui/TabSheet.java +++ b/server/src/com/vaadin/ui/TabSheet.java @@ -27,17 +27,18 @@ import java.util.Map; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; import com.vaadin.event.FieldEvents.FocusEvent; import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.event.FieldEvents.FocusNotifier; import com.vaadin.server.ErrorMessage; import com.vaadin.server.KeyMapper; -import com.vaadin.server.LegacyPaint; -import com.vaadin.server.PaintException; -import com.vaadin.server.PaintTarget; import com.vaadin.server.Resource; -import com.vaadin.shared.ui.tabsheet.TabsheetBaseConstants; -import com.vaadin.shared.ui.tabsheet.TabsheetConstants; +import com.vaadin.shared.ComponentConstants; +import com.vaadin.shared.ui.tabsheet.TabState; +import com.vaadin.shared.ui.tabsheet.TabsheetClientRpc; +import com.vaadin.shared.ui.tabsheet.TabsheetServerRpc; +import com.vaadin.shared.ui.tabsheet.TabsheetState; import com.vaadin.ui.Component.Focusable; import com.vaadin.ui.themes.Reindeer; import com.vaadin.ui.themes.Runo; @@ -70,7 +71,28 @@ import com.vaadin.ui.themes.Runo; * @since 3.0 */ public class TabSheet extends AbstractComponentContainer implements Focusable, - FocusNotifier, BlurNotifier, LegacyComponent, SelectiveRenderer { + FocusNotifier, BlurNotifier, SelectiveRenderer { + + /** + * Client to server RPC implementation for TabSheet. + * + * @since 7.2 + */ + protected class TabsheetServerRpcImpl implements TabsheetServerRpc { + + @Override + public void setSelected(String key) { + setSelectedTab(keyMapper.get(key)); + } + + @Override + public void closeTab(String key) { + final Component tab = keyMapper.get(key); + if (tab != null) { + closeHandler.onTabClose(TabSheet.this, tab); + } + } + } /** * List of component tabs (tab contents). In addition to being on this list, @@ -96,23 +118,20 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, private final KeyMapper<Component> keyMapper = new KeyMapper<Component>(); /** - * When true, the tab selection area is not displayed to the user. - */ - private boolean tabsHidden; - - /** * Handler to be called when a tab is closed. */ private CloseHandler closeHandler; - private int tabIndex; - /** * Constructs a new TabSheet. A TabSheet is immediate by default, and the * default close handler removes the tab being closed. */ public TabSheet() { super(); + + registerRpc(rpc); + registerRpc(focusBlurRpc); + // expand horizontally by default setWidth(100, UNITS_PERCENTAGE); setImmediate(true); @@ -167,18 +186,23 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * If the tab was selected, the first eligible (visible and enabled) * remaining tab is selected. * - * @param c + * @param component * the component to be removed. */ @Override - public void removeComponent(Component c) { - if (c != null && components.contains(c)) { - super.removeComponent(c); - keyMapper.remove(c); - components.remove(c); - tabs.remove(c); - if (c.equals(selected)) { + public void removeComponent(Component component) { + if (component != null && components.contains(component)) { + super.removeComponent(component); + keyMapper.remove(component); + components.remove(component); + + Tab removedTab = tabs.remove(component); + + getState().tabs + .remove(((TabSheetTabImpl) removedTab).getTabState()); + + if (component.equals(selected)) { if (components.isEmpty()) { setSelected(null); } else { @@ -281,7 +305,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * and icon and returns the corresponding (old) tab, preserving other tab * metadata like the position. * - * @param c + * @param tabComponent * 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 @@ -293,25 +317,28 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * the position at where the the tab should be added. * @return the created {@link Tab} */ - public Tab addTab(Component c, String caption, Resource icon, int position) { - if (c == null) { + public Tab addTab(Component tabComponent, String caption, Resource icon, int position) { + if (tabComponent == null) { return null; - } else if (tabs.containsKey(c)) { - Tab tab = tabs.get(c); + } else if (tabs.containsKey(tabComponent)) { + Tab tab = tabs.get(tabComponent); tab.setCaption(caption); tab.setIcon(icon); return tab; } else { - components.add(position, c); + components.add(position, tabComponent); - Tab tab = new TabSheetTabImpl(caption, icon); + TabSheetTabImpl tab = new TabSheetTabImpl( + keyMapper.key(tabComponent), caption, icon); + + getState().tabs.add(position, tab.getTabState()); + tabs.put(tabComponent, tab); - tabs.put(c, tab); if (selected == null) { - setSelected(c); + setSelected(tabComponent); fireSelectedTabChange(); } - super.addComponent(c); + super.addComponent(tabComponent); markAsDirty(); return tab; } @@ -337,20 +364,21 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * * If the tab sheet already contains the component, its tab is returned. * - * @param c + * @param component * the component to be added onto tab - should not be null. * @param position * The position where the tab should be added * @return the created {@link Tab} */ - public Tab addTab(Component c, int position) { - if (c == null) { - return null; - } else if (tabs.containsKey(c)) { - return tabs.get(c); - } else { - return addTab(c, c.getCaption(), c.getIcon(), position); + public Tab addTab(Component component, int position) { + Tab result = tabs.get(component); + + if (result == null) { + result = addTab(component, component.getCaption(), + component.getIcon(), position); } + + return result; } /** @@ -371,111 +399,26 @@ 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.getIconAlternateText(); } source.removeComponent(c); - addTab(c, caption, icon); - + Tab tab = addTab(c, caption, icon); + tab.setIconAlternateText(iconAltText); } } /** - * Paints the content of this component. - * - * @param target - * the paint target - * @throws PaintException - * if the paint operation failed. - */ - - @Override - public void paintContent(PaintTarget target) throws PaintException { - - if (areTabsHidden()) { - target.addAttribute("hidetabs", true); - } - - if (tabIndex != 0) { - target.addAttribute("tabindex", tabIndex); - } - - target.startTag("tabs"); - - for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { - final Component component = i.next(); - - Tab tab = tabs.get(component); - - target.startTag("tab"); - if (!tab.isEnabled() && tab.isVisible()) { - target.addAttribute( - TabsheetBaseConstants.ATTRIBUTE_TAB_DISABLED, true); - } - - if (!tab.isVisible()) { - target.addAttribute("hidden", true); - } - - if (tab.isClosable()) { - target.addAttribute("closable", true); - } - - // tab icon, caption and description, but used via - // VCaption.updateCaption(uidl) - final Resource icon = tab.getIcon(); - if (icon != null) { - target.addAttribute(TabsheetBaseConstants.ATTRIBUTE_TAB_ICON, - icon); - } - final String caption = tab.getCaption(); - if (caption != null && caption.length() > 0) { - target.addAttribute( - TabsheetBaseConstants.ATTRIBUTE_TAB_CAPTION, caption); - } - ErrorMessage tabError = tab.getComponentError(); - if (tabError != null) { - target.addAttribute( - TabsheetBaseConstants.ATTRIBUTE_TAB_ERROR_MESSAGE, - tabError.getFormattedHtmlMessage()); - } - final String description = tab.getDescription(); - if (description != null) { - target.addAttribute( - TabsheetBaseConstants.ATTRIBUTE_TAB_DESCRIPTION, - description); - } - - final String styleName = tab.getStyleName(); - if (styleName != null && styleName.length() != 0) { - target.addAttribute(TabsheetConstants.TAB_STYLE_NAME, styleName); - } - - target.addAttribute("key", keyMapper.key(component)); - if (component.equals(selected)) { - target.addAttribute("selected", true); - LegacyPaint.paint(component, target); - } - target.endTag("tab"); - } - - target.endTag("tabs"); - - if (selected != null) { - target.addVariable(this, "selected", keyMapper.key(selected)); - } - - } - - /** * Are the tab selection parts ("tabs") hidden. * * @return true if the tabs are hidden in the UI */ public boolean areTabsHidden() { - return tabsHidden; + return !getState(false).tabsVisible; } /** @@ -485,8 +428,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * true if the tabs should be hidden */ public void hideTabs(boolean tabsHidden) { - this.tabsHidden = tabsHidden; - markAsDirty(); + getState().tabsVisible = !tabsHidden; } /** @@ -531,6 +473,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, updateSelection(); fireSelectedTabChange(); markAsDirty(); + getRpcProxy(TabsheetClientRpc.class).revertToSharedStateSelection(); } } @@ -538,17 +481,29 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * Sets the selected tab in the TabSheet. Ensures that the selected tab is * repainted if needed. * - * @param c + * @param component * The new selection or null for no selection */ - private void setSelected(Component c) { - selected = c; + private void setSelected(Component component) { + Tab tab = tabs.get(selected); + + selected = component; // Repaint of the selected component is needed as only the selected // component is communicated to the client. Otherwise this will be a // "cached" update even though the client knows nothing about the // connector if (selected != null) { + tab = getTab(component); + + if (tab != null && tab.getDefaultFocusComponent() != null) { + tab.getDefaultFocusComponent().focus(); + } + + getState().selected = keyMapper.key(selected); + selected.markAsDirtyRecursive(); + } else { + getState().selected = null; } } @@ -632,27 +587,16 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, return selected; } - // inherits javadoc + private TabsheetServerRpcImpl rpc = new TabsheetServerRpcImpl(); - @Override - public void changeVariables(Object source, Map<String, Object> variables) { - if (variables.containsKey("selected")) { - setSelectedTab(keyMapper.get((String) variables.get("selected"))); - } - if (variables.containsKey("close")) { - final Component tab = keyMapper - .get((String) variables.get("close")); - if (tab != null) { - closeHandler.onTabClose(this, tab); - } - } - if (variables.containsKey(FocusEvent.EVENT_ID)) { - fireEvent(new FocusEvent(this)); - } - if (variables.containsKey(BlurEvent.EVENT_ID)) { - fireEvent(new BlurEvent(this)); + private FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl( + this) { + + @Override + protected void fireEvent(Event event) { + TabSheet.this.fireEvent(event); } - } + }; /** * Replaces a component (tab content) with another. This can be used to @@ -674,10 +618,10 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public void replaceComponent(Component oldComponent, Component newComponent) { + boolean selectAfterInserting = false; if (selected == oldComponent) { - // keep selection w/o selectedTabChange event - setSelected(newComponent); + selectAfterInserting = true; } Tab newTab = tabs.get(newComponent); @@ -687,6 +631,7 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, int oldLocation = -1; int newLocation = -1; int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { final Component component = i.next(); @@ -705,6 +650,11 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, } else if (newLocation == -1) { removeComponent(oldComponent); newTab = addTab(newComponent, oldLocation); + + if (selectAfterInserting) { + setSelected(newComponent); + } + // Copy all relevant metadata to the new tab (#8793) // TODO Should reuse the old tab instance instead? copyTabMetadata(oldTab, newTab); @@ -712,17 +662,20 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, components.set(oldLocation, newComponent); components.set(newLocation, oldComponent); + if (selectAfterInserting) { + setSelected(newComponent); + } + // Tab associations are not changed, but metadata is swapped between // the instances // TODO Should reassociate the instances instead? - Tab tmp = new TabSheetTabImpl(null, null); + Tab tmp = new TabSheetTabImpl(null, null, null); copyTabMetadata(newTab, tmp); copyTabMetadata(oldTab, newTab); copyTabMetadata(tmp, oldTab); markAsDirty(); } - } /* Click event */ @@ -882,13 +835,30 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, * Note! Currently only supported by TabSheet, not Accordion. * </p> * - * @param visible + * @param closable * true if the end user is allowed to close the tab, false * for not allowing to close. Should default to false. */ 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 +902,31 @@ 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. + * + * @since 7.2 + */ + public String getIconAlternateText(); + + /** + * Sets the icon alt text for the tab. + * + * @since 7.2 + * + * @param iconAltText + * the icon to set + */ + public void setIconAlternateText(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 +1010,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(); } /** @@ -1022,21 +1034,23 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, */ public class TabSheetTabImpl implements Tab { - private String caption = ""; - private Resource icon = null; - private boolean enabled = true; - private boolean visible = true; - private boolean closable = false; - private String description = null; - private ErrorMessage componentError = null; - private String styleName; + private TabState tabState; + + private Focusable defaultFocus; + + private ErrorMessage componentError; + + public TabSheetTabImpl(String key, String caption, Resource icon) { + tabState = new TabState(); - public TabSheetTabImpl(String caption, Resource icon) { if (caption == null) { caption = ""; } - this.caption = caption; - this.icon = icon; + + tabState.key = key; + tabState.caption = caption; + + setIcon(icon); } /** @@ -1045,34 +1059,58 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public String getCaption() { - return caption; + return tabState.caption; } @Override public void setCaption(String caption) { - this.caption = caption; + tabState.caption = caption; markAsDirty(); } @Override public Resource getIcon() { - return icon; + return getResource(ComponentConstants.ICON_RESOURCE + + tabState.key); } @Override public void setIcon(Resource icon) { - this.icon = icon; + // this might not be ideal (resetting icon altText), but matches + // previous semantics + setIcon(icon, ""); + } + + @Override + public String getIconAlternateText() { + return tabState.iconAltText; + } + + @Override + public void setIconAlternateText(String iconAltText) { + tabState.iconAltText = iconAltText; markAsDirty(); } @Override + public void setDefaultFocusComponent(Focusable defaultFocus) { + this.defaultFocus = defaultFocus; + } + + @Override + public Focusable getDefaultFocusComponent() { + return defaultFocus; + } + + @Override public boolean isEnabled() { - return enabled; + return tabState.enabled; } @Override public void setEnabled(boolean enabled) { - this.enabled = enabled; + tabState.enabled = enabled; + if (updateSelection()) { fireSelectedTabChange(); } @@ -1081,12 +1119,13 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public boolean isVisible() { - return visible; + return tabState.visible; } @Override public void setVisible(boolean visible) { - this.visible = visible; + tabState.visible = visible; + if (updateSelection()) { fireSelectedTabChange(); } @@ -1095,27 +1134,24 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public boolean isClosable() { - return closable; + return tabState.closable; } @Override public void setClosable(boolean closable) { - this.closable = closable; - markAsDirty(); - } - - public void close() { + tabState.closable = closable; + markAsDirty(); } @Override public String getDescription() { - return description; + return tabState.description; } @Override public void setDescription(String description) { - this.description = description; + tabState.description = description; markAsDirty(); } @@ -1127,6 +1163,11 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public void setComponentError(ErrorMessage componentError) { this.componentError = componentError; + + String formattedHtmlMessage = componentError != null ? componentError + .getFormattedHtmlMessage() : null; + tabState.componentError = formattedHtmlMessage; + markAsDirty(); } @@ -1142,13 +1183,37 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public void setStyleName(String styleName) { - this.styleName = styleName; + tabState.styleName = styleName; + markAsDirty(); } @Override public String getStyleName() { - return styleName; + return tabState.styleName; + } + + protected TabState getTabState() { + return tabState; + } + + @Override + public void setId(String id) { + tabState.id = id; + markAsDirty(); + + } + + @Override + public String getId() { + return tabState.id; + } + + @Override + public void setIcon(Resource icon, String iconAltText) { + setResource(ComponentConstants.ICON_RESOURCE + tabState.key, + icon); + tabState.iconAltText = iconAltText; } } @@ -1203,7 +1268,9 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, int oldPosition = getTabPosition(tab); components.remove(oldPosition); components.add(position, tab.getComponent()); - markAsDirty(); + + getState().tabs.remove(oldPosition); + getState().tabs.add(position, ((TabSheetTabImpl) tab).getTabState()); } /** @@ -1224,13 +1291,12 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, @Override public int getTabIndex() { - return tabIndex; + return getState(false).tabIndex; } @Override public void setTabIndex(int tabIndex) { - this.tabIndex = tabIndex; - markAsDirty(); + getState().tabIndex = tabIndex; } @Override @@ -1309,7 +1375,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.getIconAlternateText()); to.setDescription(from.getDescription()); to.setVisible(from.isVisible()); to.setEnabled(from.isEnabled()); @@ -1317,4 +1383,14 @@ public class TabSheet extends AbstractComponentContainer implements Focusable, to.setStyleName(from.getStyleName()); to.setComponentError(from.getComponentError()); } + + @Override + protected TabsheetState getState(boolean markAsDirty) { + return (TabsheetState) super.getState(markAsDirty); + } + + @Override + protected TabsheetState getState() { + return (TabsheetState) super.getState(); + } } diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java index b4bb2cde4c..4c15aca2eb 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 d701c9c92f..7a71083e85 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -16,6 +16,7 @@ package com.vaadin.ui; +import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -27,11 +28,15 @@ import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; +import com.vaadin.annotations.PreserveOnRefresh; import com.vaadin.event.Action; 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; @@ -48,10 +53,12 @@ import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinServlet; import com.vaadin.server.VaadinSession; +import com.vaadin.server.VaadinSession.State; 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 +84,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 +102,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 @@ -156,7 +164,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements public void resize(int viewWidth, int viewHeight, int windowWidth, int windowHeight) { // TODO We're not doing anything with the view dimensions - getPage().updateBrowserWindowSize(windowWidth, windowHeight); + getPage().updateBrowserWindowSize(windowWidth, windowHeight, true); } @Override @@ -167,10 +175,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 +227,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. @@ -356,7 +364,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements if (variables.containsKey(UIConstants.LOCATION_VARIABLE)) { String location = (String) variables .get(UIConstants.LOCATION_VARIABLE); - getPage().updateLocation(location); + getPage().updateLocation(location, true); } } @@ -418,8 +426,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 +555,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,8 +607,14 @@ 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) { String message = "This UI instance is already initialized (as UI id " + this.uiId @@ -615,6 +630,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements throw new IllegalStateException(message); } this.uiId = uiId; + this.embedId = embedId; // Actual theme - used for finding CustomLayout templates theme = request.getParameter("theme"); @@ -647,6 +663,57 @@ public abstract class UI extends AbstractSingleComponentContainer implements protected abstract void init(VaadinRequest request); /** + * Internal reinitialization method, should not be overridden. + * + * @since 7.2 + * @param request + * the request that caused this UI to be reloaded + */ + public void doRefresh(VaadinRequest request) { + // This is a horrible hack. We want to have the most recent location and + // browser window size available in refresh(), but we want to call + // listeners, if any, only after refresh(). So we momentarily assign the + // old values back before setting the new values again to ensure the + // events are properly fired. + + Page page = getPage(); + + URI oldLocation = page.getLocation(); + int oldWidth = page.getBrowserWindowWidth(); + int oldHeight = page.getBrowserWindowHeight(); + + page.init(request); + + refresh(request); + + URI newLocation = page.getLocation(); + int newWidth = page.getBrowserWindowWidth(); + int newHeight = page.getBrowserWindowHeight(); + + page.updateLocation(oldLocation.toString(), false); + page.updateBrowserWindowSize(oldWidth, oldHeight, false); + + page.updateLocation(newLocation.toString(), true); + page.updateBrowserWindowSize(newWidth, newHeight, true); + } + + /** + * Reinitializes this UI after a browser refresh if the UI is set to be + * preserved on refresh, typically using the {@link PreserveOnRefresh} + * annotation. This method is intended to be overridden by subclasses if + * needed; the default implementation is empty. + * <p> + * The {@link VaadinRequest} can be used to get information about the + * request that caused this UI to be reloaded. + * + * @since 7.2 + * @param request + * the request that caused this UI to be reloaded + */ + protected void refresh(VaadinRequest request) { + } + + /** * Sets the thread local for the current UI. This method is used by the * framework to set the current application whenever a new request is * processed and it is cleared when the request has been processed. @@ -1096,7 +1163,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements public void close() { closing = true; - boolean sessionExpired = (session == null || session.isClosing()); + boolean sessionExpired = (session == null || session.getState() != State.OPEN); getRpcProxy(UIClientRpc.class).uiClosed(sessionExpired); if (getPushConnection() != null) { // Push the Rpc to the client. The connection will be closed when @@ -1345,6 +1412,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 @@ -1357,6 +1433,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 @@ -1374,79 +1453,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(); - } } /** @@ -1480,6 +1553,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. * @@ -1529,4 +1613,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 f440613905..869c32751a 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; @@ -1094,5 +1184,5 @@ public class Upload extends AbstractComponent implements Component.Focusable, } return super.getListeners(eventType); - }; + } } diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java index 149051d2fb..149fcd536f 100644 --- a/server/src/com/vaadin/ui/Window.java +++ b/server/src/com/vaadin/ui/Window.java @@ -33,10 +33,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.WindowRole; import com.vaadin.util.ReflectTools; /** @@ -238,8 +240,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 +644,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 +1008,202 @@ 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 components + * the components to use as description + */ + public void setAssistiveDescription(Component... components) { + if (components == null) { + throw new IllegalArgumentException( + "Parameter connectors must be non-null"); + } else { + getState().contentDescription = components; + } + } + + /** + * 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 array of previously set components + */ + public Component[] getAssistiveDescription() { + Connector[] contentDescription = getState().contentDescription; + if (contentDescription == null) { + return null; + } + + Component[] target = new Component[contentDescription.length]; + System.arraycopy(contentDescription, 0, target, 0, + contentDescription.length); + + return target; + } + + /** + * 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; + } } |