diff options
Diffstat (limited to 'server/src')
125 files changed, 14640 insertions, 4275 deletions
diff --git a/server/src/com/vaadin/annotations/Push.java b/server/src/com/vaadin/annotations/Push.java new file mode 100644 index 0000000000..58e70acf21 --- /dev/null +++ b/server/src/com/vaadin/annotations/Push.java @@ -0,0 +1,49 @@ +/* + * 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.vaadin.shared.communication.PushMode; +import com.vaadin.ui.UI; + +/** + * Configures server push for a {@link UI}. Adding <code>@Push</code> to a UI + * class configures the UI for automatic push. If some other push mode is + * desired, it can be passed as a parameter, e.g. + * <code>@Push(PushMode.MANUAL)</code>. + * + * @see PushMode + * + * @author Vaadin Ltd. + * @since 7.1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Push { + /** + * Returns the {@link PushMode} to use for the annotated UI. The default + * push mode when this annotation is present is {@link PushMode#AUTOMATIC}. + * + * @return the push mode to use + */ + public PushMode value() default PushMode.AUTOMATIC; + +} diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index ddeac62d6d..e93db52a35 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -953,6 +953,15 @@ public interface Container extends Serializable { */ public void removeAllContainerFilters(); + /** + * Returns the filters which have been applied to the container + * + * @return A collection of filters which have been applied to the + * container. An empty collection if no filters have been + * applied. + * @since 7.1 + */ + public Collection<Filter> getContainerFilters(); } /** diff --git a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java index 9dc6037d83..0b4e3a8049 100644 --- a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java @@ -58,6 +58,23 @@ public class BeanFieldGroup<T> extends FieldGroup { } } + @Override + protected Object findPropertyId(java.lang.reflect.Field memberField) { + String fieldName = memberField.getName(); + Item dataSource = getItemDataSource(); + if (dataSource != null && dataSource.getItemProperty(fieldName) != null) { + return fieldName; + } else { + String minifiedFieldName = minifyFieldName(fieldName); + try { + return getFieldName(beanType, minifiedFieldName); + } catch (SecurityException e) { + } catch (NoSuchFieldException e) { + } + } + return null; + } + private static java.lang.reflect.Field getField(Class<?> cls, String propertyId) throws SecurityException, NoSuchFieldException { if (propertyId.contains(".")) { @@ -75,7 +92,7 @@ public class BeanFieldGroup<T> extends FieldGroup { } catch (NoSuchFieldException e) { // Try super classes until we reach Object Class<?> superClass = cls.getSuperclass(); - if (superClass != Object.class) { + if (superClass != null && superClass != Object.class) { return getField(superClass, propertyId); } else { throw e; @@ -84,6 +101,22 @@ public class BeanFieldGroup<T> extends FieldGroup { } } + private static String getFieldName(Class<?> cls, String propertyId) + throws SecurityException, NoSuchFieldException { + for (java.lang.reflect.Field field1 : cls.getDeclaredFields()) { + if (propertyId.equals(minifyFieldName(field1.getName()))) { + return field1.getName(); + } + } + // Try super classes until we reach Object + Class<?> superClass = cls.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return getFieldName(superClass, propertyId); + } else { + throw new NoSuchFieldException(); + } + } + /** * Helper method for setting the data source directly using a bean. This * method wraps the bean in a {@link BeanItem} and calls @@ -176,4 +209,4 @@ public class BeanFieldGroup<T> extends FieldGroup { } return beanValidationImplementationAvailable; } -}
\ No newline at end of file +} diff --git a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java index 9ced6588f5..c1e4b4933e 100644 --- a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java +++ b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java @@ -15,18 +15,23 @@ */ package com.vaadin.data.fieldgroup; +import java.util.Date; import java.util.EnumSet; import com.vaadin.data.Item; import com.vaadin.data.fieldgroup.FieldGroup.BindException; +import com.vaadin.ui.AbstractField; import com.vaadin.ui.AbstractSelect; import com.vaadin.ui.AbstractTextField; import com.vaadin.ui.CheckBox; import com.vaadin.ui.ComboBox; +import com.vaadin.ui.DateField; import com.vaadin.ui.Field; +import com.vaadin.ui.InlineDateField; import com.vaadin.ui.ListSelect; import com.vaadin.ui.NativeSelect; import com.vaadin.ui.OptionGroup; +import com.vaadin.ui.PopupDateField; import com.vaadin.ui.RichTextArea; import com.vaadin.ui.Table; import com.vaadin.ui.TextField; @@ -39,6 +44,8 @@ public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory { public <T extends Field> T createField(Class<?> type, Class<T> fieldType) { if (Enum.class.isAssignableFrom(type)) { return createEnumField(type, fieldType); + } else if (Date.class.isAssignableFrom(type)) { + return createDateField(type, fieldType); } else if (Boolean.class.isAssignableFrom(type) || boolean.class.isAssignableFrom(type)) { return createBooleanField(fieldType); @@ -70,6 +77,25 @@ public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory { return null; } + private <T extends Field> T createDateField(Class<?> type, + Class<T> fieldType) { + AbstractField field; + + if (InlineDateField.class.isAssignableFrom(fieldType)) { + field = new InlineDateField(); + } else if (DateField.class.isAssignableFrom(fieldType) + || fieldType == Field.class) { + field = new PopupDateField(); + } else if (AbstractTextField.class.isAssignableFrom(fieldType)) { + field = createAbstractTextField((Class<? extends AbstractTextField>) fieldType); + } else { + return null; + } + + field.setImmediate(true); + return (T) field; + } + protected AbstractSelect createCompatibleSelect( Class<? extends AbstractSelect> fieldType) { AbstractSelect select; diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java index dc1fdbb78d..981aea387d 100644 --- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java @@ -733,11 +733,12 @@ public class FieldGroup implements Serializable { * that have not been initialized. * <p> * This method processes all (Java) member fields whose type extends - * {@link Field} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. Fields that are not initialized (null) are built - * using the field factory. All non-null fields for which a property id can - * be determined are bound to the property id. + * {@link Field} and that can be mapped to a property id. Property ids are + * searched in the following order: @{@link PropertyId} annotations, exact + * field name matches and the case-insensitive matching that ignores + * underscores. Fields that are not initialized (null) are built using the + * field factory. All non-null fields for which a property id can be + * determined are bound to the property id. * </p> * <p> * For example: @@ -777,11 +778,12 @@ public class FieldGroup implements Serializable { * member fields that have not been initialized. * <p> * This method processes all (Java) member fields whose type extends - * {@link Field} and that can be mapped to a property id. Property id - * mapping is done based on the field name or on a @{@link PropertyId} - * annotation on the field. Fields that are not initialized (null) are built - * using the field factory is buildFields is true. All non-null fields for - * which a property id can be determined are bound to the property id. + * {@link Field} and that can be mapped to a property id. Property ids are + * searched in the following order: @{@link PropertyId} annotations, exact + * field name matches and the case-insensitive matching that ignores + * underscores. Fields that are not initialized (null) are built using the + * field factory is buildFields is true. All non-null fields for which a + * property id can be determined are bound to the property id. * </p> * * @param objectWithMemberFields @@ -812,7 +814,16 @@ public class FieldGroup implements Serializable { // @PropertyId(propertyId) always overrides property id propertyId = propertyIdAnnotation.value(); } else { - propertyId = memberField.getName(); + try { + propertyId = findPropertyId(memberField); + } catch (SearchException e) { + // Property id was not found, skip this field + continue; + } + if (propertyId == null) { + // Property id was not found, skip this field + continue; + } } // Ensure that the property id exists @@ -873,6 +884,55 @@ public class FieldGroup implements Serializable { } } + /** + * Searches for a property id from the current itemDataSource that matches + * the given memberField. + * <p> + * If perfect match is not found, uses a case insensitive search that also + * ignores underscores. Returns null if no match is found. Throws a + * SearchException if no item data source has been set. + * </p> + * <p> + * The propertyId search logic used by + * {@link #buildAndBindMemberFields(Object, boolean) + * buildAndBindMemberFields} can easily be customized by overriding this + * method. No other changes are needed. + * </p> + * + * @param memberField + * The field an object id is searched for + * @return + */ + protected Object findPropertyId(java.lang.reflect.Field memberField) { + String fieldName = memberField.getName(); + if (getItemDataSource() == null) { + throw new SearchException( + "Property id type for field '" + + fieldName + + "' could not be determined. No item data source has been set."); + } + Item dataSource = getItemDataSource(); + if (dataSource.getItemProperty(fieldName) != null) { + return fieldName; + } else { + String minifiedFieldName = minifyFieldName(fieldName); + for (Object itemPropertyId : dataSource.getItemPropertyIds()) { + if (itemPropertyId instanceof String) { + String itemPropertyName = (String) itemPropertyId; + if (minifiedFieldName + .equals(minifyFieldName(itemPropertyName))) { + return itemPropertyName; + } + } + } + } + return null; + } + + protected static String minifyFieldName(String fieldName) { + return fieldName.toLowerCase().replace("_", ""); + } + public static class CommitException extends Exception { public CommitException() { @@ -909,6 +969,18 @@ public class FieldGroup implements Serializable { } + public static class SearchException extends RuntimeException { + + public SearchException(String message) { + super(message); + } + + public SearchException(String message, Throwable t) { + super(message, t); + } + + } + /** * Builds a field and binds it to the given property id using the field * binder. diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index db1e1afe0d..35403d6419 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -385,6 +385,26 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends removeFilter(filter); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#hasContainerFilters() + */ + @Override + public boolean hasContainerFilters() { + return super.hasContainerFilters(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return super.getContainerFilters(); + } + /** * Make this container listen to the given property provided it notifies * when its value changes. diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java index 504b4081c1..84304431bc 100644 --- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -501,6 +501,25 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } /** + * Returns true if any filters have been applied to the container. + * + * @return true if the container has filters applied, false otherwise + * @since 7.1 + */ + protected boolean hasContainerFilters() { + return !getContainerFilters().isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#getContainerFilters() + */ + protected Collection<Filter> getContainerFilters() { + return Collections.unmodifiableCollection(filters); + } + + /** * Remove a specific container filter and re-filter the view (if necessary). * * This can be used to implement diff --git a/server/src/com/vaadin/data/util/AbstractProperty.java b/server/src/com/vaadin/data/util/AbstractProperty.java index 499421a8b4..903f2f50f2 100644 --- a/server/src/com/vaadin/data/util/AbstractProperty.java +++ b/server/src/com/vaadin/data/util/AbstractProperty.java @@ -18,7 +18,6 @@ package com.vaadin.data.util; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; -import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Property; @@ -71,27 +70,33 @@ public abstract class AbstractProperty<T> implements Property<T>, } /** - * Returns the value of the <code>Property</code> in human readable textual - * format. + * Returns a string representation of this object. The returned string + * representation depends on if the legacy Property toString mode is enabled + * or disabled. + * <p> + * If legacy Property toString mode is enabled, returns the value of the + * <code>Property</code> converted to a String. + * </p> + * <p> + * If legacy Property toString mode is disabled, the string representation + * has no special meaning + * </p> * - * @return String representation of the value stored in the Property - * @deprecated As of 7.0, use {@link #getValue()} instead and possibly - * toString on that + * @see LegacyPropertyHelper#isLegacyToStringEnabled() + * + * @return A string representation of the value value stored in the Property + * or a string representation of the Property object. + * @deprecated As of 7.0. To get the property value, use {@link #getValue()} + * instead (and possibly toString on that) */ @Deprecated @Override public String toString() { - getLogger() - .log(Level.WARNING, - "You are using Property.toString() instead of getValue() to get the value for a {0}." - + "This will not be supported starting from Vaadin 7.1 " - + "(your debugger might call toString() and cause this message to appear).", - getClass().getSimpleName()); - T v = getValue(); - if (v == null) { - return null; + if (!LegacyPropertyHelper.isLegacyToStringEnabled()) { + return super.toString(); + } else { + return LegacyPropertyHelper.legacyPropertyToString(this); } - return v.toString(); } /* Events */ diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java index 1df4dd9bfb..d7bf70caf6 100644 --- a/server/src/com/vaadin/data/util/IndexedContainer.java +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -28,7 +28,6 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Container; @@ -954,29 +953,32 @@ public class IndexedContainer extends } /** - * Returns the value of the Property in human readable textual format. - * The return value should be assignable to the <code>setValue</code> - * method if the Property is not in read-only mode. + * Returns a string representation of this object. The returned string + * representation depends on if the legacy Property toString mode is + * enabled or disabled. + * <p> + * If legacy Property toString mode is enabled, returns the value of the + * <code>Property</code> converted to a String. + * </p> + * <p> + * If legacy Property toString mode is disabled, the string + * representation has no special meaning + * </p> * - * @return <code>String</code> representation of the value stored in the - * Property - * @deprecated As of 7.0, use {@link #getValue()} instead and possibly - * toString on that + * @return A string representation of the value value stored in the + * Property or a string representation of the Property object. + * @deprecated As of 7.0. To get the property value, use + * {@link #getValue()} instead (and possibly toString on + * that) */ @Deprecated @Override public String toString() { - getLogger() - .log(Level.WARNING, - "You are using IndexedContainerProperty.toString() instead of getValue() to get the value for a {0}." - + " This will not be supported starting from Vaadin 7.1 " - + "(your debugger might call toString() and cause this message to appear).", - getClass().getSimpleName()); - Object v = getValue(); - if (v == null) { - return null; + if (!LegacyPropertyHelper.isLegacyToStringEnabled()) { + return super.toString(); + } else { + return LegacyPropertyHelper.legacyPropertyToString(this); } - return v.toString(); } private Logger getLogger() { @@ -1190,4 +1192,23 @@ public class IndexedContainer extends removeFilter(filter); } + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public boolean hasContainerFilters() { + return super.hasContainerFilters(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.AbstractInMemoryContainer#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return super.getContainerFilters(); + } } diff --git a/server/src/com/vaadin/data/util/LegacyPropertyHelper.java b/server/src/com/vaadin/data/util/LegacyPropertyHelper.java new file mode 100644 index 0000000000..0276e35dbf --- /dev/null +++ b/server/src/com/vaadin/data/util/LegacyPropertyHelper.java @@ -0,0 +1,102 @@ +/* + * 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; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.server.Constants; +import com.vaadin.server.DeploymentConfiguration.LegacyProperyToStringMode; +import com.vaadin.server.VaadinService; + +/** + * Helper class which provides methods for handling Property.toString in a + * Vaadin 6 compatible way + * + * @author Vaadin Ltd + * @since 7.1 + * @deprecated This is only used internally for backwards compatibility + */ +@Deprecated +public class LegacyPropertyHelper { + + /** + * Returns the property value converted to a String. + * + * @param p + * The property + * @return A string representation of the property value, compatible with + * how Property implementations in Vaadin 6 do it + */ + public static String legacyPropertyToString(Property p) { + maybeLogLegacyPropertyToStringWarning(p); + Object value = p.getValue(); + if (value == null) { + return null; + } + return value.toString(); + } + + public static void maybeLogLegacyPropertyToStringWarning(Property p) { + if (!logLegacyToStringWarning()) { + return; + } + + getLogger().log(Level.WARNING, + Constants.WARNING_LEGACY_PROPERTY_TOSTRING, + p.getClass().getName()); + } + + /** + * Checks if legacy Property.toString() implementation is enabled. The + * legacy Property.toString() will return the value of the property somehow + * converted to a String. If the legacy mode is disabled, toString() will + * return super.toString(). + * <p> + * The legacy toString mode can be toggled using the + * "legacyPropertyToString" init parameter + * </p> + * + * @return true if legacy Property.toString() mode is enabled, false + * otherwise + */ + public static boolean isLegacyToStringEnabled() { + if (VaadinService.getCurrent() == null) { + // This should really not happen but we need to handle it somehow. + // IF it happens it seems more safe to use the legacy mode and log. + return true; + } + return VaadinService.getCurrent().getDeploymentConfiguration() + .getLegacyPropertyToStringMode().useLegacyMode(); + } + + private static boolean logLegacyToStringWarning() { + if (VaadinService.getCurrent() == null) { + // This should really not happen but we need to handle it somehow. + // IF it happens it seems more safe to use the legacy mode and log. + return true; + } + return VaadinService.getCurrent().getDeploymentConfiguration() + .getLegacyPropertyToStringMode() == LegacyProperyToStringMode.WARNING; + + } + + private static Logger getLogger() { + return Logger.getLogger(LegacyPropertyHelper.class.getName()); + } + +} diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java index 378372d044..d8448a2b50 100644 --- a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java +++ b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java @@ -18,10 +18,10 @@ package com.vaadin.data.util.sqlcontainer; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; -import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Property; +import com.vaadin.data.util.LegacyPropertyHelper; import com.vaadin.data.util.converter.Converter.ConversionException; /** @@ -255,26 +255,33 @@ final public class ColumnProperty implements Property { } /** - * Returns the value of the Property in human readable textual format. + * Returns a string representation of this object. The returned string + * representation depends on if the legacy Property toString mode is enabled + * or disabled. + * <p> + * If legacy Property toString mode is enabled, returns the value of this + * <code>Property</code> converted to a String. + * </p> + * <p> + * If legacy Property toString mode is disabled, the string representation + * has no special meaning + * </p> * - * @see java.lang.Object#toString() - * @deprecated As of 7.0, use {@link #getValue()} instead and possibly - * toString on that + * @see LegacyPropertyHelper#isLegacyToStringEnabled() + * + * @return A string representation of the value value stored in the Property + * or a string representation of the Property object. + * @deprecated As of 7.0. To get the property value, use {@link #getValue()} + * instead (and possibly toString on that) */ @Deprecated @Override public String toString() { - getLogger() - .log(Level.WARNING, - "You are using ColumnProperty.toString() instead of getValue() to get the value for a {0}. " - + "This will not be supported starting from Vaadin 7.1 (your debugger might call toString() " - + "and cause this message to appear).", - getClass().getSimpleName()); - Object v = getValue(); - if (v == null) { - return null; + if (!LegacyPropertyHelper.isLegacyToStringEnabled()) { + return super.toString(); + } else { + return LegacyPropertyHelper.legacyPropertyToString(this); } - return v.toString(); } private static Logger getLogger() { diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java index aa8234ebb9..e9a1a2d98f 100644 --- a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java +++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java @@ -590,13 +590,32 @@ public class SQLContainer implements Container, Container.Filterable, /** * {@inheritDoc} */ - @Override public void removeAllContainerFilters() { filters.clear(); refresh(); } + /** + * Returns true if any filters have been applied to the container. + * + * @return true if the container has filters applied, false otherwise + * @since 7.1 + */ + public boolean hasContainerFilters() { + return !getContainerFilters().isEmpty(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Filterable#getContainerFilters() + */ + @Override + public Collection<Filter> getContainerFilters() { + return Collections.unmodifiableCollection(filters); + } + /**********************************************/ /** Methods from interface Container.Indexed **/ /**********************************************/ @@ -1820,4 +1839,5 @@ public class SQLContainer implements Container, Container.Filterable, private static final Logger getLogger() { return Logger.getLogger(SQLContainer.class.getName()); } + } diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java index caed5526e3..39c8365076 100644 --- a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java +++ b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java @@ -50,9 +50,23 @@ import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; public class TableQuery extends AbstractTransactionalQuery implements QueryDelegate, QueryDelegate.RowIdChangeNotifier { - /** Table name, primary key column name(s) and version column name */ + /** + * Table name (without catalog or schema information). + */ private String tableName; + private String catalogName; + private String schemaName; + /** + * Cached concatenated version of the table name. + */ + private String fullTableName; + /** + * Primary key column name(s) in the table. + */ private List<String> primaryKeyColumns; + /** + * Version column name in the table. + */ private String versionColumn; /** Currently set Filters and OrderBys */ @@ -70,15 +84,15 @@ public class TableQuery extends AbstractTransactionalQuery implements /** Set to true to output generated SQL Queries to System.out */ private final boolean debug = false; - /** Prevent no-parameters instantiation of TableQuery */ - @SuppressWarnings("unused") - private TableQuery() { - } - /** * Creates a new TableQuery using the given connection pool, SQL generator * and table name to fetch the data from. All parameters must be non-null. * + * The table name must be a simple name with no catalog or schema + * information. If those are needed, use + * {@link #TableQuery(String, String, String, JDBCConnectionPool, SQLGenerator)} + * . + * * @param tableName * Name of the database table to connect to * @param connectionPool @@ -88,15 +102,30 @@ public class TableQuery extends AbstractTransactionalQuery implements */ public TableQuery(String tableName, JDBCConnectionPool connectionPool, SQLGenerator sqlGenerator) { - super(connectionPool); - if (tableName == null || tableName.trim().length() < 1 - || connectionPool == null || sqlGenerator == null) { - throw new IllegalArgumentException( - "All parameters must be non-null and a table name must be given."); - } - this.tableName = tableName; - this.sqlGenerator = sqlGenerator; - fetchMetaData(); + this(null, null, tableName, connectionPool, sqlGenerator); + } + + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. Catalog and schema names can be + * null, all other parameters must be non-null. + * + * @param catalogName + * Name of the database catalog (can be null) + * @param schemaName + * Name of the database schema (can be null) + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + * @since 7.1 + */ + public TableQuery(String catalogName, String schemaName, String tableName, + JDBCConnectionPool connectionPool, SQLGenerator sqlGenerator) { + this(catalogName, schemaName, tableName, connectionPool, sqlGenerator, + true); } /** @@ -104,6 +133,11 @@ public class TableQuery extends AbstractTransactionalQuery implements * to fetch the data from. All parameters must be non-null. The default SQL * generator will be used for queries. * + * The table name must be a simple name with no catalog or schema + * information. If those are needed, use + * {@link #TableQuery(String, String, String, JDBCConnectionPool, SQLGenerator)} + * . + * * @param tableName * Name of the database table to connect to * @param connectionPool @@ -113,6 +147,48 @@ public class TableQuery extends AbstractTransactionalQuery implements this(tableName, connectionPool, new DefaultSQLGenerator()); } + /** + * Creates a new TableQuery using the given connection pool, SQL generator + * and table name to fetch the data from. Catalog and schema names can be + * null, all other parameters must be non-null. + * + * @param catalogName + * Name of the database catalog (can be null) + * @param schemaName + * Name of the database schema (can be null) + * @param tableName + * Name of the database table to connect to + * @param connectionPool + * Connection pool for accessing the database + * @param sqlGenerator + * SQL query generator implementation + * @param escapeNames + * true to escape special characters in catalog, schema and table + * names, false to use the names as-is + * @since 7.1 + */ + protected TableQuery(String catalogName, String schemaName, + String tableName, JDBCConnectionPool connectionPool, + SQLGenerator sqlGenerator, boolean escapeNames) { + super(connectionPool); + if (tableName == null || tableName.trim().length() < 1 + || connectionPool == null || sqlGenerator == null) { + throw new IllegalArgumentException( + "Table name, connection pool and SQL generator parameters must be non-null and non-empty."); + } + if (escapeNames) { + this.catalogName = SQLUtil.escapeSQL(catalogName); + this.schemaName = SQLUtil.escapeSQL(schemaName); + this.tableName = SQLUtil.escapeSQL(tableName); + } else { + this.catalogName = catalogName; + this.schemaName = schemaName; + this.tableName = tableName; + } + this.sqlGenerator = sqlGenerator; + fetchMetaData(); + } + /* * (non-Javadoc) * @@ -121,8 +197,8 @@ public class TableQuery extends AbstractTransactionalQuery implements @Override public int getCount() throws SQLException { getLogger().log(Level.FINE, "Fetching count..."); - StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, - filters, null, 0, 0, "COUNT(*)"); + StatementHelper sh = sqlGenerator.generateSelectQuery( + getFullTableName(), filters, null, 0, 0, "COUNT(*)"); boolean shouldCloseTransaction = false; if (!isInTransaction()) { shouldCloseTransaction = true; @@ -167,11 +243,11 @@ public class TableQuery extends AbstractTransactionalQuery implements for (int i = 0; i < primaryKeyColumns.size(); i++) { ob.add(new OrderBy(primaryKeyColumns.get(i), true)); } - sh = sqlGenerator.generateSelectQuery(tableName, filters, ob, - offset, pagelength, null); + sh = sqlGenerator.generateSelectQuery(getFullTableName(), filters, + ob, offset, pagelength, null); } else { - sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys, - offset, pagelength, null); + sh = sqlGenerator.generateSelectQuery(getFullTableName(), filters, + orderBys, offset, pagelength, null); } return executeQuery(sh); } @@ -204,11 +280,11 @@ public class TableQuery extends AbstractTransactionalQuery implements int result = 0; if (row.getId() instanceof TemporaryRowId) { setVersionColumnFlagInProperty(row); - sh = sqlGenerator.generateInsertQuery(tableName, row); + sh = sqlGenerator.generateInsertQuery(getFullTableName(), row); result = executeUpdateReturnKeys(sh, row); } else { setVersionColumnFlagInProperty(row); - sh = sqlGenerator.generateUpdateQuery(tableName, row); + sh = sqlGenerator.generateUpdateQuery(getFullTableName(), row); result = executeUpdate(sh); } if (versionColumn != null && result == 0) { @@ -244,7 +320,8 @@ public class TableQuery extends AbstractTransactionalQuery implements /* Set version column, if one is provided */ setVersionColumnFlagInProperty(row); /* Generate query */ - StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row); + StatementHelper sh = sqlGenerator.generateInsertQuery( + getFullTableName(), row); Connection connection = null; PreparedStatement pstmt = null; ResultSet generatedKeys = null; @@ -371,10 +448,61 @@ public class TableQuery extends AbstractTransactionalQuery implements versionColumn = column; } + /** + * Returns the table name for the query without catalog and schema + * information. + * + * @return table name, not null + */ public String getTableName() { return tableName; } + /** + * Returns the catalog name for the query. + * + * @return catalog name, can be null + * @since 7.1 + */ + public String getCatalogName() { + return catalogName; + } + + /** + * Returns the catalog name for the query. + * + * @return catalog name, can be null + * @since 7.1 + */ + public String getSchemaName() { + return schemaName; + } + + /** + * Returns the complete table name obtained by concatenation of the catalog + * and schema names (if any) and the table name. + * + * This method can be overridden if customization is needed. + * + * @return table name in the form it should be used in query and update + * statements + * @since 7.1 + */ + protected String getFullTableName() { + if (fullTableName == null) { + StringBuilder sb = new StringBuilder(); + if (catalogName != null) { + sb.append(catalogName).append("."); + } + if (schemaName != null) { + sb.append(schemaName).append("."); + } + sb.append(tableName); + fullTableName = sb.toString(); + } + return fullTableName; + } + public SQLGenerator getSqlGenerator() { return sqlGenerator; } @@ -480,22 +608,28 @@ public class TableQuery extends AbstractTransactionalQuery implements connection = getConnection(); DatabaseMetaData dbmd = connection.getMetaData(); if (dbmd != null) { - tableName = SQLUtil.escapeSQL(tableName); - tables = dbmd.getTables(null, null, tableName, null); + tables = dbmd.getTables(catalogName, schemaName, tableName, + null); if (!tables.next()) { - tables = dbmd.getTables(null, null, + String catalog = (catalogName != null) ? catalogName + .toUpperCase() : null; + String schema = (schemaName != null) ? schemaName + .toUpperCase() : null; + tables = dbmd.getTables(catalog, schema, tableName.toUpperCase(), null); if (!tables.next()) { throw new IllegalArgumentException( "Table with the name \"" - + tableName + + getFullTableName() + "\" was not found. Check your database contents."); } else { + catalogName = catalog; + schemaName = schema; tableName = tableName.toUpperCase(); } } tables.close(); - rs = dbmd.getPrimaryKeys(null, null, tableName); + rs = dbmd.getPrimaryKeys(catalogName, schemaName, tableName); List<String> names = new ArrayList<String>(); while (rs.next()) { names.add(rs.getString("COLUMN_NAME")); @@ -507,7 +641,7 @@ public class TableQuery extends AbstractTransactionalQuery implements if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) { throw new IllegalArgumentException( "Primary key constraints have not been defined for the table \"" - + tableName + + getFullTableName() + "\". Use FreeFormQuery to access this table."); } for (String colName : primaryKeyColumns) { @@ -592,7 +726,7 @@ public class TableQuery extends AbstractTransactionalQuery implements getLogger().log(Level.FINE, "Removing row with id: {0}", row.getId().getId()[0]); } - if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(), + if (executeUpdate(sqlGenerator.generateDeleteQuery(getFullTableName(), primaryKeyColumns, versionColumn, row)) == 1) { return true; } @@ -622,8 +756,8 @@ public class TableQuery extends AbstractTransactionalQuery implements filtersAndKeys.add(new Equal(colName, keys[ix])); ix++; } - StatementHelper sh = sqlGenerator.generateSelectQuery(tableName, - filtersAndKeys, orderBys, 0, 0, "*"); + StatementHelper sh = sqlGenerator.generateSelectQuery( + getFullTableName(), filtersAndKeys, orderBys, 0, 0, "*"); boolean shouldCloseTransaction = false; if (!isInTransaction()) { diff --git a/server/src/com/vaadin/server/AbstractClientConnector.java b/server/src/com/vaadin/server/AbstractClientConnector.java index cf579585ea..e998b8ed55 100644 --- a/server/src/com/vaadin/server/AbstractClientConnector.java +++ b/server/src/com/vaadin/server/AbstractClientConnector.java @@ -134,6 +134,7 @@ 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"; UI uI = getUI(); if (uI != null) { uI.getConnectorTracker().markDirty(this); @@ -218,6 +219,8 @@ 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"; + if (null == sharedState) { sharedState = createState(); } @@ -233,7 +236,7 @@ public abstract class AbstractClientConnector implements ClientConnector, @Override public JSONObject encodeState() throws JSONException { - return AbstractCommunicationManager.encodeState(this, getState()); + return LegacyCommunicationManager.encodeState(this, getState()); } /** @@ -642,17 +645,22 @@ public abstract class AbstractClientConnector implements ClientConnector, @Override public boolean handleConnectorRequest(VaadinRequest request, VaadinResponse response, String path) throws IOException { + DownloadStream stream = null; String[] parts = path.split("/", 2); String key = parts[0]; - ConnectorResource resource = (ConnectorResource) getResource(key); - if (resource != null) { - DownloadStream stream = resource.getStream(); - stream.writeResponse(request, response); - return true; - } else { - return false; + getSession().lock(); + try { + ConnectorResource resource = (ConnectorResource) getResource(key); + if (resource == null) { + return false; + } + stream = resource.getStream(); + } finally { + getSession().unlock(); } + stream.writeResponse(request, response); + return true; } /** diff --git a/server/src/com/vaadin/server/AbstractCommunicationManager.java b/server/src/com/vaadin/server/AbstractCommunicationManager.java deleted file mode 100644 index 17bbbda737..0000000000 --- a/server/src/com/vaadin/server/AbstractCommunicationManager.java +++ /dev/null @@ -1,2881 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.server; - -import java.io.BufferedWriter; -import java.io.ByteArrayOutputStream; -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.io.Serializable; -import java.io.StringWriter; -import java.lang.reflect.Type; -import java.net.URI; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.text.CharacterIterator; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.text.SimpleDateFormat; -import java.text.StringCharacterIterator; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.servlet.http.HttpServletResponse; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import com.vaadin.annotations.JavaScript; -import com.vaadin.annotations.PreserveOnRefresh; -import com.vaadin.annotations.StyleSheet; -import com.vaadin.server.ClientConnector.ConnectorErrorEvent; -import com.vaadin.server.ComponentSizeValidator.InvalidLayout; -import com.vaadin.server.ServerRpcManager.RpcInvocationException; -import com.vaadin.server.StreamVariable.StreamingEndEvent; -import com.vaadin.server.StreamVariable.StreamingErrorEvent; -import com.vaadin.shared.ApplicationConstants; -import com.vaadin.shared.Connector; -import com.vaadin.shared.JavaScriptConnectorState; -import com.vaadin.shared.Version; -import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; -import com.vaadin.shared.communication.MethodInvocation; -import com.vaadin.shared.communication.ServerRpc; -import com.vaadin.shared.communication.SharedState; -import com.vaadin.shared.communication.UidlValue; -import com.vaadin.shared.ui.ui.UIConstants; -import com.vaadin.ui.Component; -import com.vaadin.ui.ConnectorTracker; -import com.vaadin.ui.HasComponents; -import com.vaadin.ui.LegacyComponent; -import com.vaadin.ui.SelectiveRenderer; -import com.vaadin.ui.UI; -import com.vaadin.ui.Window; - -/** - * This is a common base class for the server-side implementations of the - * communication system between the client code (compiled with GWT into - * JavaScript) and the server side components. Its client side counterpart is - * {@link com.vaadin.client.ApplicationConnection}. - * <p> - * TODO Document better! - * - * @deprecated As of 7.0. Will likely change or be removed in a future version - */ -@Deprecated -@SuppressWarnings("serial") -public abstract class AbstractCommunicationManager implements Serializable { - - private static final String DASHDASH = "--"; - - private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler(); - - private static final RequestHandler CONNECTOR_RESOURCE_HANDLER = new ConnectorResourceHandler(); - - /** - * TODO Document me! - * - * @author peholmst - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version - */ - @Deprecated - public interface Callback extends Serializable { - - public void criticalNotification(VaadinRequest request, - VaadinResponse response, String cap, String msg, - String details, String outOfSyncURL) throws IOException; - } - - static class UploadInterruptedException extends Exception { - public UploadInterruptedException() { - super("Upload interrupted by other thread"); - } - } - - // flag used in the request to indicate that the security token should be - // written to the response - private static final String WRITE_SECURITY_TOKEN_FLAG = "writeSecurityToken"; - - /* Variable records indexes */ - public static final char VAR_BURST_SEPARATOR = '\u001d'; - - public static final char VAR_ESCAPE_CHARACTER = '\u001b'; - - private final HashMap<Integer, ClientCache> uiToClientCache = new HashMap<Integer, ClientCache>(); - - private static final int MAX_BUFFER_SIZE = 64 * 1024; - - /* Same as in apache commons file upload library that was previously used. */ - private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; - - /** - * The session this communication manager is used for - */ - private final VaadinSession session; - - private List<String> locales; - - private int pendingLocalesIndex; - - private int timeoutInterval = -1; - - private DragAndDropService dragAndDropService; - - private String requestThemeName; - - private int maxInactiveInterval; - - private ClientConnector highlightedConnector; - - private Map<String, Class<?>> publishedFileContexts = new HashMap<String, Class<?>>(); - - /** - * TODO New constructor - document me! - * - * @param session - */ - public AbstractCommunicationManager(VaadinSession session) { - this.session = session; - session.addRequestHandler(getBootstrapHandler()); - session.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER); - session.addRequestHandler(CONNECTOR_RESOURCE_HANDLER); - requireLocale(session.getLocale().toString()); - } - - protected VaadinSession getSession() { - return session; - } - - private static final int LF = "\n".getBytes()[0]; - - private static final String CRLF = "\r\n"; - - private static final String UTF8 = "UTF-8"; - - private static String readLine(InputStream stream) throws IOException { - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - int readByte = stream.read(); - while (readByte != LF) { - bout.write(readByte); - readByte = stream.read(); - } - byte[] bytes = bout.toByteArray(); - return new String(bytes, 0, bytes.length - 1, UTF8); - } - - /** - * Method used to stream content from a multipart request (either from - * servlet or portlet request) to given StreamVariable - * - * - * @param request - * @param response - * @param streamVariable - * @param owner - * @param boundary - * @throws IOException - */ - protected void doHandleSimpleMultipartFileUpload(VaadinRequest request, - VaadinResponse response, StreamVariable streamVariable, - String variableName, ClientConnector owner, String boundary) - throws IOException { - // multipart parsing, supports only one file for request, but that is - // fine for our current terminal - - final InputStream inputStream = request.getInputStream(); - - int contentLength = request.getContentLength(); - - boolean atStart = false; - boolean firstFileFieldFound = false; - - String rawfilename = "unknown"; - String rawMimeType = "application/octet-stream"; - - /* - * Read the stream until the actual file starts (empty line). Read - * filename and content type from multipart headers. - */ - while (!atStart) { - String readLine = readLine(inputStream); - contentLength -= (readLine.getBytes(UTF8).length + CRLF.length()); - if (readLine.startsWith("Content-Disposition:") - && readLine.indexOf("filename=") > 0) { - rawfilename = readLine.replaceAll(".*filename=", ""); - char quote = rawfilename.charAt(0); - rawfilename = rawfilename.substring(1); - rawfilename = rawfilename.substring(0, - rawfilename.indexOf(quote)); - firstFileFieldFound = true; - } else if (firstFileFieldFound && readLine.equals("")) { - atStart = true; - } else if (readLine.startsWith("Content-Type")) { - rawMimeType = readLine.split(": ")[1]; - } - } - - contentLength -= (boundary.length() + CRLF.length() + 2 - * DASHDASH.length() + CRLF.length()); - - /* - * Reads bytes from the underlying stream. Compares the read bytes to - * the boundary string and returns -1 if met. - * - * The matching happens so that if the read byte equals to the first - * char of boundary string, the stream goes to "buffering mode". In - * buffering mode bytes are read until the character does not match the - * corresponding from boundary string or the full boundary string is - * found. - * - * Note, if this is someday needed elsewhere, don't shoot yourself to - * foot and split to a top level helper class. - */ - InputStream simpleMultiPartReader = new SimpleMultiPartInputStream( - inputStream, boundary); - - /* - * Should report only the filename even if the browser sends the path - */ - final String filename = removePath(rawfilename); - final String mimeType = rawMimeType; - - try { - // TODO Shouldn't this check connectorEnabled? - if (owner == null) { - throw new UploadException( - "File upload ignored because the connector for the stream variable was not found"); - } - if (owner instanceof Component) { - if (((Component) owner).isReadOnly()) { - throw new UploadException( - "Warning: file upload ignored because the componente was read-only"); - } - } - boolean forgetVariable = streamToReceiver(simpleMultiPartReader, - streamVariable, filename, mimeType, contentLength); - if (forgetVariable) { - cleanStreamVariable(owner, variableName); - } - } catch (Exception e) { - session.lock(); - try { - handleConnectorRelatedException(owner, e); - } finally { - session.unlock(); - } - } - sendUploadResponse(request, response); - - } - - /** - * Used to stream plain file post (aka XHR2.post(File)) - * - * @param request - * @param response - * @param streamVariable - * @param owner - * @param contentLength - * @throws IOException - */ - protected void doHandleXhrFilePost(VaadinRequest request, - VaadinResponse response, StreamVariable streamVariable, - String variableName, ClientConnector owner, int contentLength) - throws IOException { - - // These are unknown in filexhr ATM, maybe add to Accept header that - // is accessible in portlets - final String filename = "unknown"; - final String mimeType = filename; - final InputStream stream = request.getInputStream(); - try { - /* - * safe cast as in GWT terminal all variable owners are expected to - * be components. - */ - Component component = (Component) owner; - if (component.isReadOnly()) { - throw new UploadException( - "Warning: file upload ignored because the component was read-only"); - } - boolean forgetVariable = streamToReceiver(stream, streamVariable, - filename, mimeType, contentLength); - if (forgetVariable) { - cleanStreamVariable(owner, variableName); - } - } catch (Exception e) { - session.lock(); - try { - handleConnectorRelatedException(owner, e); - } finally { - session.unlock(); - } - } - sendUploadResponse(request, response); - } - - /** - * @param in - * @param streamVariable - * @param filename - * @param type - * @param contentLength - * @return true if the streamvariable has informed that the terminal can - * forget this variable - * @throws UploadException - */ - protected final boolean streamToReceiver(final InputStream in, - StreamVariable streamVariable, String filename, String type, - int contentLength) throws UploadException { - if (streamVariable == null) { - throw new IllegalStateException( - "StreamVariable for the post not found"); - } - - final VaadinSession session = getSession(); - - OutputStream out = null; - int totalBytes = 0; - StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( - filename, type, contentLength); - try { - boolean listenProgress; - session.lock(); - try { - streamVariable.streamingStarted(startedEvent); - out = streamVariable.getOutputStream(); - listenProgress = streamVariable.listenProgress(); - } finally { - session.unlock(); - } - - // Gets the output target stream - if (out == null) { - throw new NoOutputStreamException(); - } - - if (null == in) { - // No file, for instance non-existent filename in html upload - throw new NoInputStreamException(); - } - - final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; - int bytesReadToBuffer = 0; - while ((bytesReadToBuffer = in.read(buffer)) > 0) { - out.write(buffer, 0, bytesReadToBuffer); - totalBytes += bytesReadToBuffer; - if (listenProgress) { - // update progress if listener set and contentLength - // received - session.lock(); - try { - StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( - filename, type, contentLength, totalBytes); - streamVariable.onProgress(progressEvent); - } finally { - session.unlock(); - } - } - if (streamVariable.isInterrupted()) { - throw new UploadInterruptedException(); - } - } - - // upload successful - out.close(); - StreamingEndEvent event = new StreamingEndEventImpl(filename, type, - totalBytes); - session.lock(); - try { - streamVariable.streamingFinished(event); - } finally { - session.unlock(); - } - - } catch (UploadInterruptedException e) { - // Download interrupted by application code - tryToCloseStream(out); - StreamingErrorEvent event = new StreamingErrorEventImpl(filename, - type, contentLength, totalBytes, e); - session.lock(); - try { - streamVariable.streamingFailed(event); - } finally { - session.unlock(); - } - // Note, we are not throwing interrupted exception forward as it is - // not a terminal level error like all other exception. - } catch (final Exception e) { - tryToCloseStream(out); - session.lock(); - try { - StreamingErrorEvent event = new StreamingErrorEventImpl( - filename, type, contentLength, totalBytes, e); - streamVariable.streamingFailed(event); - // throw exception for terminal to be handled (to be passed to - // terminalErrorHandler) - throw new UploadException(e); - } finally { - session.unlock(); - } - } - return startedEvent.isDisposed(); - } - - static void tryToCloseStream(OutputStream out) { - try { - // try to close output stream (e.g. file handle) - if (out != null) { - out.close(); - } - } catch (IOException e1) { - // NOP - } - } - - /** - * Removes any possible path information from the filename and returns the - * filename. Separators / and \\ are used. - * - * @param name - * @return - */ - private static String removePath(String filename) { - if (filename != null) { - filename = filename.replaceAll("^.*[/\\\\]", ""); - } - - return filename; - } - - /** - * TODO document - * - * @param request - * @param response - * @throws IOException - */ - protected void sendUploadResponse(VaadinRequest request, - VaadinResponse response) throws IOException { - response.setContentType("text/html"); - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - outWriter.print("<html><body>download handled</body></html>"); - outWriter.flush(); - out.close(); - } - - /** - * Internally process a UIDL request from the client. - * - * This method calls - * {@link #handleVariables(VaadinRequest, VaadinResponse, Callback, VaadinSession, UI)} - * to process any changes to variables by the client and then repaints - * affected components using {@link #paintAfterVariableChanges()}. - * - * Also, some cleanup is done when a request arrives for an session that has - * already been closed. - * - * The method handleUidlRequest(...) in subclasses should call this method. - * - * TODO better documentation - * - * @param request - * @param response - * @param callback - * @param uI - * target window for the UIDL request, can be null if target not - * found - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - * @throws JSONException - */ - public void handleUidlRequest(VaadinRequest request, - VaadinResponse response, Callback callback, UI uI) - throws IOException, InvalidUIDLSecurityKeyException, JSONException { - - checkWidgetsetVersion(request); - requestThemeName = request.getParameter("theme"); - maxInactiveInterval = request.getWrappedSession() - .getMaxInactiveInterval(); - // repaint requested or session has timed out and new one is created - boolean repaintAll; - final OutputStream out; - - repaintAll = (request - .getParameter(ApplicationConstants.URL_PARAMETER_REPAINT_ALL) != null); - // || (request.getSession().isNew()); FIXME What the h*ll is this?? - out = response.getOutputStream(); - - boolean analyzeLayouts = false; - if (repaintAll) { - // analyzing can be done only with repaintAll - analyzeLayouts = (request - .getParameter(ApplicationConstants.PARAM_ANALYZE_LAYOUTS) != null); - - String pid = request - .getParameter(ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR); - if (pid != null) { - highlightedConnector = uI.getConnectorTracker().getConnector( - pid); - highlightConnector(highlightedConnector); - } - } - - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - - // The rest of the process is synchronized with the session - // in order to guarantee that no parallel variable handling is - // made - session.lock(); - try { - - // Verify that there's an UI - if (uI == null) { - // This should not happen, no windows exists but - // session is still open. - getLogger().warning("Could not get UI for session"); - return; - } - - session.setLastRequestTimestamp(System.currentTimeMillis()); - - // Change all variables based on request parameters - if (!handleVariables(request, response, callback, session, uI)) { - - // var inconsistency; the client is probably out-of-sync - SystemMessages ci = response.getService().getSystemMessages( - uI.getLocale(), request); - String msg = ci.getOutOfSyncMessage(); - String cap = ci.getOutOfSyncCaption(); - if (msg != null || cap != null) { - callback.criticalNotification(request, response, cap, msg, - null, ci.getOutOfSyncURL()); - // will reload page after this - return; - } - // No message to show, let's just repaint all. - repaintAll = true; - } - - paintAfterVariableChanges(request, response, callback, repaintAll, - outWriter, uI, analyzeLayouts); - postPaint(uI); - } finally { - session.unlock(); - } - - outWriter.close(); - requestThemeName = null; - } - - /** - * Checks that the version reported by the client (widgetset) matches that - * of the server. - * - * @param request - */ - private void checkWidgetsetVersion(VaadinRequest request) { - String widgetsetVersion = request.getParameter("v-wsver"); - if (widgetsetVersion == null) { - // Only check when the widgetset version is reported. It is reported - // in the first UIDL request (not the initial request as it is a - // plain GET /) - return; - } - - if (!Version.getFullVersion().equals(widgetsetVersion)) { - getLogger().warning( - String.format(Constants.WIDGETSET_MISMATCH_INFO, - Version.getFullVersion(), widgetsetVersion)); - } - } - - /** - * Method called after the paint phase while still being synchronized on the - * session - * - * @param uI - * - */ - protected void postPaint(UI uI) { - // Remove connectors that have been detached from the session during - // handling of the request - uI.getConnectorTracker().cleanConnectorMap(); - } - - protected void highlightConnector(ClientConnector highlightedConnector) { - StringBuilder sb = new StringBuilder(); - sb.append("*** Debug details of a connector: *** \n"); - sb.append("Type: "); - sb.append(highlightedConnector.getClass().getName()); - sb.append("\nId:"); - sb.append(highlightedConnector.getConnectorId()); - if (highlightedConnector instanceof Component) { - Component component = (Component) highlightedConnector; - if (component.getCaption() != null) { - sb.append("\nCaption:"); - sb.append(component.getCaption()); - } - } - printHighlightedConnectorHierarchy(sb, highlightedConnector); - getLogger().info(sb.toString()); - } - - protected void printHighlightedConnectorHierarchy(StringBuilder sb, - ClientConnector connector) { - LinkedList<ClientConnector> h = new LinkedList<ClientConnector>(); - h.add(connector); - ClientConnector parent = connector.getParent(); - while (parent != null) { - h.addFirst(parent); - parent = parent.getParent(); - } - - sb.append("\nConnector hierarchy:\n"); - VaadinSession session2 = connector.getUI().getSession(); - sb.append(session2.getClass().getName()); - sb.append("("); - sb.append(session2.getClass().getSimpleName()); - sb.append(".java"); - sb.append(":1)"); - int l = 1; - for (ClientConnector connector2 : h) { - sb.append("\n"); - for (int i = 0; i < l; i++) { - sb.append(" "); - } - l++; - Class<? extends ClientConnector> connectorClass = connector2 - .getClass(); - Class<?> topClass = connectorClass; - while (topClass.getEnclosingClass() != null) { - topClass = topClass.getEnclosingClass(); - } - sb.append(connectorClass.getName()); - sb.append("("); - sb.append(topClass.getSimpleName()); - sb.append(".java:1)"); - } - } - - /** - * TODO document - * - * @param request - * @param response - * @param callback - * @param repaintAll - * @param outWriter - * @param window - * @param analyzeLayouts - * @throws PaintException - * @throws IOException - * @throws JSONException - */ - private void paintAfterVariableChanges(VaadinRequest request, - VaadinResponse response, Callback callback, boolean repaintAll, - final PrintWriter outWriter, UI uI, boolean analyzeLayouts) - throws PaintException, IOException, JSONException { - openJsonMessage(outWriter, response); - - // security key - Object writeSecurityTokenFlag = request - .getAttribute(WRITE_SECURITY_TOKEN_FLAG); - - if (writeSecurityTokenFlag != null) { - outWriter.print(getSecurityKeyUIDL(request)); - } - - writeUidlResponse(request, repaintAll, outWriter, uI, analyzeLayouts); - - closeJsonMessage(outWriter); - - outWriter.close(); - - } - - /** - * Gets the security key (and generates one if needed) as UIDL. - * - * @param request - * @return the security key UIDL or "" if the feature is turned off - */ - public String getSecurityKeyUIDL(VaadinRequest request) { - final String seckey = getSecurityKey(request); - if (seckey != null) { - return "\"" + ApplicationConstants.UIDL_SECURITY_TOKEN_ID + "\":\"" - + seckey + "\","; - } else { - return ""; - } - } - - /** - * Gets the security key (and generates one if needed). - * - * @param request - * @return the security key - */ - protected String getSecurityKey(VaadinRequest request) { - String seckey = null; - WrappedSession session = request.getWrappedSession(); - seckey = (String) session - .getAttribute(ApplicationConstants.UIDL_SECURITY_TOKEN_ID); - if (seckey == null) { - seckey = UUID.randomUUID().toString(); - session.setAttribute(ApplicationConstants.UIDL_SECURITY_TOKEN_ID, - seckey); - } - - return seckey; - } - - @SuppressWarnings("unchecked") - public void writeUidlResponse(VaadinRequest request, boolean repaintAll, - final PrintWriter outWriter, UI ui, boolean analyzeLayouts) - throws PaintException, JSONException { - ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>(); - VaadinSession session = ui.getSession(); - // Paints components - ConnectorTracker uiConnectorTracker = ui.getConnectorTracker(); - getLogger().log(Level.FINE, "* Creating response to client"); - if (repaintAll) { - getClientCache(ui).clear(); - uiConnectorTracker.markAllConnectorsDirty(); - uiConnectorTracker.markAllClientSidesUninitialized(); - - // Reset sent locales - locales = null; - requireLocale(session.getLocale().toString()); - } - - dirtyVisibleConnectors - .addAll(getDirtyVisibleConnectors(uiConnectorTracker)); - - getLogger().log(Level.FINE, "Found {0} dirty connectors to paint", - dirtyVisibleConnectors.size()); - for (ClientConnector connector : dirtyVisibleConnectors) { - boolean initialized = uiConnectorTracker - .isClientSideInitialized(connector); - connector.beforeClientResponse(!initialized); - } - - uiConnectorTracker.setWritingResponse(true); - try { - outWriter.print("\"changes\":["); - - List<InvalidLayout> invalidComponentRelativeSizes = null; - - JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter, - !repaintAll); - legacyPaint(paintTarget, dirtyVisibleConnectors); - - if (analyzeLayouts) { - invalidComponentRelativeSizes = ComponentSizeValidator - .validateComponentRelativeSizes(ui.getContent(), null, - null); - - // Also check any existing subwindows - if (ui.getWindows() != null) { - for (Window subWindow : ui.getWindows()) { - invalidComponentRelativeSizes = ComponentSizeValidator - .validateComponentRelativeSizes( - subWindow.getContent(), - invalidComponentRelativeSizes, null); - } - } - } - - paintTarget.close(); - outWriter.print("], "); // close changes - - // send shared state to client - - // for now, send the complete state of all modified and new - // components - - // Ideally, all this would be sent before "changes", but that causes - // complications with legacy components that create sub-components - // in their paint phase. Nevertheless, this will be processed on the - // client after component creation but before legacy UIDL - // processing. - JSONObject sharedStates = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - // encode and send shared state - try { - JSONObject stateJson = connector.encodeState(); - - if (stateJson != null && stateJson.length() != 0) { - sharedStates.put(connector.getConnectorId(), stateJson); - } - } catch (JSONException e) { - throw new PaintException( - "Failed to serialize shared state for connector " - + connector.getClass().getName() + " (" - + connector.getConnectorId() + "): " - + e.getMessage(), e); - } - } - outWriter.print("\"state\":"); - outWriter.append(sharedStates.toString()); - outWriter.print(", "); // close states - - // TODO This should be optimized. The type only needs to be - // sent once for each connector id + on refresh. Use the same cache - // as - // widget mapping - - JSONObject connectorTypes = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - String connectorType = paintTarget.getTag(connector); - try { - connectorTypes.put(connector.getConnectorId(), - connectorType); - } catch (JSONException e) { - throw new PaintException( - "Failed to send connector type for connector " - + connector.getConnectorId() + ": " - + e.getMessage(), e); - } - } - outWriter.print("\"types\":"); - outWriter.append(connectorTypes.toString()); - outWriter.print(", "); // close states - - // Send update hierarchy information to the client. - - // This could be optimized aswell to send only info if hierarchy has - // actually changed. Much like with the shared state. Note though - // that an empty hierarchy is information aswell (e.g. change from 1 - // child to 0 children) - - outWriter.print("\"hierarchy\":"); - - JSONObject hierarchyInfo = new JSONObject(); - for (ClientConnector connector : dirtyVisibleConnectors) { - String connectorId = connector.getConnectorId(); - JSONArray children = new JSONArray(); - - for (ClientConnector child : AbstractClientConnector - .getAllChildrenIterable(connector)) { - if (isConnectorVisibleToClient(child)) { - children.put(child.getConnectorId()); - } - } - try { - hierarchyInfo.put(connectorId, children); - } catch (JSONException e) { - throw new PaintException( - "Failed to send hierarchy information about " - + connectorId + " to the client: " - + e.getMessage(), e); - } - } - outWriter.append(hierarchyInfo.toString()); - outWriter.print(", "); // close hierarchy - - uiConnectorTracker.markAllConnectorsClean(); - - // send server to client RPC calls for components in the UI, in call - // order - - // collect RPC calls from components in the UI in the order in - // which they were performed, remove the calls from components - - LinkedList<ClientConnector> rpcPendingQueue = new LinkedList<ClientConnector>( - dirtyVisibleConnectors); - List<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(dirtyVisibleConnectors); - - JSONArray rpcCalls = new JSONArray(); - for (ClientMethodInvocation invocation : pendingInvocations) { - // add invocation to rpcCalls - try { - JSONArray invocationJson = new JSONArray(); - invocationJson.put(invocation.getConnector() - .getConnectorId()); - invocationJson.put(invocation.getInterfaceName()); - invocationJson.put(invocation.getMethodName()); - JSONArray paramJson = new JSONArray(); - for (int i = 0; i < invocation.getParameterTypes().length; ++i) { - Type parameterType = invocation.getParameterTypes()[i]; - Object referenceParameter = null; - // TODO Use default values for RPC parameter types - // if (!JsonCodec.isInternalType(parameterType)) { - // try { - // referenceParameter = parameterType.newInstance(); - // } catch (Exception e) { - // logger.log(Level.WARNING, - // "Error creating reference object for parameter of type " - // + parameterType.getName()); - // } - // } - EncodeResult encodeResult = JsonCodec.encode( - invocation.getParameters()[i], - referenceParameter, parameterType, - ui.getConnectorTracker()); - paramJson.put(encodeResult.getEncodedValue()); - } - invocationJson.put(paramJson); - rpcCalls.put(invocationJson); - } catch (JSONException e) { - throw new PaintException( - "Failed to serialize RPC method call parameters for connector " - + invocation.getConnector() - .getConnectorId() + " method " - + invocation.getInterfaceName() + "." - + invocation.getMethodName() + ": " - + e.getMessage(), e); - } - - } - - if (rpcCalls.length() > 0) { - outWriter.print("\"rpc\" : "); - outWriter.append(rpcCalls.toString()); - outWriter.print(", "); // close rpc - } - - outWriter.print("\"meta\" : {"); - boolean metaOpen = false; - - if (repaintAll) { - metaOpen = true; - outWriter.write("\"repaintAll\":true"); - if (analyzeLayouts) { - outWriter.write(", \"invalidLayouts\":"); - outWriter.write("["); - if (invalidComponentRelativeSizes != null) { - boolean first = true; - for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { - if (!first) { - outWriter.write(","); - } else { - first = false; - } - invalidLayout.reportErrors(outWriter, this, - System.err); - } - } - outWriter.write("]"); - } - if (highlightedConnector != null) { - outWriter.write(", \"hl\":\""); - outWriter.write(highlightedConnector.getConnectorId()); - outWriter.write("\""); - highlightedConnector = null; - } - } - - SystemMessages ci = request.getService().getSystemMessages( - ui.getLocale(), request); - - // meta instruction for client to enable auto-forward to - // sessionExpiredURL after timer expires. - if (ci != null && ci.getSessionExpiredMessage() == null - && ci.getSessionExpiredCaption() == null - && ci.isSessionExpiredNotificationEnabled()) { - int newTimeoutInterval = getTimeoutInterval(); - if (repaintAll || (timeoutInterval != newTimeoutInterval)) { - String escapedURL = ci.getSessionExpiredURL() == null ? "" - : ci.getSessionExpiredURL().replace("/", "\\/"); - if (metaOpen) { - outWriter.write(","); - } - outWriter.write("\"timedRedirect\":{\"interval\":" - + (newTimeoutInterval + 15) + ",\"url\":\"" - + escapedURL + "\"}"); - metaOpen = true; - } - timeoutInterval = newTimeoutInterval; - } - - outWriter.print("}, \"resources\" : {"); - - // Precache custom layouts - - // TODO We should only precache the layouts that are not - // cached already (plagiate from usedPaintableTypes) - int resourceIndex = 0; - for (final Iterator<Object> i = paintTarget.getUsedResources() - .iterator(); i.hasNext();) { - final String resource = (String) i.next(); - InputStream is = null; - try { - is = getThemeResourceAsStream(ui, getTheme(ui), resource); - } catch (final Exception e) { - // FIXME: Handle exception - getLogger().log(Level.FINER, - "Failed to get theme resource stream.", e); - } - if (is != null) { - - outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\"" - + resource + "\" : "); - final StringBuffer layout = new StringBuffer(); - - try { - final InputStreamReader r = new InputStreamReader(is, - "UTF-8"); - final char[] buffer = new char[20000]; - int charsRead = 0; - while ((charsRead = r.read(buffer)) > 0) { - layout.append(buffer, 0, charsRead); - } - r.close(); - } catch (final java.io.IOException e) { - // FIXME: Handle exception - getLogger().log(Level.INFO, "Resource transfer failed", - e); - } - outWriter.print("\"" - + JsonPaintTarget.escapeJSON(layout.toString()) - + "\""); - } else { - // FIXME: Handle exception - getLogger().severe("CustomLayout not found: " + resource); - } - } - outWriter.print("}"); - - Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget - .getUsedClientConnectors(); - boolean typeMappingsOpen = false; - ClientCache clientCache = getClientCache(ui); - - List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); - - for (Class<? extends ClientConnector> class1 : usedClientConnectors) { - if (clientCache.cache(class1)) { - // client does not know the mapping key for this type, send - // mapping to client - newConnectorTypes.add(class1); - - if (!typeMappingsOpen) { - typeMappingsOpen = true; - outWriter.print(", \"typeMappings\" : { "); - } else { - outWriter.print(" , "); - } - String canonicalName = class1.getCanonicalName(); - outWriter.print("\""); - outWriter.print(canonicalName); - outWriter.print("\" : "); - outWriter.print(getTagForType(class1)); - } - } - if (typeMappingsOpen) { - outWriter.print(" }"); - } - - boolean typeInheritanceMapOpen = false; - if (typeMappingsOpen) { - // send the whole type inheritance map if any new mappings - for (Class<? extends ClientConnector> class1 : usedClientConnectors) { - if (!ClientConnector.class.isAssignableFrom(class1 - .getSuperclass())) { - continue; - } - if (!typeInheritanceMapOpen) { - typeInheritanceMapOpen = true; - outWriter.print(", \"typeInheritanceMap\" : { "); - } else { - outWriter.print(" , "); - } - outWriter.print("\""); - outWriter.print(getTagForType(class1)); - outWriter.print("\" : "); - outWriter - .print(getTagForType((Class<? extends ClientConnector>) class1 - .getSuperclass())); - } - if (typeInheritanceMapOpen) { - outWriter.print(" }"); - } - } - - /* - * Ensure super classes come before sub classes to get script - * dependency order right. Sub class @JavaScript might assume that - * - * @JavaScript defined by super class is already loaded. - */ - Collections.sort(newConnectorTypes, new Comparator<Class<?>>() { - @Override - public int compare(Class<?> o1, Class<?> o2) { - // TODO optimize using Class.isAssignableFrom? - return hierarchyDepth(o1) - hierarchyDepth(o2); - } - - private int hierarchyDepth(Class<?> type) { - if (type == Object.class) { - return 0; - } else { - return hierarchyDepth(type.getSuperclass()) + 1; - } - } - }); - - List<String> scriptDependencies = new ArrayList<String>(); - List<String> styleDependencies = new ArrayList<String>(); - - for (Class<? extends ClientConnector> class1 : newConnectorTypes) { - JavaScript jsAnnotation = class1 - .getAnnotation(JavaScript.class); - if (jsAnnotation != null) { - for (String uri : jsAnnotation.value()) { - scriptDependencies.add(registerDependency(uri, class1)); - } - } - - StyleSheet styleAnnotation = class1 - .getAnnotation(StyleSheet.class); - if (styleAnnotation != null) { - for (String uri : styleAnnotation.value()) { - styleDependencies.add(registerDependency(uri, class1)); - } - } - } - - // Include script dependencies in output if there are any - if (!scriptDependencies.isEmpty()) { - outWriter.print(", \"scriptDependencies\": " - + new JSONArray(scriptDependencies).toString()); - } - - // Include style dependencies in output if there are any - if (!styleDependencies.isEmpty()) { - outWriter.print(", \"styleDependencies\": " - + new JSONArray(styleDependencies).toString()); - } - - // add any pending locale definitions requested by the client - printLocaleDeclarations(outWriter); - - if (dragAndDropService != null) { - dragAndDropService.printJSONResponse(outWriter); - } - - for (ClientConnector connector : dirtyVisibleConnectors) { - uiConnectorTracker.markClientSideInitialized(connector); - } - - assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; - - writePerformanceData(outWriter); - } finally { - uiConnectorTracker.setWritingResponse(false); - } - } - - public static JSONObject encodeState(ClientConnector connector, - SharedState state) throws JSONException { - UI uI = connector.getUI(); - ConnectorTracker connectorTracker = uI.getConnectorTracker(); - Class<? extends SharedState> stateType = connector.getStateType(); - Object diffState = connectorTracker.getDiffState(connector); - boolean supportsDiffState = !JavaScriptConnectorState.class - .isAssignableFrom(stateType); - if (diffState == null && supportsDiffState) { - // Use an empty state object as reference for full - // repaints - - try { - SharedState referenceState = stateType.newInstance(); - EncodeResult encodeResult = JsonCodec.encode(referenceState, - null, stateType, uI.getConnectorTracker()); - diffState = encodeResult.getEncodedValue(); - } catch (Exception e) { - getLogger() - .log(Level.WARNING, - "Error creating reference object for state of type {0}", - stateType.getName()); - } - } - EncodeResult encodeResult = JsonCodec.encode(state, diffState, - stateType, uI.getConnectorTracker()); - if (supportsDiffState) { - connectorTracker.setDiffState(connector, - (JSONObject) encodeResult.getEncodedValue()); - } - return (JSONObject) encodeResult.getDiff(); - } - - /** - * Resolves a dependency URI, registering the URI with this - * {@code AbstractCommunicationManager} if needed and returns a fully - * qualified URI. - */ - private String registerDependency(String resourceUri, Class<?> context) { - try { - URI uri = new URI(resourceUri); - String protocol = uri.getScheme(); - - if (ApplicationConstants.PUBLISHED_PROTOCOL_NAME.equals(protocol)) { - // Strip initial slash - String resourceName = uri.getPath().substring(1); - return registerPublishedFile(resourceName, context); - } - - if (protocol != null || uri.getHost() != null) { - return resourceUri; - } - - // Bare path interpreted as published file - return registerPublishedFile(resourceUri, context); - } catch (URISyntaxException e) { - getLogger().log(Level.WARNING, - "Could not parse resource url " + resourceUri, e); - return resourceUri; - } - } - - private String registerPublishedFile(String name, Class<?> context) { - synchronized (publishedFileContexts) { - // Add to map of names accepted by servePublishedFile - if (publishedFileContexts.containsKey(name)) { - Class<?> oldContext = publishedFileContexts.get(name); - if (oldContext != context) { - getLogger() - .log(Level.WARNING, - "{0} published by both {1} and {2}. File from {2} will be used.", - new Object[] { name, context, oldContext }); - } - } else { - publishedFileContexts.put(name, context); - } - } - - return ApplicationConstants.PUBLISHED_PROTOCOL_PREFIX + "/" + name; - } - - /** - * Adds the performance timing data (used by TestBench 3) to the UIDL - * response. - */ - private void writePerformanceData(final PrintWriter outWriter) { - outWriter.write(String.format(", \"timings\":[%d, %d]", - session.getCumulativeRequestDuration(), - session.getLastRequestDuration())); - } - - private void legacyPaint(PaintTarget paintTarget, - ArrayList<ClientConnector> dirtyVisibleConnectors) - throws PaintException { - List<LegacyComponent> legacyComponents = new ArrayList<LegacyComponent>(); - for (Connector connector : dirtyVisibleConnectors) { - // All Components that want to use paintContent must implement - // LegacyComponent - if (connector instanceof LegacyComponent) { - legacyComponents.add((LegacyComponent) connector); - } - } - sortByHierarchy((List) legacyComponents); - for (LegacyComponent c : legacyComponents) { - if (getLogger().isLoggable(Level.FINE)) { - getLogger().log( - Level.FINE, - "Painting LegacyComponent {0}@{1}", - new Object[] { c.getClass().getName(), - Integer.toHexString(c.hashCode()) }); - } - paintTarget.startTag("change"); - final String pid = c.getConnectorId(); - paintTarget.addAttribute("pid", pid); - LegacyPaint.paint(c, paintTarget); - paintTarget.endTag("change"); - } - - } - - private void sortByHierarchy(List<Component> paintables) { - // Vaadin 6 requires parents to be painted before children as component - // containers rely on that their updateFromUIDL method has been called - // before children start calling e.g. updateCaption - Collections.sort(paintables, new Comparator<Component>() { - - @Override - public int compare(Component c1, Component c2) { - int depth1 = 0; - while (c1.getParent() != null) { - depth1++; - c1 = c1.getParent(); - } - int depth2 = 0; - while (c2.getParent() != null) { - depth2++; - c2 = c2.getParent(); - } - if (depth1 < depth2) { - return -1; - } - if (depth1 > depth2) { - return 1; - } - return 0; - } - }); - - } - - private ClientCache getClientCache(UI uI) { - Integer uiId = Integer.valueOf(uI.getUIId()); - ClientCache cache = uiToClientCache.get(uiId); - if (cache == null) { - cache = new ClientCache(); - uiToClientCache.put(uiId, cache); - } - return cache; - } - - /** - * Checks if the connector is visible in context. For Components, - * {@link #isComponentVisibleToClient(Component)} is used. For other types - * of connectors, the contextual visibility of its first Component ancestor - * is used. If no Component ancestor is found, the connector is not visible. - * - * @param connector - * The connector to check - * @return <code>true</code> if the connector is visible to the client, - * <code>false</code> otherwise - */ - public static boolean isConnectorVisibleToClient(ClientConnector connector) { - if (connector instanceof Component) { - return isComponentVisibleToClient((Component) connector); - } else { - ClientConnector parent = connector.getParent(); - if (parent == null) { - return false; - } else { - return isConnectorVisibleToClient(parent); - } - } - } - - /** - * Checks if the component should be visible to the client. Returns false if - * the child should not be sent to the client, true otherwise. - * - * @param child - * The child to check - * @return true if the child is visible to the client, false otherwise - */ - public static boolean isComponentVisibleToClient(Component child) { - if (!child.isVisible()) { - return false; - } - HasComponents parent = child.getParent(); - - if (parent instanceof SelectiveRenderer) { - if (!((SelectiveRenderer) parent).isRendered(child)) { - return false; - } - } - - if (parent != null) { - return isComponentVisibleToClient(parent); - } else { - if (child instanceof UI) { - // UI has no parent and visibility was checked above - return true; - } else { - // Component which is not attached to any UI - return false; - } - } - } - - private static class NullIterator<E> implements Iterator<E> { - - @Override - public boolean hasNext() { - return false; - } - - @Override - public E next() { - return null; - } - - @Override - public void remove() { - } - - } - - /** - * Collects all pending RPC calls from listed {@link ClientConnector}s and - * clears their RPC queues. - * - * @param rpcPendingQueue - * list of {@link ClientConnector} of interest - * @return ordered list of pending RPC calls - */ - private List<ClientMethodInvocation> collectPendingRpcCalls( - List<ClientConnector> rpcPendingQueue) { - List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); - for (ClientConnector connector : rpcPendingQueue) { - List<ClientMethodInvocation> paintablePendingRpc = connector - .retrievePendingRpcCalls(); - if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { - List<ClientMethodInvocation> oldPendingRpc = pendingInvocations; - int totalCalls = pendingInvocations.size() - + paintablePendingRpc.size(); - pendingInvocations = new ArrayList<ClientMethodInvocation>( - totalCalls); - - // merge two ordered comparable lists - for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { - if (paintableIndex >= paintablePendingRpc.size() - || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc - .get(oldIndex)) - .compareTo(paintablePendingRpc - .get(paintableIndex)) <= 0)) { - pendingInvocations.add(oldPendingRpc.get(oldIndex++)); - } else { - pendingInvocations.add(paintablePendingRpc - .get(paintableIndex++)); - } - } - } - } - return pendingInvocations; - } - - protected abstract InputStream getThemeResourceAsStream(UI uI, - String themeName, String resource); - - private int getTimeoutInterval() { - return maxInactiveInterval; - } - - private String getTheme(UI uI) { - String themeName = uI.getTheme(); - String requestThemeName = getRequestTheme(); - - if (requestThemeName != null) { - themeName = requestThemeName; - } - if (themeName == null) { - themeName = VaadinServlet.getDefaultTheme(); - } - return themeName; - } - - private String getRequestTheme() { - return requestThemeName; - } - - /** - * Returns false if the cross site request forgery protection is turned off. - * - * @param session - * @return false if the XSRF is turned off, true otherwise - */ - public boolean isXSRFEnabled(VaadinSession session) { - return session.getConfiguration().isXsrfProtectionEnabled(); - } - - /** - * TODO document - * - * If this method returns false, something was submitted that we did not - * expect; this is probably due to the client being out-of-sync and sending - * variable changes for non-existing pids - * - * @return true if successful, false if there was an inconsistency - */ - private boolean handleVariables(VaadinRequest request, - VaadinResponse response, Callback callback, VaadinSession session, - UI uI) throws IOException, InvalidUIDLSecurityKeyException, - JSONException { - boolean success = true; - - String changes = getRequestPayload(request); - if (changes != null) { - - // Manage bursts one by one - final String[] bursts = changes.split(String - .valueOf(VAR_BURST_SEPARATOR)); - - // Security: double cookie submission pattern unless disabled by - // property - if (isXSRFEnabled(session)) { - if (bursts.length == 1 && "init".equals(bursts[0])) { - // init request; don't handle any variables, key sent in - // response. - request.setAttribute(WRITE_SECURITY_TOKEN_FLAG, true); - return true; - } else { - // ApplicationServlet has stored the security token in the - // session; check that it matched the one sent in the UIDL - String sessId = (String) request - .getWrappedSession() - .getAttribute( - ApplicationConstants.UIDL_SECURITY_TOKEN_ID); - - if (sessId == null || !sessId.equals(bursts[0])) { - throw new InvalidUIDLSecurityKeyException( - "Security key mismatch"); - } - } - - } - - for (int bi = 1; bi < bursts.length; bi++) { - // unescape any encoded separator characters in the burst - final String burst = unescapeBurst(bursts[bi]); - success &= handleBurst(request, uI, burst); - - // In case that there were multiple bursts, we know that this is - // a special synchronous case for closing window. Thus we are - // not interested in sending any UIDL changes back to client. - // Still we must clear component tree between bursts to ensure - // that no removed components are updated. The painting after - // the last burst is handled normally by the calling method. - if (bi < bursts.length - 1) { - - // We will be discarding all changes - final PrintWriter outWriter = new PrintWriter( - new CharArrayWriter()); - - paintAfterVariableChanges(request, response, callback, - true, outWriter, uI, false); - - } - - } - } - /* - * Note that we ignore inconsistencies while handling unload request. - * The client can't remove invalid variable changes from the burst, and - * we don't have the required logic implemented on the server side. E.g. - * a component is removed in a previous burst. - */ - return success; - } - - /** - * 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. - * - * 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 - * @return true if the processing of the burst was successful and there were - * no messages to non-existent components - */ - public boolean handleBurst(VaadinRequest source, UI uI, final String burst) { - boolean success = true; - try { - Set<Connector> enabledConnectors = new HashSet<Connector>(); - - List<MethodInvocation> invocations = parseInvocations( - uI.getConnectorTracker(), burst); - for (MethodInvocation invocation : invocations) { - final ClientConnector connector = getConnector(uI, - invocation.getConnectorId()); - - if (connector != null && connector.isConnectorEnabled()) { - enabledConnectors.add(connector); - } - } - - for (int i = 0; i < invocations.size(); i++) { - MethodInvocation invocation = invocations.get(i); - - final ClientConnector connector = getConnector(uI, - invocation.getConnectorId()); - if (connector == null) { - getLogger() - .log(Level.WARNING, - "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})", - new Object[] { invocation.getConnectorId(), - invocation.getInterfaceName(), - invocation.getMethodName() }); - continue; - } - - if (!enabledConnectors.contains(connector)) { - - if (invocation instanceof LegacyChangeVariablesInvocation) { - LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; - // TODO convert window close to a separate RPC call and - // handle above - not a variable change - - // Handle special case where window-close is called - // after the window has been removed from the - // application or the application has closed - Map<String, Object> changes = legacyInvocation - .getVariableChanges(); - if (changes.size() == 1 && changes.containsKey("close") - && Boolean.TRUE.equals(changes.get("close"))) { - // Silently ignore this - continue; - } - } - - // Connector is disabled, log a warning and move to the next - String msg = "Ignoring RPC call for disabled connector " - + connector.getClass().getName(); - if (connector instanceof Component) { - String caption = ((Component) connector).getCaption(); - if (caption != null) { - msg += ", caption=" + caption; - } - } - getLogger().warning(msg); - continue; - } - - if (invocation instanceof ServerRpcMethodInvocation) { - try { - ServerRpcManager.applyInvocation(connector, - (ServerRpcMethodInvocation) invocation); - } catch (RpcInvocationException e) { - handleConnectorRelatedException(connector, e); - } - } else { - - // All code below is for legacy variable changes - LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; - Map<String, Object> changes = legacyInvocation - .getVariableChanges(); - try { - if (connector instanceof VariableOwner) { - changeVariables(source, (VariableOwner) connector, - changes); - } else { - throw new IllegalStateException( - "Received legacy variable change for " - + connector.getClass().getName() - + " (" - + connector.getConnectorId() - + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: " - + changes.keySet()); - } - } catch (Exception e) { - handleConnectorRelatedException(connector, e); - } - } - } - } catch (JSONException e) { - getLogger().log(Level.WARNING, - "Unable to parse RPC call from the client: {0}", - e.getMessage()); - // TODO or return success = false? - throw new RuntimeException(e); - } - - return success; - } - - /** - * Handles an exception that occurred when processing Rpc calls or a file - * upload. - * - * @param ui - * The UI where the exception occured - * @param throwable - * The exception - * @param connector - * The Rpc target - */ - private void handleConnectorRelatedException(ClientConnector connector, - Throwable throwable) { - ErrorEvent errorEvent = new ConnectorErrorEvent(connector, throwable); - ErrorHandler handler = ErrorEvent.findErrorHandler(connector); - handler.error(errorEvent); - } - - /** - * Parse a message burst from the client into a list of MethodInvocation - * instances. - * - * @param connectorTracker - * The ConnectorTracker used to lookup connectors - * @param burst - * message string (JSON) - * @return list of MethodInvocation to perform - * @throws JSONException - */ - private List<MethodInvocation> parseInvocations( - ConnectorTracker connectorTracker, final String burst) - throws JSONException { - JSONArray invocationsJson = new JSONArray(burst); - - ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); - - MethodInvocation previousInvocation = null; - // parse JSON to MethodInvocations - for (int i = 0; i < invocationsJson.length(); ++i) { - - JSONArray invocationJson = invocationsJson.getJSONArray(i); - - MethodInvocation invocation = parseInvocation(invocationJson, - previousInvocation, connectorTracker); - 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 - // rejected because of an error. - invocations.add(invocation); - previousInvocation = invocation; - } - } - return invocations; - } - - private MethodInvocation parseInvocation(JSONArray invocationJson, - MethodInvocation previousInvocation, - ConnectorTracker connectorTracker) throws JSONException { - String connectorId = invocationJson.getString(0); - String interfaceName = invocationJson.getString(1); - String methodName = invocationJson.getString(2); - - 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(); - return null; - } - - JSONArray parametersJson = invocationJson.getJSONArray(3); - - if (LegacyChangeVariablesInvocation.isLegacyVariableChange( - interfaceName, methodName)) { - if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) { - previousInvocation = null; - } - - return parseLegacyChangeVariablesInvocation(connectorId, - interfaceName, methodName, - (LegacyChangeVariablesInvocation) previousInvocation, - parametersJson, connectorTracker); - } else { - return parseServerRpcInvocation(connectorId, interfaceName, - methodName, parametersJson, connectorTracker); - } - - } - - private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation( - String connectorId, String interfaceName, String methodName, - LegacyChangeVariablesInvocation previousInvocation, - JSONArray parametersJson, ConnectorTracker connectorTracker) - throws JSONException { - if (parametersJson.length() != 2) { - throw new JSONException( - "Invalid parameters in legacy change variables call. Expected 2, was " - + parametersJson.length()); - } - String variableName = parametersJson.getString(0); - UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType( - UidlValue.class, true, parametersJson.get(1), connectorTracker); - - Object value = uidlValue.getValue(); - - if (previousInvocation != null - && previousInvocation.getConnectorId().equals(connectorId)) { - previousInvocation.setVariableChange(variableName, value); - return null; - } else { - return new LegacyChangeVariablesInvocation(connectorId, - variableName, value); - } - } - - private ServerRpcMethodInvocation parseServerRpcInvocation( - String connectorId, String interfaceName, String methodName, - JSONArray parametersJson, ConnectorTracker connectorTracker) - throws JSONException { - ClientConnector connector = connectorTracker.getConnector(connectorId); - - ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName); - if (rpcManager == null) { - /* - * Security: Don't even decode the json parameters if no RpcManager - * corresponding to the received method invocation has been - * registered. - */ - getLogger() - .log(Level.WARNING, - "Ignoring RPC call to {0}.{1} in connector {2} ({3}) as no RPC implementation is regsitered", - new Object[] { interfaceName, methodName, - connector.getClass().getName(), connectorId }); - return null; - } - - // Use interface from RpcManager instead of loading the class based on - // the string name to avoid problems with OSGi - Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface(); - - ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( - connectorId, rpcInterface, methodName, parametersJson.length()); - - Object[] parameters = new Object[parametersJson.length()]; - Type[] declaredRpcMethodParameterTypes = invocation.getMethod() - .getGenericParameterTypes(); - - for (int j = 0; j < parametersJson.length(); ++j) { - Object parameterValue = parametersJson.get(j); - Type parameterType = declaredRpcMethodParameterTypes[j]; - parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, - parameterValue, connectorTracker); - } - invocation.setParameters(parameters); - return invocation; - } - - protected void changeVariables(Object source, final VariableOwner owner, - Map<String, Object> m) { - owner.changeVariables(source, m); - } - - protected ClientConnector getConnector(UI uI, String connectorId) { - ClientConnector c = uI.getConnectorTracker().getConnector(connectorId); - if (c == null - && connectorId.equals(getDragAndDropService().getConnectorId())) { - return getDragAndDropService(); - } - - return c; - } - - private DragAndDropService getDragAndDropService() { - if (dragAndDropService == null) { - dragAndDropService = new DragAndDropService(this); - } - return dragAndDropService; - } - - /** - * Reads the request data from the Request and returns it converted to an - * UTF-8 string. - * - * @param request - * @return - * @throws IOException - */ - protected String getRequestPayload(VaadinRequest request) - throws IOException { - - int requestLength = request.getContentLength(); - if (requestLength == 0) { - return null; - } - - ByteArrayOutputStream bout = requestLength <= 0 ? new ByteArrayOutputStream() - : new ByteArrayOutputStream(requestLength); - - InputStream inputStream = request.getInputStream(); - byte[] buffer = new byte[MAX_BUFFER_SIZE]; - - while (true) { - int read = inputStream.read(buffer); - if (read == -1) { - break; - } - bout.write(buffer, 0, read); - } - String result = new String(bout.toByteArray(), "utf-8"); - - return result; - } - - /** - * 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(); - } - - /** - * Prints the queued (pending) locale definitions to a {@link PrintWriter} - * in a (UIDL) format that can be sent to the client and used there in - * formatting dates, times etc. - * - * @param outWriter - */ - private void printLocaleDeclarations(PrintWriter outWriter) { - /* - * ----------------------------- Sending Locale sensitive date - * ----------------------------- - */ - - // Send locale informations to client - outWriter.print(", \"locales\":["); - for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { - - final Locale l = generateLocale(locales.get(pendingLocalesIndex)); - // Locale name - outWriter.print("{\"name\":\"" + l.toString() + "\","); - - /* - * Month names (both short and full) - */ - final DateFormatSymbols dfs = new DateFormatSymbols(l); - final String[] short_months = dfs.getShortMonths(); - final String[] months = dfs.getMonths(); - outWriter.print("\"smn\":[\"" - + // ShortMonthNames - short_months[0] + "\",\"" + short_months[1] + "\",\"" - + short_months[2] + "\",\"" + short_months[3] + "\",\"" - + short_months[4] + "\",\"" + short_months[5] + "\",\"" - + short_months[6] + "\",\"" + short_months[7] + "\",\"" - + short_months[8] + "\",\"" + short_months[9] + "\",\"" - + short_months[10] + "\",\"" + short_months[11] + "\"" - + "],"); - outWriter.print("\"mn\":[\"" - + // MonthNames - months[0] + "\",\"" + months[1] + "\",\"" + months[2] - + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" - + months[5] + "\",\"" + months[6] + "\",\"" + months[7] - + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" - + months[10] + "\",\"" + months[11] + "\"" + "],"); - - /* - * Weekday names (both short and full) - */ - final String[] short_days = dfs.getShortWeekdays(); - final String[] days = dfs.getWeekdays(); - outWriter.print("\"sdn\":[\"" - + // ShortDayNames - short_days[1] + "\",\"" + short_days[2] + "\",\"" - + short_days[3] + "\",\"" + short_days[4] + "\",\"" - + short_days[5] + "\",\"" + short_days[6] + "\",\"" - + short_days[7] + "\"" + "],"); - outWriter.print("\"dn\":[\"" - + // DayNames - days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" - + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" - + days[7] + "\"" + "],"); - - /* - * First day of week (0 = sunday, 1 = monday) - */ - final Calendar cal = new GregorianCalendar(l); - outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); - - /* - * Date formatting (MM/DD/YYYY etc.) - */ - - DateFormat dateFormat = DateFormat.getDateTimeInstance( - DateFormat.SHORT, DateFormat.SHORT, l); - if (!(dateFormat instanceof SimpleDateFormat)) { - getLogger().log(Level.WARNING, - "Unable to get default date pattern for locale {0}", l); - dateFormat = new SimpleDateFormat(); - } - final String df = ((SimpleDateFormat) dateFormat).toPattern(); - - int timeStart = df.indexOf("H"); - if (timeStart < 0) { - timeStart = df.indexOf("h"); - } - final int ampm_first = df.indexOf("a"); - // E.g. in Korean locale AM/PM is before h:mm - // TODO should take that into consideration on client-side as well, - // now always h:mm a - if (ampm_first > 0 && ampm_first < timeStart) { - timeStart = ampm_first; - } - // Hebrew locale has time before the date - final boolean timeFirst = timeStart == 0; - String dateformat; - if (timeFirst) { - int dateStart = df.indexOf(' '); - if (ampm_first > dateStart) { - dateStart = df.indexOf(' ', ampm_first); - } - dateformat = df.substring(dateStart + 1); - } else { - dateformat = df.substring(0, timeStart - 1); - } - - outWriter.print("\"df\":\"" + dateformat.trim() + "\","); - - /* - * Time formatting (24 or 12 hour clock and AM/PM suffixes) - */ - final String timeformat = df.substring(timeStart, df.length()); - /* - * Doesn't return second or milliseconds. - * - * We use timeformat to determine 12/24-hour clock - */ - final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; - // TODO there are other possibilities as well, like 'h' in french - // (ignore them, too complicated) - final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." - : ":"; - // outWriter.print("\"tf\":\"" + timeformat + "\","); - outWriter.print("\"thc\":" + twelve_hour_clock + ","); - outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\""); - if (twelve_hour_clock) { - final String[] ampm = dfs.getAmPmStrings(); - outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] - + "\"]"); - } - outWriter.print("}"); - if (pendingLocalesIndex < locales.size() - 1) { - outWriter.print(","); - } - } - outWriter.print("]"); // Close locales - } - - protected void closeJsonMessage(PrintWriter outWriter) { - outWriter.print("}]"); - } - - /** - * Writes the opening of JSON message to be sent to client. - * - * @param outWriter - * @param response - */ - protected void openJsonMessage(PrintWriter outWriter, - VaadinResponse response) { - // Sets the response type - response.setContentType("application/json; charset=UTF-8"); - // some dirt to prevent cross site scripting - outWriter.print("for(;;);[{"); - } - - /** - * Returns dirty components which are in given window. Components in an - * invisible subtrees are omitted. - * - * @param w - * UI window for which dirty components is to be fetched - * @return - */ - private ArrayList<ClientConnector> getDirtyVisibleConnectors( - ConnectorTracker connectorTracker) { - ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>(); - for (ClientConnector c : connectorTracker.getDirtyConnectors()) { - if (isConnectorVisibleToClient(c)) { - dirtyConnectors.add(c); - } - } - - return dirtyConnectors; - } - - /** - * Queues a locale to be sent to the client (browser) for date and time - * entry etc. All locale specific information is derived from server-side - * {@link Locale} instances and sent to the client when needed, eliminating - * the need to use the {@link Locale} class and all the framework behind it - * on the client. - * - * @see Locale#toString() - * - * @param value - */ - public void requireLocale(String value) { - if (locales == null) { - locales = new ArrayList<String>(); - locales.add(session.getLocale().toString()); - pendingLocalesIndex = 0; - } - if (!locales.contains(value)) { - locales.add(value); - } - } - - /** - * Constructs a {@link Locale} instance to be sent to the client based on a - * short locale description string. - * - * @see #requireLocale(String) - * - * @param value - * @return - */ - private Locale generateLocale(String value) { - final String[] temp = value.split("_"); - if (temp.length == 1) { - return new Locale(temp[0]); - } else if (temp.length == 2) { - return new Locale(temp[0], temp[1]); - } else { - return new Locale(temp[0], temp[1], temp[2]); - } - } - - protected class InvalidUIDLSecurityKeyException extends - GeneralSecurityException { - - InvalidUIDLSecurityKeyException(String message) { - super(message); - } - - } - - private final HashMap<Class<? extends ClientConnector>, Integer> typeToKey = new HashMap<Class<? extends ClientConnector>, Integer>(); - private int nextTypeKey = 0; - - private BootstrapHandler bootstrapHandler; - - String getTagForType(Class<? extends ClientConnector> class1) { - Integer id = typeToKey.get(class1); - if (id == null) { - id = nextTypeKey++; - typeToKey.put(class1, id); - if (getLogger().isLoggable(Level.FINE)) { - getLogger().log(Level.FINE, "Mapping {0} to {1}", - new Object[] { class1.getName(), id }); - } - } - return id.toString(); - } - - /** - * Helper class for terminal to keep track of data that client is expected - * to know. - * - * TODO make customlayout templates (from theme) to be cached here. - */ - class ClientCache implements Serializable { - - private final Set<Object> res = new HashSet<Object>(); - - /** - * - * @param paintable - * @return true if the given class was added to cache - */ - boolean cache(Object object) { - return res.add(object); - } - - public void clear() { - res.clear(); - } - - } - - public String getStreamVariableTargetUrl(ClientConnector owner, - String name, StreamVariable value) { - /* - * We will use the same APP/* URI space as ApplicationResources but - * prefix url with UPLOAD - * - * eg. APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] - * - * SECKEY is created on each paint to make URL's unpredictable (to - * prevent CSRF attacks). - * - * NAME and PID from URI forms a key to fetch StreamVariable when - * handling post - */ - String paintableId = owner.getConnectorId(); - UI ui = owner.getUI(); - int uiId = ui.getUIId(); - String key = uiId + "/" + paintableId + "/" + name; - - ConnectorTracker connectorTracker = ui.getConnectorTracker(); - connectorTracker.addStreamVariable(paintableId, name, value); - String seckey = connectorTracker.getSeckey(value); - - return ApplicationConstants.APP_PROTOCOL_PREFIX - + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey; - - } - - public void cleanStreamVariable(ClientConnector owner, String name) { - owner.getUI().getConnectorTracker() - .cleanStreamVariable(owner.getConnectorId(), name); - } - - /** - * Gets the bootstrap handler that should be used for generating the pages - * bootstrapping applications for this communication manager. - * - * @return the bootstrap handler to use - */ - private BootstrapHandler getBootstrapHandler() { - if (bootstrapHandler == null) { - bootstrapHandler = createBootstrapHandler(); - } - - return bootstrapHandler; - } - - /** - * @return - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version - */ - @Deprecated - protected abstract BootstrapHandler createBootstrapHandler(); - - /** - * Handles a request by passing it to each registered {@link RequestHandler} - * in turn until one produces a response. This method is used for requests - * that have not been handled by any specific functionality in the terminal - * implementation (e.g. {@link VaadinServlet}). - * <p> - * The request handlers are invoked in the revere order in which they were - * added to the session until a response has been produced. This means that - * the most recently added handler is used first and the first request - * handler that was added to the session is invoked towards the end unless - * any previous handler has already produced a response. - * </p> - * - * @param request - * the Vaadin request to get information from - * @param response - * the response to which data can be written - * @return returns <code>true</code> if a {@link RequestHandler} has - * produced a response and <code>false</code> if no response has - * been written. - * @throws IOException - * if a handler throws an exception - * - * @see VaadinSession#addRequestHandler(RequestHandler) - * @see RequestHandler - * - * @since 7.0 - */ - protected boolean handleOtherRequest(VaadinRequest request, - VaadinResponse response) throws IOException { - // Use a copy to avoid ConcurrentModificationException - for (RequestHandler handler : new ArrayList<RequestHandler>( - session.getRequestHandlers())) { - if (handler.handleRequest(session, request, response)) { - return true; - } - } - // If not handled - return false; - } - - public void handleBrowserDetailsRequest(VaadinRequest request, - VaadinResponse response, VaadinSession session) throws IOException { - - session.lock(); - - try { - assert UI.getCurrent() == null; - - response.setContentType("application/json; charset=UTF-8"); - - UI uI = getBrowserDetailsUI(request, session); - - JSONObject params = new JSONObject(); - params.put(UIConstants.UI_ID_PARAMETER, uI.getUIId()); - String initialUIDL = getInitialUIDL(request, uI); - params.put("uidl", initialUIDL); - - // NOTE! GateIn requires, for some weird reason, getOutputStream - // to be used instead of getWriter() (it seems to interpret - // application/json as a binary content type) - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - - outWriter.write(params.toString()); - // NOTE GateIn requires the buffers to be flushed to work - outWriter.flush(); - out.flush(); - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - session.unlock(); - } - } - - private UI getBrowserDetailsUI(VaadinRequest request, VaadinSession session) { - VaadinService vaadinService = request.getService(); - - List<UIProvider> uiProviders = session.getUIProviders(); - - UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent( - request); - - UIProvider provider = null; - Class<? extends UI> uiClass = null; - for (UIProvider p : uiProviders) { - // Check for existing LegacyWindow - if (p instanceof LegacyApplicationUIProvider) { - LegacyApplicationUIProvider legacyProvider = (LegacyApplicationUIProvider) p; - - UI existingUi = legacyProvider - .getExistingUI(classSelectionEvent); - if (existingUi != null) { - reinitUI(existingUi, request); - return existingUi; - } - } - - uiClass = p.getUIClass(classSelectionEvent); - if (uiClass != null) { - provider = p; - break; - } - } - - if (provider == null || uiClass == null) { - 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 - - Integer retainedUIId = retainOnRefreshUIs.get(windowName); - - if (retainedUIId != null) { - UI retainedUI = session.getUIById(retainedUIId.intValue()); - if (uiClass.isInstance(retainedUI)) { - reinitUI(retainedUI, request); - return retainedUI; - } else { - getLogger().log( - Level.INFO, - "Not using retained UI in {0} because retained UI was of type {1}" - + " but {2} is expected for the request.", - new Object[] { windowName, retainedUI.getClass(), - uiClass }); - } - } - } - - // No existing UI found - go on by creating and initializing one - - Integer uiId = Integer.valueOf(session.getNextUIid()); - - // Explicit Class.cast to detect if the UIProvider does something - // unexpected - UICreateEvent event = new UICreateEvent(request, uiClass, uiId); - UI ui = uiClass.cast(provider.createInstance(event)); - - // Initialize some fields for a newly created UI - if (ui.getSession() != session) { - // Session already set for LegacyWindow - ui.setSession(session); - } - - // Set thread local here so it is available in init - UI.setCurrent(ui); - - ui.doInit(request, uiId.intValue()); - - session.addUI(ui); - - // Remember if it should be remembered - if (vaadinService.preserveUIOnRefresh(provider, event)) { - // Remember this UI - if (windowName == null) { - getLogger() - .log(Level.WARNING, - "There is no window.name available for UI {0} that should be preserved.", - uiClass); - } else { - session.getPreserveOnRefreshUIs().put(windowName, uiId); - } - } - - return ui; - } - - /** - * Updates a UI that has already been initialized but is now loaded again, - * e.g. because of {@link PreserveOnRefresh}. - * - * @param ui - * @param request - */ - 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); - } - } - - /** - * Generates the initial UIDL message that can e.g. be included in a html - * page to avoid a separate round trip just for getting the UIDL. - * - * @param request - * the request that caused the initialization - * @param uI - * the UI for which the UIDL should be generated - * @return a string with the initial UIDL message - * @throws PaintException - * if an exception occurs while painting - * @throws JSONException - * if an exception occurs while encoding output - */ - protected String getInitialUIDL(VaadinRequest request, UI uI) - throws PaintException, JSONException { - // TODO maybe unify writeUidlResponse()? - StringWriter sWriter = new StringWriter(); - PrintWriter pWriter = new PrintWriter(sWriter); - pWriter.print("{"); - if (isXSRFEnabled(uI.getSession())) { - pWriter.print(getSecurityKeyUIDL(request)); - } - writeUidlResponse(request, true, pWriter, uI, false); - pWriter.print("}"); - String initialUIDL = sWriter.toString(); - getLogger().log(Level.FINE, "Initial UIDL:{0}", initialUIDL); - return initialUIDL; - } - - /** - * Serve a connector resource from the classpath if the resource has - * previously been registered by calling - * {@link #registerPublishedFile(String, Class)}. Sending arbitrary files - * from the classpath is prevented by only accepting resource names that - * have explicitly been registered. Resources can currently only be - * registered by including a {@link JavaScript} or {@link StyleSheet} - * annotation on a Connector class. - * - * @param request - * @param response - * - * @throws IOException - */ - public void servePublishedFile(VaadinRequest request, - VaadinResponse response) throws IOException { - - String pathInfo = request.getPathInfo(); - // + 2 to also remove beginning and ending slashes - String fileName = pathInfo - .substring(ApplicationConstants.PUBLISHED_FILE_PATH.length() + 2); - - final String mimetype = response.getService().getMimeType(fileName); - - // Security check: avoid accidentally serving from the UI of the - // classpath instead of relative to the context class - if (fileName.startsWith("/")) { - getLogger().log(Level.WARNING, - "Published file request starting with / rejected: {0}", - fileName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - return; - } - - // Check that the resource name has been registered - Class<?> context; - synchronized (publishedFileContexts) { - context = publishedFileContexts.get(fileName); - } - - // Security check: don't serve resource if the name hasn't been - // registered in the map - if (context == null) { - getLogger() - .log(Level.WARNING, - "Rejecting published file request for file that has not been published: {0}", - fileName); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - return; - } - - // Resolve file relative to the location of the context class - InputStream in = context.getResourceAsStream(fileName); - if (in == null) { - getLogger() - .log(Level.WARNING, - "{0} published by {1} not found. Verify that the file {2}/{3} is available on the classpath.", - new Object[] { - fileName, - context.getName(), - context.getPackage().getName() - .replace('.', '/'), fileName }); - response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); - return; - } - - // TODO Check and set cache headers - - OutputStream out = null; - try { - if (mimetype != null) { - response.setContentType(mimetype); - } - - out = response.getOutputStream(); - - final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE]; - - int bytesRead = 0; - while ((bytesRead = in.read(buffer)) > 0) { - out.write(buffer, 0, bytesRead); - } - out.flush(); - } finally { - try { - in.close(); - } catch (Exception e) { - // Do nothing - } - if (out != null) { - try { - out.close(); - } catch (Exception e) { - // Do nothing - } - } - } - } - - /** - * Handles file upload request submitted via Upload component. - * - * @param UI - * The UI for this request - * - * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable) - * - * @param request - * @param response - * @throws IOException - * @throws InvalidUIDLSecurityKeyException - */ - public void handleFileUpload(VaadinSession session, VaadinRequest request, - VaadinResponse response) throws IOException, - InvalidUIDLSecurityKeyException { - - /* - * URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See - * #createReceiverUrl - */ - - String pathInfo = request.getPathInfo(); - // strip away part until the data we are interested starts - int startOfData = pathInfo - .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX) - + ServletPortletHelper.UPLOAD_URL_PREFIX.length(); - String uppUri = pathInfo.substring(startOfData); - String[] parts = uppUri.split("/", 4); // 0= UIid, 1 = cid, 2= name, 3 - // = sec key - String uiId = parts[0]; - String connectorId = parts[1]; - String variableName = parts[2]; - UI uI = session.getUIById(Integer.parseInt(uiId)); - UI.setCurrent(uI); - - StreamVariable streamVariable = uI.getConnectorTracker() - .getStreamVariable(connectorId, variableName); - String secKey = uI.getConnectorTracker().getSeckey(streamVariable); - if (secKey.equals(parts[3])) { - - ClientConnector source = getConnector(uI, connectorId); - String contentType = request.getContentType(); - if (contentType.contains("boundary")) { - // Multipart requests contain boundary string - doHandleSimpleMultipartFileUpload(request, response, - streamVariable, variableName, source, - contentType.split("boundary=")[1]); - } else { - // if boundary string does not exist, the posted file is from - // XHR2.post(File) - doHandleXhrFilePost(request, response, streamVariable, - variableName, source, request.getContentLength()); - } - } else { - throw new InvalidUIDLSecurityKeyException( - "Security key in upload post did not match!"); - } - - } - - /** - * Handles a heartbeat request. Heartbeat requests are periodically sent by - * the client-side to inform the server that the UI sending the heartbeat is - * still alive (the browser window is open, the connection is up) even when - * there are no UIDL requests for a prolonged period of time. UIs that do - * not receive either heartbeat or UIDL requests are eventually removed from - * the session and garbage collected. - * - * @param request - * @param response - * @param session - * @throws IOException - */ - public void handleHeartbeatRequest(VaadinRequest request, - VaadinResponse response, VaadinSession session) throws IOException { - UI ui = null; - try { - int uiId = Integer.parseInt(request - .getParameter(UIConstants.UI_ID_PARAMETER)); - ui = session.getUIById(uiId); - } catch (NumberFormatException nfe) { - // null-check below handles this as well - } - if (ui != null) { - ui.setLastHeartbeatTimestamp(System.currentTimeMillis()); - // Ensure that the browser does not cache heartbeat responses. - // iOS 6 Safari requires this (#10370) - response.setHeader("Cache-Control", "no-cache"); - } else { - response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found"); - } - } - - /** - * Stream that extracts content from another stream until the boundary - * string is encountered. - * - * Public only for unit tests, should be considered private for all other - * purposes. - */ - public static class SimpleMultiPartInputStream extends InputStream { - - /** - * Counter of how many characters have been matched to boundary string - * from the stream - */ - int matchedCount = -1; - - /** - * Used as pointer when returning bytes after partly matched boundary - * string. - */ - int curBoundaryIndex = 0; - /** - * The byte found after a "promising start for boundary" - */ - private int bufferedByte = -1; - private boolean atTheEnd = false; - - private final char[] boundary; - - private final InputStream realInputStream; - - public SimpleMultiPartInputStream(InputStream realInputStream, - String boundaryString) { - boundary = (CRLF + DASHDASH + boundaryString).toCharArray(); - this.realInputStream = realInputStream; - } - - @Override - public int read() throws IOException { - if (atTheEnd) { - // End boundary reached, nothing more to read - return -1; - } else if (bufferedByte >= 0) { - /* Purge partially matched boundary if there was such */ - return getBuffered(); - } else if (matchedCount != -1) { - /* - * Special case where last "failed" matching ended with first - * character from boundary string - */ - return matchForBoundary(); - } else { - int fromActualStream = realInputStream.read(); - if (fromActualStream == -1) { - // unexpected end of stream - throw new IOException( - "The multipart stream ended unexpectedly"); - } - if (boundary[0] == fromActualStream) { - /* - * If matches the first character in boundary string, start - * checking if the boundary is fetched. - */ - return matchForBoundary(); - } - return fromActualStream; - } - } - - /** - * Reads the input to expect a boundary string. Expects that the first - * character has already been matched. - * - * @return -1 if the boundary was matched, else returns the first byte - * from boundary - * @throws IOException - */ - private int matchForBoundary() throws IOException { - matchedCount = 0; - /* - * Going to "buffered mode". Read until full boundary match or a - * different character. - */ - while (true) { - matchedCount++; - if (matchedCount == boundary.length) { - /* - * The whole boundary matched so we have reached the end of - * file - */ - atTheEnd = true; - return -1; - } - int fromActualStream = realInputStream.read(); - if (fromActualStream != boundary[matchedCount]) { - /* - * Did not find full boundary, cache the mismatching byte - * and start returning the partially matched boundary. - */ - bufferedByte = fromActualStream; - return getBuffered(); - } - } - } - - /** - * Returns the partly matched boundary string and the byte following - * that. - * - * @return - * @throws IOException - */ - private int getBuffered() throws IOException { - int b; - if (matchedCount == 0) { - // The boundary has been returned, return the buffered byte. - b = bufferedByte; - bufferedByte = -1; - matchedCount = -1; - } else { - b = boundary[curBoundaryIndex++]; - if (curBoundaryIndex == matchedCount) { - // The full boundary has been returned, remaining is the - // char that did not match the boundary. - - curBoundaryIndex = 0; - if (bufferedByte != boundary[0]) { - /* - * next call for getBuffered will return the - * bufferedByte that came after the partial boundary - * match - */ - matchedCount = 0; - } else { - /* - * Special case where buffered byte again matches the - * boundaryString. This could be the start of the real - * end boundary. - */ - matchedCount = 0; - bufferedByte = -1; - } - } - } - if (b == -1) { - throw new IOException("The multipart stream ended unexpectedly"); - } - return b; - } - } - - private static final Logger getLogger() { - return Logger.getLogger(AbstractCommunicationManager.class.getName()); - } -} diff --git a/server/src/com/vaadin/server/BootstrapHandler.java b/server/src/com/vaadin/server/BootstrapHandler.java index 403fefc0e1..dddfb385a6 100644 --- a/server/src/com/vaadin/server/BootstrapHandler.java +++ b/server/src/com/vaadin/server/BootstrapHandler.java @@ -41,6 +41,7 @@ import org.jsoup.parser.Tag; import com.vaadin.shared.ApplicationConstants; import com.vaadin.shared.Version; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; /** @@ -51,7 +52,14 @@ import com.vaadin.ui.UI; * @deprecated As of 7.0. Will likely change or be removed in a future version */ @Deprecated -public abstract class BootstrapHandler implements RequestHandler { +public abstract class BootstrapHandler extends SynchronizedRequestHandler { + + /** + * Parameter that is added to the UI init request if the session has already + * been restarted when generating the bootstrap HTML and ?restartApplication + * should thus be ignored when handling the UI init request. + */ + public static final String IGNORE_RESTART_PARAM = "ignoreRestart"; protected class BootstrapContext implements Serializable { @@ -61,6 +69,7 @@ public abstract class BootstrapHandler implements RequestHandler { private String widgetsetName; private String themeName; private String appId; + private PushMode pushMode; public BootstrapContext(VaadinResponse response, BootstrapFragmentResponse bootstrapResponse) { @@ -98,6 +107,30 @@ public abstract class BootstrapHandler implements RequestHandler { return themeName; } + public PushMode getPushMode() { + if (pushMode == null) { + UICreateEvent event = new UICreateEvent(getRequest(), + getUIClass()); + + pushMode = getBootstrapResponse().getUIProvider().getPushMode( + event); + if (pushMode == null) { + pushMode = getRequest().getService() + .getDeploymentConfiguration().getPushMode(); + } + + if (pushMode.isEnabled() + && !getRequest().getService().ensurePushAvailable()) { + /* + * Fall back if not supported (ensurePushAvailable will log + * information to the developer the first time this happens) + */ + pushMode = PushMode.DISABLED; + } + } + return pushMode; + } + public String getAppId() { if (appId == null) { appId = getRequest().getService().getMainDivId(getSession(), @@ -113,10 +146,19 @@ public abstract class BootstrapHandler implements RequestHandler { } @Override - public boolean handleRequest(VaadinSession session, VaadinRequest request, - VaadinResponse response) throws IOException { + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) throws IOException { + if (ServletPortletHelper.isAppRequest(request)) { + // We do not want to handle /APP requests here, instead let it fall + // through and produce a 404 + return false; + } try { + // Update WebBrowser here only to make WebBrowser information + // available in init for LegacyApplications + session.getBrowser().updateRequestDetails(request); + List<UIProvider> uiProviders = session.getUIProviders(); UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent( @@ -241,11 +283,9 @@ public abstract class BootstrapHandler implements RequestHandler { /* * Enable Chrome Frame in all versions of IE if installed. - * - * Claim IE10 support to avoid using compatibility mode. */ head.appendElement("meta").attr("http-equiv", "X-UA-Compatible") - .attr("content", "IE=9;chrome=1"); + .attr("content", "IE=10;chrome=1"); String title = response.getUIProvider().getPageTitle( new UICreateEvent(context.getRequest(), context.getUIClass())); @@ -334,8 +374,8 @@ public abstract class BootstrapHandler implements RequestHandler { VaadinRequest request = context.getRequest(); VaadinService vaadinService = request.getService(); - String staticFileLocation = vaadinService - .getStaticFileLocation(request); + String vaadinLocation = vaadinService.getStaticFileLocation(request) + + "/VAADIN/"; fragmentNodes .add(new Element(Tag.valueOf("iframe"), "") @@ -345,8 +385,14 @@ public abstract class BootstrapHandler implements RequestHandler { "position:absolute;width:0;height:0;border:0;overflow:hidden") .attr("src", "javascript:false")); - String bootstrapLocation = staticFileLocation - + "/VAADIN/vaadinBootstrap.js"; + if (context.getPushMode().isEnabled()) { + // Load client-side dependencies for push support + fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr( + "type", "text/javascript").attr("src", + vaadinLocation + ApplicationConstants.VAADIN_PUSH_JS)); + } + + String bootstrapLocation = vaadinLocation + "vaadinBootstrap.js"; fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type", "text/javascript").attr("src", bootstrapLocation)); Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr( @@ -415,6 +461,12 @@ public abstract class BootstrapHandler implements RequestHandler { appConfig.put("theme", themeName); } + // Ignore restartApplication that might be passed to UI init + if (request + .getParameter(VaadinService.URL_PARAMETER_RESTART_APPLICATION) != null) { + appConfig.put("extraParams", "&" + IGNORE_RESTART_PARAM + "=1"); + } + JSONObject versionInfo = new JSONObject(); versionInfo.put("vaadinVersion", Version.getFullVersion()); appConfig.put("versionInfo", versionInfo); diff --git a/server/src/com/vaadin/server/BrowserWindowOpener.java b/server/src/com/vaadin/server/BrowserWindowOpener.java index 8e049ca454..a6e420f89c 100644 --- a/server/src/com/vaadin/server/BrowserWindowOpener.java +++ b/server/src/com/vaadin/server/BrowserWindowOpener.java @@ -38,7 +38,8 @@ public class BrowserWindowOpener extends AbstractExtension { private final String path; private final Class<? extends UI> uiClass; - public BrowserWindowOpenerUIProvider(Class<? extends UI> uiClass, String path) { + public BrowserWindowOpenerUIProvider(Class<? extends UI> uiClass, + String path) { this.path = ensureInitialSlash(path); this.uiClass = uiClass; } diff --git a/server/src/com/vaadin/server/ClientConnector.java b/server/src/com/vaadin/server/ClientConnector.java index 5e95b18281..3b52fbc730 100644 --- a/server/src/com/vaadin/server/ClientConnector.java +++ b/server/src/com/vaadin/server/ClientConnector.java @@ -300,7 +300,7 @@ public interface ClientConnector extends Connector { /** * Called by the framework to encode the state to a JSONObject. This is * typically done by calling the static method - * {@link AbstractCommunicationManager#encodeState(ClientConnector, SharedState)} + * {@link LegacyCommunicationManager#encodeState(ClientConnector, SharedState)} * . * * @return a JSON object with the encoded connector state @@ -318,8 +318,12 @@ public interface ClientConnector extends Connector { * routed to this method with the remaining part of the requested path * available in the path parameter. * <p> - * {@link DynamicConnectorResource} can be used to easily make an - * appropriate URL available to the client-side code. + * NOTE that the session is not locked when this method is called. It is the + * responsibility of the connector to ensure that the session is locked + * while handling state or other session related data. For best performance + * the session should be unlocked before writing a large response to the + * client. + * </p> * * @param request * the request that should be handled diff --git a/server/src/com/vaadin/server/CommunicationManager.java b/server/src/com/vaadin/server/CommunicationManager.java deleted file mode 100644 index 8b3550481d..0000000000 --- a/server/src/com/vaadin/server/CommunicationManager.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.server; - -import java.io.InputStream; - -import javax.servlet.ServletContext; - -import com.vaadin.ui.UI; - -/** - * Application manager processes changes and paints for single application - * instance. - * - * This class handles applications running as servlets. - * - * @see AbstractCommunicationManager - * - * @author Vaadin Ltd. - * @since 5.0 - * - * @deprecated As of 7.0. Will likely change or be removed in a future version - */ -@Deprecated -@SuppressWarnings("serial") -public class CommunicationManager extends AbstractCommunicationManager { - - /** - * TODO New constructor - document me! - * - * @param session - */ - public CommunicationManager(VaadinSession session) { - super(session); - } - - @Override - protected BootstrapHandler createBootstrapHandler() { - return new BootstrapHandler() { - @Override - protected String getServiceUrl(BootstrapContext context) { - String pathInfo = context.getRequest().getPathInfo(); - if (pathInfo == null) { - return null; - } else { - /* - * Make a relative URL to the servlet by adding one ../ for - * each path segment in pathInfo (i.e. the part of the - * requested path that comes after the servlet mapping) - */ - return VaadinServletService - .getCancelingRelativePath(pathInfo); - } - } - - @Override - public String getThemeName(BootstrapContext context) { - String themeName = context.getRequest().getParameter( - VaadinServlet.URL_PARAMETER_THEME); - if (themeName == null) { - themeName = super.getThemeName(context); - } - return themeName; - } - }; - } - - @Override - protected InputStream getThemeResourceAsStream(UI uI, String themeName, - String resource) { - VaadinServletService service = (VaadinServletService) uI.getSession() - .getService(); - ServletContext servletContext = service.getServlet() - .getServletContext(); - return servletContext.getResourceAsStream("/" - + VaadinServlet.THEME_DIR_PATH + '/' + themeName + "/" - + resource); - } -} diff --git a/server/src/com/vaadin/server/ComponentSizeValidator.java b/server/src/com/vaadin/server/ComponentSizeValidator.java index f5e2e2fe12..27d087a2b2 100644 --- a/server/src/com/vaadin/server/ComponentSizeValidator.java +++ b/server/src/com/vaadin/server/ComponentSizeValidator.java @@ -191,7 +191,6 @@ public class ComponentSizeValidator implements Serializable { } public void reportErrors(PrintWriter clientJSON, - AbstractCommunicationManager communicationManager, PrintStream serverErrorStream) { clientJSON.write("{"); @@ -269,8 +268,7 @@ public class ComponentSizeValidator implements Serializable { } else { first = false; } - subError.reportErrors(clientJSON, communicationManager, - serverErrorStream); + subError.reportErrors(clientJSON, serverErrorStream); } clientJSON.write("]"); serverErrorStream.println("<< Sub erros"); diff --git a/server/src/com/vaadin/server/ConnectorResource.java b/server/src/com/vaadin/server/ConnectorResource.java index 8f8591e6b1..8682f8ce6f 100644 --- a/server/src/com/vaadin/server/ConnectorResource.java +++ b/server/src/com/vaadin/server/ConnectorResource.java @@ -30,6 +30,15 @@ public interface ConnectorResource extends Resource { /** * Gets resource as stream. + * <p> + * Note that this method is called while the session is locked to prevent + * race conditions but the methods in the returned {@link DownloadStream} + * are assumed to be unrelated to the VaadinSession and are called without + * holding session locks (to prevent locking the session during long file + * downloads). + * </p> + * + * @return A download stream which produces the resource content */ public DownloadStream getStream(); diff --git a/server/src/com/vaadin/server/ConnectorResourceHandler.java b/server/src/com/vaadin/server/ConnectorResourceHandler.java index 03a2fcc115..00d82988d3 100644 --- a/server/src/com/vaadin/server/ConnectorResourceHandler.java +++ b/server/src/com/vaadin/server/ConnectorResourceHandler.java @@ -16,6 +16,7 @@ package com.vaadin.server; import java.io.IOException; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -25,6 +26,7 @@ import javax.servlet.http.HttpServletResponse; import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.UI; +import com.vaadin.util.CurrentInstance; public class ConnectorResourceHandler implements RequestHandler { // APP/connector/[uiid]/[cid]/[filename.xyz] @@ -46,28 +48,38 @@ public class ConnectorResourceHandler implements RequestHandler { return false; } Matcher matcher = CONNECTOR_RESOURCE_PATTERN.matcher(requestPath); - if (matcher.matches()) { - String uiId = matcher.group(1); - String cid = matcher.group(2); - String key = matcher.group(3); - UI ui = session.getUIById(Integer.parseInt(uiId)); + if (!matcher.matches()) { + return false; + } + String uiId = matcher.group(1); + String cid = matcher.group(2); + String key = matcher.group(3); + + session.lock(); + UI ui; + ClientConnector connector; + try { + ui = session.getUIById(Integer.parseInt(uiId)); if (ui == null) { return error(request, response, "Ignoring connector request for no-existent root " + uiId); } - UI.setCurrent(ui); - VaadinSession.setCurrent(ui.getSession()); - - ClientConnector connector = ui.getConnectorTracker().getConnector( - cid); + connector = ui.getConnectorTracker().getConnector(cid); if (connector == null) { return error(request, response, "Ignoring connector request for no-existent connector " + cid + " in root " + uiId); } + } finally { + session.unlock(); + } + + Map<Class<?>, CurrentInstance> oldThreadLocals = CurrentInstance + .setThreadLocals(ui); + try { if (!connector.handleConnectorRequest(request, response, key)) { return error(request, response, connector.getClass() .getSimpleName() @@ -75,20 +87,11 @@ public class ConnectorResourceHandler implements RequestHandler { + connector.getConnectorId() + ") did not handle connector request for " + key); } - - return true; - } else if (requestPath.matches('/' + ApplicationConstants.APP_PATH - + "(/.*)?")) { - /* - * This should be the last request handler before we get to - * bootstrap logic. Prevent /APP requests from reaching bootstrap - * handlers to help protect the /APP name space for framework usage. - */ - return error(request, response, - "Returning 404 for /APP request not yet handled."); - } else { - return false; + } finally { + CurrentInstance.restoreThreadLocals(oldThreadLocals); } + + return true; } private static boolean error(VaadinRequest request, diff --git a/server/src/com/vaadin/server/Constants.java b/server/src/com/vaadin/server/Constants.java index a9bc3e5b9e..f8d8105286 100644 --- a/server/src/com/vaadin/server/Constants.java +++ b/server/src/com/vaadin/server/Constants.java @@ -15,6 +15,8 @@ */ package com.vaadin.server; +import com.vaadin.shared.communication.PushMode; + /** * TODO Document me! * @@ -47,6 +49,13 @@ public interface Constants { + "in web.xml. The default of 5min will be used.\n" + "==========================================================="; + static final String WARNING_PUSH_MODE_NOT_RECOGNIZED = "\n" + + "===========================================================\n" + + "WARNING: pushMode has been set to an unrecognized value\n" + + "in web.xml. The permitted values are \"disabled\", \"manual\",\n" + + "and \"automatic\". The default of \"disabled\" will be used.\n" + + "==========================================================="; + static final String WIDGETSET_MISMATCH_INFO = "\n" + "=================================================================\n" + "The widgetset in use does not seem to be built for the Vaadin\n" @@ -56,6 +65,53 @@ public interface Constants { + " Widgetset version: %s\n" + "================================================================="; + static final String REQUIRED_ATMOSPHERE_VERSION = "1.0.12"; + + static final String INVALID_ATMOSPHERE_VERSION_WARNING = "\n" + + "=================================================================\n" + + "Vaadin depends on Atomsphere {0} but version {1} was found.\n" + + "This might cause compatibility problems if push is used.\n" + + "================================================================="; + + static final String ATMOSPHERE_MISSING_ERROR = "\n" + + "=================================================================\n" + + "Atmosphere could not be loaded. When using push with Vaadin, the\n" + + "Atmosphere framework must be present on the classpath.\n" + + "If using a dependency management system, please add a dependency\n" + + "to vaadin-push.\n" + + "If managing dependencies manually, please make sure Atmosphere\n" + + REQUIRED_ATMOSPHERE_VERSION + + " is included on the classpath.\n" + + "Will fall back to using " + + PushMode.class.getSimpleName() + + "." + + PushMode.DISABLED.name() + + ".\n" + + "================================================================="; + + static final String PUSH_NOT_SUPPORTED_ERROR = "\n" + + "=================================================================\n" + + "Push is not supported for {0}\n" + + "Will fall back to using " + + PushMode.class.getSimpleName() + + "." + + PushMode.DISABLED.name() + + ".\n" + + "================================================================="; + + public static final String WARNING_LEGACY_PROPERTY_TOSTRING = "You are using toString() instead of getValue() to get the value for a Property of type {0}" + + ". This is strongly discouraged and only provided for backwards compatibility with Vaadin 6. " + + "To disable this warning message and retain the behavior, set the init parameter \"" + + Constants.SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING + + "\" to \"true\". To disable the legacy functionality, set \"" + + Constants.SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING + + "\" to false." + + " (Note that your debugger might call toString() and trigger this message)."; + + static final String WARNING_UNKNOWN_LEGACY_PROPERTY_TOSTRING_VALUE = "Unknown value '{0}' for parameter " + + Constants.SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING + + ". Supported values are 'false','warning','true'"; + static final String URL_PARAMETER_THEME = "theme"; static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode"; @@ -63,7 +119,9 @@ public interface Constants { static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime"; static final String SERVLET_PARAMETER_HEARTBEAT_INTERVAL = "heartbeatInterval"; static final String SERVLET_PARAMETER_CLOSE_IDLE_SESSIONS = "closeIdleSessions"; + static final String SERVLET_PARAMETER_PUSH_MODE = "pushMode"; static final String SERVLET_PARAMETER_UI_PROVIDER = "UIProvider"; + static final String SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING = "legacyPropertyToString"; // Configurable parameter names static final String PARAMETER_VAADIN_RESOURCES = "Resources"; diff --git a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java index 5b0c3fe8d1..80c3644d77 100644 --- a/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DefaultDeploymentConfiguration.java @@ -17,8 +17,11 @@ package com.vaadin.server; import java.util.Properties; +import java.util.logging.Level; import java.util.logging.Logger; +import com.vaadin.shared.communication.PushMode; + /** * The default implementation of {@link DeploymentConfiguration} based on a base * class for resolving system properties and a set of init parameters. @@ -33,7 +36,9 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { private int resourceCacheTime; private int heartbeatInterval; private boolean closeIdleSessions; + private PushMode pushMode; private final Class<?> systemPropertyBaseClass; + private LegacyProperyToStringMode legacyPropertyToStringMode; /** * Create a new deployment configuration instance. @@ -55,6 +60,27 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { checkResourceCacheTime(); checkHeartbeatInterval(); checkCloseIdleSessions(); + checkPushMode(); + checkLegacyPropertyToString(); + } + + private void checkLegacyPropertyToString() { + String param = getApplicationOrSystemProperty( + Constants.SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING, "warning"); + if ("true".equals(param)) { + legacyPropertyToStringMode = LegacyProperyToStringMode.ENABLED; + } else if ("false".equals(param)) { + legacyPropertyToStringMode = LegacyProperyToStringMode.DISABLED; + } else { + if (!"warning".equals(param)) { + getLogger() + .log(Level.WARNING, + Constants.WARNING_UNKNOWN_LEGACY_PROPERTY_TOSTRING_VALUE, + param); + } + legacyPropertyToStringMode = LegacyProperyToStringMode.WARNING; + + } } @Override @@ -167,12 +193,32 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { return heartbeatInterval; } + /** + * {@inheritDoc} + * <p> + * The default value is false. + */ @Override public boolean isCloseIdleSessions() { return closeIdleSessions; } /** + * {@inheritDoc} + * <p> + * The default mode is {@link PushMode#DISABLED}. + */ + @Override + public PushMode getPushMode() { + return pushMode; + } + + @Override + public Properties getInitParameters() { + return initParameters; + } + + /** * Log a warning if Vaadin is not running in production mode. */ private void checkProductionMode() { @@ -231,13 +277,26 @@ public class DefaultDeploymentConfiguration implements DeploymentConfiguration { .equals("true"); } + private void checkPushMode() { + String mode = getApplicationOrSystemProperty( + Constants.SERVLET_PARAMETER_PUSH_MODE, + PushMode.DISABLED.toString()); + try { + pushMode = Enum.valueOf(PushMode.class, mode.toUpperCase()); + } catch (IllegalArgumentException e) { + getLogger().warning(Constants.WARNING_PUSH_MODE_NOT_RECOGNIZED); + pushMode = PushMode.DISABLED; + } + } + private Logger getLogger() { return Logger.getLogger(getClass().getName()); } @Override - public Properties getInitParameters() { - return initParameters; + @Deprecated + public LegacyProperyToStringMode getLegacyPropertyToStringMode() { + return legacyPropertyToStringMode; } } diff --git a/server/src/com/vaadin/server/DeploymentConfiguration.java b/server/src/com/vaadin/server/DeploymentConfiguration.java index bd4bc928f4..bf9c019b6d 100644 --- a/server/src/com/vaadin/server/DeploymentConfiguration.java +++ b/server/src/com/vaadin/server/DeploymentConfiguration.java @@ -19,6 +19,9 @@ package com.vaadin.server; import java.io.Serializable; import java.util.Properties; +import com.vaadin.data.util.AbstractProperty; +import com.vaadin.shared.communication.PushMode; + /** * A collection of properties configured at deploy time as well as a way of * accessing third party properties not explicitly supported by this class. @@ -28,6 +31,23 @@ import java.util.Properties; * @since 7.0.0 */ public interface DeploymentConfiguration extends Serializable { + + /** + * Determines the mode of the "legacyPropertyToString" parameter. + * + * @author Vaadin Ltd + * @since 7.1 + */ + @Deprecated + public enum LegacyProperyToStringMode { + DISABLED, WARNING, ENABLED; + + public boolean useLegacyMode() { + return this == WARNING || this == ENABLED; + } + + } + /** * Returns whether Vaadin is in production mode. * @@ -78,6 +98,14 @@ public interface DeploymentConfiguration extends Serializable { public boolean isCloseIdleSessions(); /** + * Returns the mode of bidirectional ("push") client-server communication + * that should be used. + * + * @return The push mode in use. + */ + public PushMode getPushMode(); + + /** * Gets the properties configured for the deployment, e.g. as init * parameters to the servlet or portlet. * @@ -101,4 +129,13 @@ public interface DeploymentConfiguration extends Serializable { public String getApplicationOrSystemProperty(String propertyName, String defaultValue); + /** + * Returns to legacy Property.toString() mode used. See + * {@link AbstractProperty#isLegacyToStringEnabled()} for more information. + * + * @return The Property.toString() mode in use. + */ + @Deprecated + public LegacyProperyToStringMode getLegacyPropertyToStringMode(); + } diff --git a/server/src/com/vaadin/server/DownloadStream.java b/server/src/com/vaadin/server/DownloadStream.java index e2f9fc5296..4e66831f1d 100644 --- a/server/src/com/vaadin/server/DownloadStream.java +++ b/server/src/com/vaadin/server/DownloadStream.java @@ -28,6 +28,11 @@ import javax.servlet.http.HttpServletResponse; /** * Downloadable stream. + * <p> + * Note that the methods in a DownloadStream are called without locking the + * session to prevent locking the session during long file downloads. If your + * DownloadStream uses anything from the session, you must handle the locking. + * </p> * * @author Vaadin Ltd. * @since 3.0 diff --git a/server/src/com/vaadin/server/DragAndDropService.java b/server/src/com/vaadin/server/DragAndDropService.java index 5a54b5ae3a..a83e83ef7f 100644 --- a/server/src/com/vaadin/server/DragAndDropService.java +++ b/server/src/com/vaadin/server/DragAndDropService.java @@ -16,7 +16,7 @@ package com.vaadin.server; import java.io.IOException; -import java.io.PrintWriter; +import java.io.Writer; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -50,13 +50,13 @@ public class DragAndDropService implements VariableOwner, ClientConnector { private DragAndDropEvent dragEvent; - private final AbstractCommunicationManager manager; + private final LegacyCommunicationManager manager; private AcceptCriterion acceptCriterion; private ErrorHandler errorHandler; - public DragAndDropService(AbstractCommunicationManager manager) { + public DragAndDropService(LegacyCommunicationManager manager) { this.manager = manager; } @@ -209,10 +209,10 @@ public class DragAndDropService implements VariableOwner, ClientConnector { return true; } - void printJSONResponse(PrintWriter outWriter) throws PaintException { + public void printJSONResponse(Writer outWriter) throws IOException { if (isDirty()) { - outWriter.print(", \"dd\":"); + outWriter.write(", \"dd\":"); JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager, outWriter, false); diff --git a/server/src/com/vaadin/server/FileDownloader.java b/server/src/com/vaadin/server/FileDownloader.java index 7cc1fd7cc8..9b49ad8edd 100644 --- a/server/src/com/vaadin/server/FileDownloader.java +++ b/server/src/com/vaadin/server/FileDownloader.java @@ -129,10 +129,15 @@ public class FileDownloader extends AbstractExtension { // Ignore if it isn't for us return false; } + getSession().lock(); + DownloadStream stream; - Resource resource = getFileDownloadResource(); - if (resource instanceof ConnectorResource) { - DownloadStream stream = ((ConnectorResource) resource).getStream(); + try { + Resource resource = getFileDownloadResource(); + if (!(resource instanceof ConnectorResource)) { + return false; + } + stream = ((ConnectorResource) resource).getStream(); if (stream.getParameter("Content-Disposition") == null) { // Content-Disposition: attachment generally forces download @@ -140,15 +145,15 @@ public class FileDownloader extends AbstractExtension { "attachment; filename=\"" + stream.getFileName() + "\""); } - // Content-Type to block eager browser plug-ins from hijacking the - // file + // Content-Type to block eager browser plug-ins from hijacking + // the file if (isOverrideContentType()) { stream.setContentType("application/octet-stream;charset=UTF-8"); } - stream.writeResponse(request, response); - return true; - } else { - return false; + } finally { + getSession().unlock(); } + stream.writeResponse(request, response); + return true; } } diff --git a/server/src/com/vaadin/server/GAEVaadinServlet.java b/server/src/com/vaadin/server/GAEVaadinServlet.java index 0d2063d446..b4a83603b0 100644 --- a/server/src/com/vaadin/server/GAEVaadinServlet.java +++ b/server/src/com/vaadin/server/GAEVaadinServlet.java @@ -184,16 +184,14 @@ public class GAEVaadinServlet extends VaadinServlet { return; } - RequestType requestType = getRequestType(request); - - if (requestType == RequestType.STATIC_FILE) { + if (isStaticResourceRequest(request)) { // no locking needed, let superclass handle super.service(request, response); cleanSession(request); return; } - if (requestType == RequestType.APP) { + if (ServletPortletHelper.isAppRequest(request)) { // no locking needed, let superclass handle getApplicationContext(request, MemcacheServiceFactory.getMemcacheService()); @@ -205,7 +203,11 @@ public class GAEVaadinServlet extends VaadinServlet { final HttpSession session = request.getSession(getService() .requestCanCreateSession(request)); if (session == null) { - handleServiceSessionExpired(request, response); + try { + getService().handleSessionExpired(request, response); + } catch (ServiceException e) { + throw new ServletException(e); + } cleanSession(request); return; } @@ -218,19 +220,21 @@ public class GAEVaadinServlet extends VaadinServlet { // try to get lock long started = new Date().getTime(); // non-UIDL requests will try indefinitely - while (requestType != RequestType.UIDL - || new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { - locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), - MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); - if (locked) { - break; - } - try { - Thread.sleep(RETRY_AFTER_MILLISECONDS); - } catch (InterruptedException e) { - getLogger().finer( - "Thread.sleep() interrupted while waiting for lock. Trying again. " - + e); + if (!ServletPortletHelper.isUIDLRequest(request)) { + while (new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { + locked = memcache.put(mutex, 1, + Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (locked) { + break; + } + try { + Thread.sleep(RETRY_AFTER_MILLISECONDS); + } catch (InterruptedException e) { + getLogger().finer( + "Thread.sleep() interrupted while waiting for lock. Trying again. " + + e); + } } } diff --git a/server/src/com/vaadin/server/GlobalResourceHandler.java b/server/src/com/vaadin/server/GlobalResourceHandler.java index 0fac14e20c..d411b286d0 100644 --- a/server/src/com/vaadin/server/GlobalResourceHandler.java +++ b/server/src/com/vaadin/server/GlobalResourceHandler.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpServletResponse; import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.LegacyComponent; import com.vaadin.ui.UI; +import com.vaadin.util.CurrentInstance; /** * A {@link RequestHandler} that takes care of {@link ConnectorResource}s that @@ -85,30 +86,38 @@ public class GlobalResourceHandler implements RequestHandler { return error(request, response, pathInfo + " is not a valid global resource path"); } + session.lock(); + Map<Class<?>, CurrentInstance> oldThreadLocals = null; + DownloadStream stream = null; + try { + UI ui = session.getUIById(Integer.parseInt(uiid)); + if (ui == null) { + return error(request, response, "No UI found for id " + uiid); + } + oldThreadLocals = CurrentInstance.setThreadLocals(ui); + ConnectorResource resource; + if (LEGACY_TYPE.equals(type)) { + resource = legacyResources.get(key); + } else { + return error(request, response, "Unknown global resource type " + + type + " in requested path " + pathInfo); + } - UI ui = session.getUIById(Integer.parseInt(uiid)); - if (ui == null) { - return error(request, response, "No UI found for id " + uiid); - } - UI.setCurrent(ui); - - ConnectorResource resource; - if (LEGACY_TYPE.equals(type)) { - resource = legacyResources.get(key); - } else { - return error(request, response, "Unknown global resource type " - + type + " in requested path " + pathInfo); - } - - if (resource == null) { - return error(request, response, "Global resource " + key - + " not found"); - } + if (resource == null) { + return error(request, response, "Global resource " + key + + " not found"); + } - DownloadStream stream = resource.getStream(); - if (stream == null) { - return error(request, response, "Resource " + resource - + " didn't produce any stream."); + stream = resource.getStream(); + if (stream == null) { + return error(request, response, "Resource " + resource + + " didn't produce any stream."); + } + } finally { + session.unlock(); + if (oldThreadLocals != null) { + CurrentInstance.restoreThreadLocals(oldThreadLocals); + } } stream.writeResponse(request, response); diff --git a/server/src/com/vaadin/server/JsonCodec.java b/server/src/com/vaadin/server/JsonCodec.java index 9a70efab28..d533ed99f3 100644 --- a/server/src/com/vaadin/server/JsonCodec.java +++ b/server/src/com/vaadin/server/JsonCodec.java @@ -667,7 +667,7 @@ public class JsonCodec implements Serializable { } else if (value instanceof Connector) { Connector connector = (Connector) value; if (value instanceof Component - && !(AbstractCommunicationManager + && !(LegacyCommunicationManager .isComponentVisibleToClient((Component) value))) { return encodeNull(); } @@ -871,7 +871,7 @@ public class JsonCodec implements Serializable { for (Entry<?, ?> entry : map.entrySet()) { ClientConnector key = (ClientConnector) entry.getKey(); - if (AbstractCommunicationManager.isConnectorVisibleToClient(key)) { + if (LegacyCommunicationManager.isConnectorVisibleToClient(key)) { EncodeResult encodedValue = encode(entry.getValue(), null, valueType, connectorTracker); jsonMap.put(key.getConnectorId(), diff --git a/server/src/com/vaadin/server/JsonPaintTarget.java b/server/src/com/vaadin/server/JsonPaintTarget.java index 11bfb33fe1..ca70391f64 100644 --- a/server/src/com/vaadin/server/JsonPaintTarget.java +++ b/server/src/com/vaadin/server/JsonPaintTarget.java @@ -18,6 +18,7 @@ package com.vaadin.server; import java.io.PrintWriter; import java.io.Serializable; +import java.io.Writer; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -60,7 +61,7 @@ public class JsonPaintTarget implements PaintTarget { private boolean closed = false; - private final AbstractCommunicationManager manager; + private final LegacyCommunicationManager manager; private int changes = 0; @@ -86,14 +87,13 @@ public class JsonPaintTarget implements PaintTarget { * @throws PaintException * if the paint operation failed. */ - public JsonPaintTarget(AbstractCommunicationManager manager, - PrintWriter outWriter, boolean cachingRequired) - throws PaintException { + public JsonPaintTarget(LegacyCommunicationManager manager, + Writer outWriter, boolean cachingRequired) throws PaintException { this.manager = manager; // Sets the target for UIDL writing - uidlBuffer = outWriter; + uidlBuffer = new PrintWriter(outWriter); // Initialize tag-writing mOpenTags = new Stack<String>(); @@ -1007,7 +1007,7 @@ public class JsonPaintTarget implements PaintTarget { return manager.getTagForType(clientConnectorClass); } - Collection<Class<? extends ClientConnector>> getUsedClientConnectors() { + public Collection<Class<? extends ClientConnector>> getUsedClientConnectors() { return usedClientConnectors; } diff --git a/server/src/com/vaadin/server/LegacyCommunicationManager.java b/server/src/com/vaadin/server/LegacyCommunicationManager.java new file mode 100644 index 0000000000..c0194db243 --- /dev/null +++ b/server/src/com/vaadin/server/LegacyCommunicationManager.java @@ -0,0 +1,498 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.server; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.ClientConnector.ConnectorErrorEvent; +import com.vaadin.server.communication.LocaleWriter; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.JavaScriptConnectorState; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.SelectiveRenderer; +import com.vaadin.ui.UI; + +/** + * This is a common base class for the server-side implementations of the + * communication system between the client code (compiled with GWT into + * JavaScript) and the server side components. Its client side counterpart is + * {@link com.vaadin.client.ApplicationConnection}. + * <p> + * TODO Document better! + * + * @deprecated As of 7.0. Will likely change or be removed in a future version + */ +@Deprecated +@SuppressWarnings("serial") +public class LegacyCommunicationManager implements Serializable { + + // TODO Refactor (#11410) + private final HashMap<Integer, ClientCache> uiToClientCache = new HashMap<Integer, ClientCache>(); + + /** + * The session this communication manager is used for + */ + private final VaadinSession session; + + // TODO Refactor to UI shared state (#11378) + private List<String> locales; + + // TODO Move to VaadinSession (#11409) + private DragAndDropService dragAndDropService; + + // TODO Refactor (#11412) + private String requestThemeName; + + // TODO Refactor (#11413) + private Map<String, Class<?>> publishedFileContexts = new HashMap<String, Class<?>>(); + + /** + * TODO New constructor - document me! + * + * @param session + */ + public LegacyCommunicationManager(VaadinSession session) { + this.session = session; + requireLocale(session.getLocale().toString()); + } + + protected VaadinSession getSession() { + return session; + } + + /** + * @deprecated As of 7.1. See #11411. + */ + @Deprecated + public static JSONObject encodeState(ClientConnector connector, + SharedState state) throws JSONException { + UI uI = connector.getUI(); + ConnectorTracker connectorTracker = uI.getConnectorTracker(); + Class<? extends SharedState> stateType = connector.getStateType(); + Object diffState = connectorTracker.getDiffState(connector); + boolean supportsDiffState = !JavaScriptConnectorState.class + .isAssignableFrom(stateType); + if (diffState == null && supportsDiffState) { + // Use an empty state object as reference for full + // repaints + + try { + SharedState referenceState = stateType.newInstance(); + EncodeResult encodeResult = JsonCodec.encode(referenceState, + null, stateType, uI.getConnectorTracker()); + diffState = encodeResult.getEncodedValue(); + } catch (Exception e) { + getLogger() + .log(Level.WARNING, + "Error creating reference object for state of type {0}", + stateType.getName()); + } + } + EncodeResult encodeResult = JsonCodec.encode(state, diffState, + stateType, uI.getConnectorTracker()); + if (supportsDiffState) { + connectorTracker.setDiffState(connector, + (JSONObject) encodeResult.getEncodedValue()); + } + return (JSONObject) encodeResult.getDiff(); + } + + /** + * Resolves a dependency URI, registering the URI with this + * {@code LegacyCommunicationManager} if needed and returns a fully + * qualified URI. + * + * @deprecated As of 7.1. See #11413. + */ + @Deprecated + public String registerDependency(String resourceUri, Class<?> context) { + try { + URI uri = new URI(resourceUri); + String protocol = uri.getScheme(); + + if (ApplicationConstants.PUBLISHED_PROTOCOL_NAME.equals(protocol)) { + // Strip initial slash + String resourceName = uri.getPath().substring(1); + return registerPublishedFile(resourceName, context); + } + + if (protocol != null || uri.getHost() != null) { + return resourceUri; + } + + // Bare path interpreted as published file + return registerPublishedFile(resourceUri, context); + } catch (URISyntaxException e) { + getLogger().log(Level.WARNING, + "Could not parse resource url " + resourceUri, e); + return resourceUri; + } + } + + /** + * @deprecated As of 7.1. See #11413. + */ + @Deprecated + public Map<String, Class<?>> getDependencies() { + return publishedFileContexts; + } + + private String registerPublishedFile(String name, Class<?> context) { + // Add to map of names accepted by servePublishedFile + if (publishedFileContexts.containsKey(name)) { + Class<?> oldContext = publishedFileContexts.get(name); + if (oldContext != context) { + getLogger() + .log(Level.WARNING, + "{0} published by both {1} and {2}. File from {2} will be used.", + new Object[] { name, context, oldContext }); + } + } else { + publishedFileContexts.put(name, context); + } + + return ApplicationConstants.PUBLISHED_PROTOCOL_PREFIX + "/" + name; + } + + /** + * @deprecated As of 7.1. See #11410. + */ + @Deprecated + public ClientCache getClientCache(UI uI) { + Integer uiId = Integer.valueOf(uI.getUIId()); + ClientCache cache = uiToClientCache.get(uiId); + if (cache == null) { + cache = new ClientCache(); + uiToClientCache.put(uiId, cache); + } + return cache; + } + + /** + * Checks if the connector is visible in context. For Components, + * {@link #isComponentVisibleToClient(Component)} is used. For other types + * of connectors, the contextual visibility of its first Component ancestor + * is used. If no Component ancestor is found, the connector is not visible. + * + * @deprecated As of 7.1. See #11411. + * + * @param connector + * The connector to check + * @return <code>true</code> if the connector is visible to the client, + * <code>false</code> otherwise + */ + @Deprecated + public static boolean isConnectorVisibleToClient(ClientConnector connector) { + if (connector instanceof Component) { + return isComponentVisibleToClient((Component) connector); + } else { + ClientConnector parent = connector.getParent(); + if (parent == null) { + return false; + } else { + return isConnectorVisibleToClient(parent); + } + } + } + + /** + * Checks if the component should be visible to the client. Returns false if + * the child should not be sent to the client, true otherwise. + * + * @deprecated As of 7.1. See #11411. + * + * @param child + * The child to check + * @return true if the child is visible to the client, false otherwise + */ + @Deprecated + public static boolean isComponentVisibleToClient(Component child) { + if (!child.isVisible()) { + return false; + } + HasComponents parent = child.getParent(); + + if (parent instanceof SelectiveRenderer) { + if (!((SelectiveRenderer) parent).isRendered(child)) { + return false; + } + } + + if (parent != null) { + return isComponentVisibleToClient(parent); + } else { + if (child instanceof UI) { + // UI has no parent and visibility was checked above + return true; + } else { + // Component which is not attached to any UI + return false; + } + } + } + + /** + * @deprecated As of 7.1. See #11412. + */ + @Deprecated + public String getTheme(UI uI) { + String themeName = uI.getTheme(); + String requestThemeName = getRequestTheme(); + + if (requestThemeName != null) { + themeName = requestThemeName; + } + if (themeName == null) { + themeName = VaadinServlet.getDefaultTheme(); + } + return themeName; + } + + private String getRequestTheme() { + return requestThemeName; + } + + /** + * @deprecated As of 7.1. See #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; + } + + /** + * Prints the queued (pending) locale definitions to a {@link PrintWriter} + * in a (UIDL) format that can be sent to the client and used there in + * formatting dates, times etc. + * + * @deprecated As of 7.1. See #11378. + * + * @param outWriter + */ + @Deprecated + public void printLocaleDeclarations(Writer writer) throws IOException { + new LocaleWriter().write(locales, writer); + } + + /** + * Queues a locale to be sent to the client (browser) for date and time + * entry etc. All locale specific information is derived from server-side + * {@link Locale} instances and sent to the client when needed, eliminating + * the need to use the {@link Locale} class and all the framework behind it + * on the client. + * + * @deprecated As of 7.1. See #11378. + * + * @see Locale#toString() + * + * @param value + */ + @Deprecated + public void requireLocale(String value) { + if (locales == null) { + locales = new ArrayList<String>(); + locales.add(session.getLocale().toString()); + } + if (!locales.contains(value)) { + locales.add(value); + } + } + + /** + * @deprecated As of 7.1. See #11378. + */ + @Deprecated + public void resetLocales() { + locales = null; + } + + /** + * @deprecated As of 7.1. Will be removed in the future. + */ + @Deprecated + public static class InvalidUIDLSecurityKeyException extends + GeneralSecurityException { + + public InvalidUIDLSecurityKeyException(String message) { + super(message); + } + } + + 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. + */ + @Deprecated + public String getTagForType(Class<? extends ClientConnector> class1) { + Integer id = typeToKey.get(class1); + if (id == null) { + id = nextTypeKey++; + typeToKey.put(class1, id); + if (getLogger().isLoggable(Level.FINE)) { + getLogger().log(Level.FINE, "Mapping {0} to {1}", + new Object[] { class1.getName(), id }); + } + } + return id.toString(); + } + + /** + * Helper class for terminal to keep track of data that client is expected + * to know. + * + * TODO make customlayout templates (from theme) to be cached here. + * + * @deprecated As of 7.1. See #11410. + */ + @Deprecated + public class ClientCache implements Serializable { + + private final Set<Object> res = new HashSet<Object>(); + + /** + * + * @param paintable + * @return true if the given class was added to cache + */ + public boolean cache(Object object) { + return res.add(object); + } + + public void clear() { + res.clear(); + } + + } + + /** + * @deprecated As of 7.1. See #11411. + */ + @Deprecated + public String getStreamVariableTargetUrl(ClientConnector owner, + String name, StreamVariable value) { + /* + * We will use the same APP/* URI space as ApplicationResources but + * prefix url with UPLOAD + * + * eg. APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] + * + * SECKEY is created on each paint to make URL's unpredictable (to + * prevent CSRF attacks). + * + * NAME and PID from URI forms a key to fetch StreamVariable when + * handling post + */ + String paintableId = owner.getConnectorId(); + UI ui = owner.getUI(); + int uiId = ui.getUIId(); + String key = uiId + "/" + paintableId + "/" + name; + + ConnectorTracker connectorTracker = ui.getConnectorTracker(); + connectorTracker.addStreamVariable(paintableId, name, value); + String seckey = connectorTracker.getSeckey(value); + + return ApplicationConstants.APP_PROTOCOL_PREFIX + + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey; + + } + + /** + * Handles an exception that occurred when processing RPC calls or a file + * upload. + * + * @deprecated As of 7.1. See #11411. + * + * @param ui + * The UI where the exception occured + * @param throwable + * The exception + * @param connector + * The Rpc target + */ + @Deprecated + public void handleConnectorRelatedException(ClientConnector connector, + Throwable throwable) { + ErrorEvent errorEvent = new ConnectorErrorEvent(connector, throwable); + ErrorHandler handler = ErrorEvent.findErrorHandler(connector); + handler.error(errorEvent); + } + + /** + * Requests that the given UI should be fully re-rendered on the client + * side. + * + * @since 7.1 + * @deprecated. As of 7.1. Should be refactored once locales are fixed + * (#11378) + */ + @Deprecated + public void repaintAll(UI ui) { + getClientCache(ui).clear(); + ui.getConnectorTracker().markAllConnectorsDirty(); + ui.getConnectorTracker().markAllClientSidesUninitialized(); + + // Reset sent locales + resetLocales(); + requireLocale(session.getLocale().toString()); + } + + private static final Logger getLogger() { + return Logger.getLogger(LegacyCommunicationManager.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/LegacyPaint.java b/server/src/com/vaadin/server/LegacyPaint.java index 09477aaf3e..8d59dfd5ea 100644 --- a/server/src/com/vaadin/server/LegacyPaint.java +++ b/server/src/com/vaadin/server/LegacyPaint.java @@ -50,7 +50,7 @@ public class LegacyPaint implements Serializable { public static void paint(Component component, PaintTarget target) throws PaintException { // Only paint content of visible components. - if (!AbstractCommunicationManager.isComponentVisibleToClient(component)) { + if (!LegacyCommunicationManager.isComponentVisibleToClient(component)) { return; } diff --git a/server/src/com/vaadin/server/Page.java b/server/src/com/vaadin/server/Page.java index 8737b478c3..d4c16fe7f7 100644 --- a/server/src/com/vaadin/server/Page.java +++ b/server/src/com/vaadin/server/Page.java @@ -21,14 +21,18 @@ import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.util.EventObject; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import com.vaadin.event.EventRouter; import com.vaadin.shared.ui.BorderStyle; 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.ui.JavaScript; import com.vaadin.ui.LegacyWindow; import com.vaadin.ui.Link; @@ -218,7 +222,7 @@ public class Page implements Serializable { } } - private static final Method BROWSWER_RESIZE_METHOD = ReflectTools + private static final Method BROWSER_RESIZE_METHOD = ReflectTools .findMethod(BrowserWindowResizeListener.class, "browserWindowResized", BrowserWindowResizeEvent.class); @@ -303,6 +307,102 @@ public class Page implements Serializable { } } + /** + * Contains dynamically injected styles injected in the HTML document at + * runtime. + * + * @since 7.1 + */ + public static class Styles implements Serializable { + + private final Map<Integer, String> stringInjections = new HashMap<Integer, String>(); + + private final Map<Integer, Resource> resourceInjections = new HashMap<Integer, Resource>(); + + // The combined injection counter between both string and resource + // injections. Used as the key for the injection maps + private int injectionCounter = 0; + + // Points to the next injection that has not yet been made into the Page + private int nextInjectionPosition = 0; + + private final UI ui; + + private Styles(UI ui) { + this.ui = ui; + } + + /** + * Injects a raw CSS string into the page. + * + * @param css + * The CSS to inject + */ + public void add(String css) { + if (css == null) { + throw new IllegalArgumentException( + "Cannot inject null CSS string"); + } + + stringInjections.put(injectionCounter++, css); + ui.markAsDirty(); + } + + /** + * Injects a CSS resource into the page + * + * @param resource + * The resource to inject. + */ + public void add(Resource resource) { + if (resource == null) { + throw new IllegalArgumentException( + "Cannot inject null resource"); + } + + resourceInjections.put(injectionCounter++, resource); + ui.markAsDirty(); + } + + private void paint(PaintTarget target) throws PaintException { + + // If full repaint repaint all injections + if (target.isFullRepaint()) { + nextInjectionPosition = 0; + } + + if (injectionCounter > nextInjectionPosition) { + + target.startTag("css-injections"); + + while (injectionCounter > nextInjectionPosition) { + + String stringInjection = stringInjections + .get(nextInjectionPosition); + if (stringInjection != null) { + target.startTag("css-string"); + target.addAttribute("id", nextInjectionPosition); + target.addText(stringInjection); + target.endTag("css-string"); + } + + Resource resourceInjection = resourceInjections + .get(nextInjectionPosition); + if (resourceInjection != null) { + target.startTag("css-resource"); + target.addAttribute("id", nextInjectionPosition); + target.addAttribute("url", resourceInjection); + target.endTag("css-resource"); + } + + nextInjectionPosition++; + } + + target.endTag("css-injections"); + } + } + } + private EventRouter eventRouter; private final UI uI; @@ -312,13 +412,18 @@ public class Page implements Serializable { private JavaScript javaScript; + private Styles styles; + /** * The current browser location. */ private URI location; - public Page(UI uI) { + private final PageState state; + + public Page(UI uI, PageState state) { this.uI = uI; + this.state = state; } private void addListener(Class<?> eventType, Object target, Method method) { @@ -504,20 +609,27 @@ public class Page implements Serializable { } /** - * Adds a new {@link BrowserWindowResizeListener} to this uI. The listener - * will be notified whenever the browser window within which this uI resides + * Adds a new {@link BrowserWindowResizeListener} to this UI. The listener + * will be notified whenever the browser window within which this UI resides * is resized. + * <p> + * In most cases, the UI should be in lazy resize mode when using browser + * window resize listeners. Otherwise, a large number of events can be + * received while a resize is being performed. Use + * {@link UI#setResizeLazy(boolean)}. + * </p> * * @param resizeListener * the listener to add * * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent) - * @see #setResizeLazy(boolean) + * @see UI#setResizeLazy(boolean) */ public void addBrowserWindowResizeListener( BrowserWindowResizeListener resizeListener) { addListener(BrowserWindowResizeEvent.class, resizeListener, - BROWSWER_RESIZE_METHOD); + BROWSER_RESIZE_METHOD); + getState(true).hasResizeListeners = true; } /** @@ -539,7 +651,9 @@ public class Page implements Serializable { public void removeBrowserWindowResizeListener( BrowserWindowResizeListener resizeListener) { removeListener(BrowserWindowResizeEvent.class, resizeListener, - BROWSWER_RESIZE_METHOD); + BROWSER_RESIZE_METHOD); + getState(true).hasResizeListeners = eventRouter + .hasListeners(BrowserWindowResizeEvent.class); } /** @@ -576,10 +690,23 @@ public class Page implements Serializable { javaScript = new JavaScript(); javaScript.extend(uI); } - return javaScript; } + /** + * Returns that stylesheet associated with this Page. The stylesheet + * contains additional styles injected at runtime into the HTML document. + * + * @since 7.1 + */ + public Styles getStyles() { + + if (styles == null) { + styles = new Styles(uI); + } + return styles; + } + public void paintContent(PaintTarget target) throws PaintException { if (!openList.isEmpty()) { for (final Iterator<OpenResource> i = openList.iterator(); i @@ -637,6 +764,9 @@ public class Page implements Serializable { location.toString()); } + if (styles != null) { + styles.paint(target); + } } /** @@ -915,4 +1045,36 @@ public class Page implements Serializable { uI.getRpcProxy(PageClientRpc.class).setTitle(title); } + /** + * Reloads the page in the browser. + */ + public void reload() { + uI.getRpcProxy(PageClientRpc.class).reload(); + } + + /** + * Returns the page state. + * <p> + * The page state is transmitted to UIConnector together with + * {@link UIState} rather than as an individual entity. + * </p> + * <p> + * The state should be considered an internal detail of Page. Classes + * outside of Page should not access it directly but only through public + * APIs provided by Page. + * </p> + * + * @since 7.1 + * @param markAsDirty + * true to mark the state as dirty + * @return PageState object that can be read in any case and modified if + * markAsDirty is true + */ + protected PageState getState(boolean markAsDirty) { + if (markAsDirty) { + uI.markAsDirty(); + } + return state; + } + } diff --git a/server/src/com/vaadin/server/PortletCommunicationManager.java b/server/src/com/vaadin/server/PortletCommunicationManager.java deleted file mode 100644 index cece75847c..0000000000 --- a/server/src/com/vaadin/server/PortletCommunicationManager.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.server; - -import java.io.IOException; -import java.io.InputStream; - -import javax.portlet.MimeResponse; -import javax.portlet.PortletContext; -import javax.portlet.PortletRequest; -import javax.portlet.PortletResponse; -import javax.portlet.RenderRequest; -import javax.portlet.RenderResponse; -import javax.portlet.ResourceURL; - -import org.json.JSONException; -import org.json.JSONObject; - -import com.vaadin.shared.ApplicationConstants; -import com.vaadin.ui.UI; - -/** - * TODO document me! - * - * @author peholmst - * - * - * @deprecated As of 7.0. Will likely change or be removed in a future version - */ -@Deprecated -@SuppressWarnings("serial") -public class PortletCommunicationManager extends AbstractCommunicationManager { - - public PortletCommunicationManager(VaadinSession session) { - super(session); - } - - @Override - protected BootstrapHandler createBootstrapHandler() { - return new BootstrapHandler() { - @Override - public boolean handleRequest(VaadinSession session, - VaadinRequest request, VaadinResponse response) - throws IOException { - PortletRequest portletRequest = ((VaadinPortletRequest) request) - .getPortletRequest(); - if (portletRequest instanceof RenderRequest) { - return super.handleRequest(session, request, response); - } else { - return false; - } - } - - @Override - protected String getServiceUrl(BootstrapContext context) { - ResourceURL portletResourceUrl = getRenderResponse(context) - .createResourceURL(); - portletResourceUrl.setResourceID(VaadinPortlet.RESOURCE_URL_ID); - return portletResourceUrl.toString(); - } - - private RenderResponse getRenderResponse(BootstrapContext context) { - PortletResponse response = ((VaadinPortletResponse) context - .getResponse()).getPortletResponse(); - - RenderResponse renderResponse = (RenderResponse) response; - return renderResponse; - } - - @Override - protected void appendMainScriptTagContents( - BootstrapContext context, StringBuilder builder) - throws JSONException, IOException { - // fixed base theme to use - all portal pages with Vaadin - // applications will load this exactly once - String portalTheme = ((VaadinPortletRequest) context - .getRequest()) - .getPortalProperty(VaadinPortlet.PORTAL_PARAMETER_VAADIN_THEME); - if (portalTheme != null - && !portalTheme.equals(context.getThemeName())) { - String portalThemeUri = getThemeUri(context, portalTheme); - // XSS safe - originates from portal properties - builder.append("vaadin.loadTheme('" + portalThemeUri - + "');"); - } - - super.appendMainScriptTagContents(context, builder); - } - - @Override - protected String getMainDivStyle(BootstrapContext context) { - VaadinService vaadinService = context.getRequest().getService(); - return vaadinService.getDeploymentConfiguration() - .getApplicationOrSystemProperty( - VaadinPortlet.PORTLET_PARAMETER_STYLE, null); - } - - @Override - protected JSONObject getApplicationParameters( - BootstrapContext context) throws JSONException, - PaintException { - JSONObject parameters = super.getApplicationParameters(context); - VaadinPortletResponse response = (VaadinPortletResponse) context - .getResponse(); - MimeResponse portletResponse = (MimeResponse) response - .getPortletResponse(); - ResourceURL resourceURL = portletResponse.createResourceURL(); - resourceURL.setResourceID("v-browserDetails"); - parameters.put("browserDetailsUrl", resourceURL.toString()); - - // Always send path info as a query parameter - parameters.put( - ApplicationConstants.SERVICE_URL_PATH_AS_PARAMETER, - true); - - return parameters; - } - - }; - - } - - @Override - protected InputStream getThemeResourceAsStream(UI uI, String themeName, - String resource) { - VaadinPortletSession session = (VaadinPortletSession) uI.getSession(); - PortletContext portletContext = session.getPortletSession() - .getPortletContext(); - return portletContext.getResourceAsStream("/" - + VaadinPortlet.THEME_DIR_PATH + '/' + themeName + "/" - + resource); - } - -} diff --git a/server/src/com/vaadin/server/RequestHandler.java b/server/src/com/vaadin/server/RequestHandler.java index 24107b744b..873752c5f2 100644 --- a/server/src/com/vaadin/server/RequestHandler.java +++ b/server/src/com/vaadin/server/RequestHandler.java @@ -19,17 +19,26 @@ package com.vaadin.server; import java.io.IOException; import java.io.Serializable; +import com.vaadin.ui.UI; + /** - * Handler for producing a response to non-UIDL requests. Handlers can be added - * to service sessions using - * {@link VaadinSession#addRequestHandler(RequestHandler)} + * Handler for producing a response to HTTP requests. Handlers can be either + * added on a {@link VaadinService service} level, common for all users, or on a + * {@link VaadinSession session} level for only a single user. */ public interface RequestHandler extends Serializable { /** - * Handles a non-UIDL request. If a response is written, this method should - * return <code>true</code> to indicate that no more request handlers should - * be invoked for the request. + * Called when a request needs to be handled. If a response is written, this + * method should return <code>true</code> to indicate that no more request + * handlers should be invoked for the request. + * <p> + * Note that request handlers by default do not lock the session. If you are + * using VaadinSession or anything inside the VaadinSession you must ensure + * the session is locked. This can be done by extending + * {@link SynchronizedRequestHandler} or by using + * {@link VaadinSession#access(Runnable)} or {@link UI#access(Runnable)}. + * </p> * * @param session * The session for the request @@ -40,6 +49,7 @@ public interface RequestHandler extends Serializable { * @return true if a response has been written and no further request * handlers should be called, otherwise false * @throws IOException + * If an IO error occurred */ boolean handleRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException; diff --git a/server/src/com/vaadin/server/RequestTimer.java b/server/src/com/vaadin/server/RequestTimer.java deleted file mode 100644 index 2f91348ce5..0000000000 --- a/server/src/com/vaadin/server/RequestTimer.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.server; - -import java.io.Serializable; - -/** - * Times the handling of requests and stores the information as an attribute in - * the request. The timing info is later passed on to the client in the UIDL and - * the client provides JavaScript API for accessing this data from e.g. - * TestBench. - * - * @author Jonatan Kronqvist / Vaadin Ltd - */ -public class RequestTimer implements Serializable { - private long requestStartTime = 0; - - /** - * Starts the timing of a request. This should be called before any - * processing of the request. - */ - public void start() { - requestStartTime = System.nanoTime(); - } - - /** - * Stops the timing of a request. This should be called when all processing - * of a request has finished. - * - * @param context - */ - public void stop(VaadinSession context) { - // Measure and store the total handling time. This data can be - // used in TestBench 3 tests. - long time = (System.nanoTime() - requestStartTime) / 1000000; - - // The timings must be stored in the context, since a new - // RequestTimer is created for every request. - context.setLastRequestDuration(time); - } -} diff --git a/server/src/com/vaadin/server/ServerRpcManager.java b/server/src/com/vaadin/server/ServerRpcManager.java index ec25ce83ca..a1682cb453 100644 --- a/server/src/com/vaadin/server/ServerRpcManager.java +++ b/server/src/com/vaadin/server/ServerRpcManager.java @@ -139,7 +139,7 @@ public class ServerRpcManager<T extends ServerRpc> implements Serializable { * * @return RPC interface type */ - protected Class<T> getRpcInterface() { + public Class<T> getRpcInterface() { return rpcInterface; } diff --git a/server/src/com/vaadin/server/ServletPortletHelper.java b/server/src/com/vaadin/server/ServletPortletHelper.java index ce9872f40e..c14467a10e 100644 --- a/server/src/com/vaadin/server/ServletPortletHelper.java +++ b/server/src/com/vaadin/server/ServletPortletHelper.java @@ -23,23 +23,14 @@ import com.vaadin.shared.ApplicationConstants; import com.vaadin.ui.Component; import com.vaadin.ui.UI; -/* - * Copyright 2000-2013 Vaadin Ltd. +/** + * Contains helper methods shared by {@link VaadinServlet} and + * {@link VaadinPortlet}. * - * 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. + * @deprecated As of 7.1. Will be removed or refactored in the future. */ - -class ServletPortletHelper implements Serializable { +@Deprecated +public class ServletPortletHelper implements Serializable { public static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/"; /** * The default SystemMessages (read-only). @@ -132,6 +123,10 @@ class ServletPortletHelper implements Serializable { return hasPathPrefix(request, ApplicationConstants.HEARTBEAT_PATH + '/'); } + public static boolean isPushRequest(VaadinRequest request) { + return hasPathPrefix(request, ApplicationConstants.PUSH_PATH + '/'); + } + public static void initDefaultUIProvider(VaadinSession session, VaadinService vaadinService) throws ServiceException { String uiProperty = vaadinService.getDeploymentConfiguration() @@ -200,7 +195,7 @@ class ServletPortletHelper implements Serializable { * <li>{@link Locale#getDefault()}</li> * </ol> */ - static Locale findLocale(Component component, VaadinSession session, + public static Locale findLocale(Component component, VaadinSession session, VaadinRequest request) { if (component == null) { component = UI.getCurrent(); @@ -234,5 +229,4 @@ class ServletPortletHelper implements Serializable { return Locale.getDefault(); } - } diff --git a/server/src/com/vaadin/server/SessionExpiredHandler.java b/server/src/com/vaadin/server/SessionExpiredHandler.java new file mode 100644 index 0000000000..6a7896f3d1 --- /dev/null +++ b/server/src/com/vaadin/server/SessionExpiredHandler.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.server; + +import java.io.IOException; + +/** + * A specialized RequestHandler which is capable of sending session expiration + * messages to the user. + * + * @since 7.1 + * @author Vaadin Ltd + */ +public interface SessionExpiredHandler extends RequestHandler { + + /** + * Called when the a session expiration has occured and a notification needs + * to be sent to the user. If a response is written, this method should + * return <code>true</code> to indicate that no more + * {@link SessionExpiredHandler} handlers should be invoked for the request. + * + * @param request + * The request to handle + * @param response + * The response object to which a response can be written. + * @return true if a response has been written and no further request + * handlers should be called, otherwise false + * @throws IOException + * If an IO error occurred + * @since 7.1 + */ + boolean handleSessionExpired(VaadinRequest request, VaadinResponse response) + throws IOException; + +} diff --git a/server/src/com/vaadin/server/SynchronizedRequestHandler.java b/server/src/com/vaadin/server/SynchronizedRequestHandler.java new file mode 100644 index 0000000000..ac730dcecb --- /dev/null +++ b/server/src/com/vaadin/server/SynchronizedRequestHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.server; + +import java.io.IOException; + +/** + * RequestHandler which takes care of locking and unlocking of the VaadinSession + * automatically. The session is locked before + * {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)} + * is called and unlocked after it has completed. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.1 + */ +public abstract class SynchronizedRequestHandler implements RequestHandler { + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + session.lock(); + try { + return synchronizedHandleRequest(session, request, response); + } finally { + session.unlock(); + } + } + + /** + * Identical to + * {@link #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)} + * except the {@link VaadinSession} is locked before this is called and + * unlocked after this has completed. + * + * @see #handleRequest(VaadinSession, VaadinRequest, VaadinResponse) + * @param session + * The session for the request + * @param request + * The request to handle + * @param response + * The response object to which a response can be written. + * @return true if a response has been written and no further request + * handlers should be called, otherwise false + * + * @throws IOException + * If an IO error occurred + */ + public abstract boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) throws IOException; + +} diff --git a/server/src/com/vaadin/server/UIProvider.java b/server/src/com/vaadin/server/UIProvider.java index a91db6b88d..0305b907e6 100644 --- a/server/src/com/vaadin/server/UIProvider.java +++ b/server/src/com/vaadin/server/UIProvider.java @@ -20,9 +20,11 @@ import java.io.Serializable; import java.lang.annotation.Annotation; import com.vaadin.annotations.PreserveOnRefresh; +import com.vaadin.annotations.Push; import com.vaadin.annotations.Theme; import com.vaadin.annotations.Title; import com.vaadin.annotations.Widgetset; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.UI; public abstract class UIProvider implements Serializable { @@ -149,4 +151,27 @@ public abstract class UIProvider implements Serializable { return titleAnnotation.value(); } } + + /** + * Finds the {@link PushMode} to use for a specific UI. If no specific push + * mode is required, <code>null</code> is returned. + * <p> + * The default implementation uses the @{@link Push} annotation if it's + * defined for the UI class. + * + * @param event + * the UI create event with information about the UI and the + * current request. + * @return the push mode to use, or <code>null</code> if the default push + * mode should be used + * + */ + public PushMode getPushMode(UICreateEvent event) { + Push push = getAnnotationFor(event.getUIClass(), Push.class); + if (push == null) { + return null; + } else { + return push.value(); + } + } } diff --git a/server/src/com/vaadin/server/UnsupportedBrowserHandler.java b/server/src/com/vaadin/server/UnsupportedBrowserHandler.java index 55d5a5c78f..5fc00408a9 100644 --- a/server/src/com/vaadin/server/UnsupportedBrowserHandler.java +++ b/server/src/com/vaadin/server/UnsupportedBrowserHandler.java @@ -24,18 +24,18 @@ import java.io.Writer; * * <p> * This handler is usually added to the application by - * {@link AbstractCommunicationManager}. + * {@link LegacyCommunicationManager}. * </p> */ @SuppressWarnings("serial") -public class UnsupportedBrowserHandler implements RequestHandler { +public class UnsupportedBrowserHandler extends SynchronizedRequestHandler { /** Cookie used to ignore browser checks */ public static final String FORCE_LOAD_COOKIE = "vaadinforceload=1"; @Override - public boolean handleRequest(VaadinSession session, VaadinRequest request, - VaadinResponse response) throws IOException { + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) throws IOException { // Check if the browser is supported // If Chrome Frame is available we'll assume it's ok diff --git a/server/src/com/vaadin/server/VaadinPortlet.java b/server/src/com/vaadin/server/VaadinPortlet.java index ac4e904898..327ce78a6c 100644 --- a/server/src/com/vaadin/server/VaadinPortlet.java +++ b/server/src/com/vaadin/server/VaadinPortlet.java @@ -15,16 +15,10 @@ */ package com.vaadin.server; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.security.GeneralSecurityException; import java.util.Enumeration; import java.util.Map; import java.util.Properties; @@ -46,12 +40,11 @@ import javax.portlet.ResourceRequest; import javax.portlet.ResourceResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; import com.liferay.portal.kernel.util.PortalClassLoaderUtil; import com.liferay.portal.kernel.util.PropsUtil; -import com.vaadin.server.AbstractCommunicationManager.Callback; -import com.vaadin.ui.UI; +import com.vaadin.server.communication.PortletDummyRequestHandler; +import com.vaadin.server.communication.PortletUIInitHandler; import com.vaadin.util.CurrentInstance; /** @@ -257,24 +250,6 @@ public class VaadinPortlet extends GenericPortlet implements Constants, } - public static class AbstractApplicationPortletWrapper implements Callback { - - private final VaadinPortlet portlet; - - public AbstractApplicationPortletWrapper(VaadinPortlet portlet) { - this.portlet = portlet; - } - - @Override - public void criticalNotification(VaadinRequest request, - VaadinResponse response, String cap, String msg, - String details, String outOfSyncURL) throws IOException { - portlet.criticalNotification((VaadinPortletRequest) request, - (VaadinPortletResponse) response, cap, msg, details, - outOfSyncURL); - } - } - /** * 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. @@ -332,11 +307,16 @@ public class VaadinPortlet extends GenericPortlet implements Constants, } DeploymentConfiguration deploymentConfiguration = createDeploymentConfiguration(initParameters); - vaadinService = createPortletService(deploymentConfiguration); + try { + vaadinService = createPortletService(deploymentConfiguration); + } catch (ServiceException e) { + throw new PortletException("Could not initialized VaadinPortlet", e); + } // Sets current service even though there are no request and response vaadinService.setCurrentInstances(null, null); portletInitialized(); + CurrentInstance.clearAll(); } @@ -350,15 +330,21 @@ public class VaadinPortlet extends GenericPortlet implements Constants, } protected VaadinPortletService createPortletService( - DeploymentConfiguration deploymentConfiguration) { - return new VaadinPortletService(this, deploymentConfiguration); + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { + VaadinPortletService service = new VaadinPortletService(this, + deploymentConfiguration); + service.init(); + return service; } /** * @author Vaadin Ltd * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @deprecated As of 7.0. This is no longer used and only provided for + * backwards compatibility. Each {@link RequestHandler} can + * individually decide whether it wants to handle a request or + * not. */ @Deprecated protected enum RequestType { @@ -369,8 +355,10 @@ public class VaadinPortlet extends GenericPortlet implements Constants, * @param vaadinRequest * @return * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @deprecated As of 7.0. This is no longer used and only provided for + * backwards compatibility. Each {@link RequestHandler} can + * individually decide whether it wants to handle a request or + * not. */ @Deprecated protected RequestType getRequestType(VaadinPortletRequest vaadinRequest) { @@ -381,7 +369,7 @@ public class VaadinPortlet extends GenericPortlet implements Constants, ResourceRequest resourceRequest = (ResourceRequest) request; if (ServletPortletHelper.isUIDLRequest(vaadinRequest)) { return RequestType.UIDL; - } else if (isBrowserDetailsRequest(resourceRequest)) { + } else if (PortletUIInitHandler.isUIInitRequest(vaadinRequest)) { return RequestType.BROWSER_DETAILS; } else if (ServletPortletHelper.isFileUploadRequest(vaadinRequest)) { return RequestType.FILE_UPLOAD; @@ -392,12 +380,9 @@ public class VaadinPortlet extends GenericPortlet implements Constants, return RequestType.APP; } else if (ServletPortletHelper.isHeartbeatRequest(vaadinRequest)) { return RequestType.HEARTBEAT; - } else if (isDummyRequest(resourceRequest)) { + } else if (PortletDummyRequestHandler.isDummyRequest(vaadinRequest)) { return RequestType.DUMMY; } else { - // these are not served with ResourceRequests, but by a servlet - // on the portal at portlet root path (configured by default by - // Liferay at deployment time, similar on other portals) return RequestType.STATIC_FILE; } } else if (request instanceof ActionRequest) { @@ -408,16 +393,6 @@ public class VaadinPortlet extends GenericPortlet implements Constants, return RequestType.UNKNOWN; } - private boolean isBrowserDetailsRequest(ResourceRequest request) { - return request.getResourceID() != null - && request.getResourceID().equals("v-browserDetails"); - } - - private boolean isDummyRequest(ResourceRequest request) { - return request.getResourceID() != null - && request.getResourceID().equals("DUMMY"); - } - /** * @param request * @param response @@ -430,145 +405,14 @@ public class VaadinPortlet extends GenericPortlet implements Constants, @Deprecated protected void handleRequest(PortletRequest request, PortletResponse response) throws PortletException, IOException { - RequestTimer requestTimer = new RequestTimer(); - requestTimer.start(); CurrentInstance.clearAll(); setCurrent(this); - try { - AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper( - this); - - VaadinPortletRequest vaadinRequest = createVaadinRequest(request); - - VaadinPortletResponse vaadinResponse = new VaadinPortletResponse( - response, getService()); - - getService().setCurrentInstances(vaadinRequest, vaadinResponse); - - RequestType requestType = getRequestType(vaadinRequest); - - if (requestType == RequestType.UNKNOWN) { - handleUnknownRequest(request, response); - } else if (requestType == RequestType.DUMMY) { - /* - * This dummy page is used by action responses to redirect to, - * in order to prevent the boot strap code from being rendered - * into strange places such as iframes. - */ - ((ResourceResponse) response).setContentType("text/html"); - final OutputStream out = ((ResourceResponse) response) - .getPortletOutputStream(); - final PrintWriter outWriter = new PrintWriter( - new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))); - outWriter.print("<html><body>dummy page</body></html>"); - outWriter.close(); - } else { - VaadinPortletSession vaadinSession = null; - - try { - // TODO What about PARAM_UNLOADBURST & - // redirectToApplication?? - - vaadinSession = (VaadinPortletSession) getService() - .findVaadinSession(vaadinRequest); - if (vaadinSession == null) { - return; - } - - PortletCommunicationManager communicationManager = (PortletCommunicationManager) vaadinSession - .getCommunicationManager(); - - if (requestType == RequestType.PUBLISHED_FILE) { - communicationManager.servePublishedFile(vaadinRequest, - vaadinResponse); - return; - } else if (requestType == RequestType.HEARTBEAT) { - communicationManager.handleHeartbeatRequest( - vaadinRequest, vaadinResponse, vaadinSession); - return; - } - - /* Update browser information from request */ - vaadinSession.getBrowser().updateRequestDetails( - vaadinRequest); - - /* Notify listeners */ - - // Finds the right UI - UI uI = null; - if (requestType == RequestType.UIDL) { - uI = getService().findUI(vaadinRequest); - } - - // TODO Should this happen before or after the transaction - // starts? - if (request instanceof RenderRequest) { - vaadinSession.firePortletRenderRequest(uI, - (RenderRequest) request, - (RenderResponse) response); - } else if (request instanceof ActionRequest) { - vaadinSession.firePortletActionRequest(uI, - (ActionRequest) request, - (ActionResponse) response); - } else if (request instanceof EventRequest) { - vaadinSession.firePortletEventRequest(uI, - (EventRequest) request, - (EventResponse) response); - } else if (request instanceof ResourceRequest) { - vaadinSession.firePortletResourceRequest(uI, - (ResourceRequest) request, - (ResourceResponse) response); - } - - /* Handle the request */ - if (requestType == RequestType.FILE_UPLOAD) { - // UI is resolved in handleFileUpload by - // PortletCommunicationManager - communicationManager.handleFileUpload(vaadinSession, - vaadinRequest, vaadinResponse); - return; - } else if (requestType == RequestType.BROWSER_DETAILS) { - communicationManager.handleBrowserDetailsRequest( - vaadinRequest, vaadinResponse, vaadinSession); - return; - } else if (requestType == RequestType.UIDL) { - // Handles AJAX UIDL requests - communicationManager.handleUidlRequest(vaadinRequest, - vaadinResponse, portletWrapper, uI); - - // Ensure that the browser does not cache UIDL - // responses. - // iOS 6 Safari requires this (#9732) - response.setProperty("Cache-Control", "no-cache"); - return; - } else { - handleOtherRequest(vaadinRequest, vaadinResponse, - requestType, vaadinSession, - communicationManager); - } - } catch (final SessionExpiredException e) { - // TODO Figure out a better way to deal with - // SessionExpiredExceptions - getLogger().finest("A user session has expired"); - } catch (final GeneralSecurityException e) { - // TODO Figure out a better way to deal with - // GeneralSecurityExceptions - getLogger() - .fine("General security exception, the security key was probably incorrect."); - } catch (final Throwable e) { - handleServiceException(vaadinRequest, vaadinResponse, - vaadinSession, e); - } finally { - if (vaadinSession != null) { - getService().cleanupSession(vaadinSession); - requestTimer.stop(vaadinSession); - } - } - } - } finally { - CurrentInstance.clearAll(); + getService().handleRequest(createVaadinRequest(request), + createVaadinResponse(response)); + } catch (ServiceException e) { + throw new PortletException(e); } } @@ -592,50 +436,12 @@ public class VaadinPortlet extends GenericPortlet implements Constants, } - protected VaadinPortletService getService() { - return vaadinService; - } - - private void handleUnknownRequest(PortletRequest request, - PortletResponse response) { - getLogger().warning("Unknown request type"); + private VaadinPortletResponse createVaadinResponse(PortletResponse response) { + return new VaadinPortletResponse(response, getService()); } - /** - * Handle a portlet request that is not for static files, UIDL or upload. - * Also render requests are handled here. - * - * This method is called after starting the application and calling portlet - * and transaction listeners. - * - * @param request - * @param response - * @param requestType - * @param vaadinSession - * @param vaadinSession - * @param communicationManager - * @throws PortletException - * @throws IOException - * @throws MalformedURLException - */ - private void handleOtherRequest(VaadinPortletRequest request, - VaadinResponse response, RequestType requestType, - VaadinSession vaadinSession, - PortletCommunicationManager communicationManager) - throws PortletException, IOException, MalformedURLException { - if (requestType == RequestType.APP || requestType == RequestType.RENDER) { - if (!communicationManager.handleOtherRequest(request, response)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND, - "Not found"); - } - } else if (requestType == RequestType.EVENT) { - // nothing to do, listeners do all the work - } else if (requestType == RequestType.ACTION) { - // nothing to do, listeners do all the work - } else { - throw new IllegalStateException( - "handleRequest() without anything to do - should never happen!"); - } + protected VaadinPortletService getService() { + return vaadinService; } @Override @@ -678,98 +484,6 @@ public class VaadinPortlet extends GenericPortlet implements Constants, handleRequest(request, response); } - private void handleServiceException(VaadinPortletRequest request, - VaadinPortletResponse response, VaadinSession vaadinSession, - Throwable e) throws IOException, PortletException { - // TODO Check that this error handler is working when running inside a - // portlet - - // if this was an UIDL request, response UIDL back to client - ErrorHandler errorHandler = ErrorEvent.findErrorHandler(vaadinSession); - if (getRequestType(request) == RequestType.UIDL) { - SystemMessages ci = getService().getSystemMessages( - ServletPortletHelper.findLocale(null, vaadinSession, - request), request); - criticalNotification(request, response, - ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), - null, ci.getInternalErrorURL()); - if (errorHandler != null) { - errorHandler.error(new ErrorEvent(e)); - } - } else { - if (errorHandler != null) { - errorHandler.error(new ErrorEvent(e)); - } else { - // Re-throw other exceptions - throw new PortletException(e); - } - } - } - - /** - * Send notification to client's application. Used to notify client of - * critical errors and session expiration due to long inactivity. Server has - * no knowledge of what application client refers to. - * - * @param request - * the Portlet request instance. - * @param response - * the Portlet response to write to. - * @param caption - * for the notification - * @param message - * for the notification - * @param details - * a detail message to show in addition to the passed message. - * Currently shown directly but could be hidden behind a details - * drop down. - * @param url - * url to load after message, null for current page - * @throws IOException - * if the writing failed due to input/output error. - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version - */ - @Deprecated - void criticalNotification(VaadinPortletRequest request, - VaadinPortletResponse response, String caption, String message, - String details, String url) throws IOException { - - // clients JS app is still running, but server application either - // no longer exists or it might fail to perform reasonably. - // send a notification to client's application and link how - // to "restart" application. - - if (caption != null) { - caption = "\"" + caption + "\""; - } - if (details != null) { - if (message == null) { - message = details; - } else { - message += "<br/><br/>" + details; - } - } - if (message != null) { - message = "\"" + message + "\""; - } - if (url != null) { - url = "\"" + url + "\""; - } - - // Set the response type - response.setContentType("application/json; charset=UTF-8"); - final OutputStream out = response.getOutputStream(); - final PrintWriter outWriter = new PrintWriter(new BufferedWriter( - new OutputStreamWriter(out, "UTF-8"))); - outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {" - + "\"appError\": {" + "\"caption\":" + caption + "," - + "\"message\" : " + message + "," + "\"url\" : " + url - + "}}, \"resources\": {}, \"locales\":[]}]"); - outWriter.close(); - } - private static final Logger getLogger() { return Logger.getLogger(VaadinPortlet.class.getName()); } diff --git a/server/src/com/vaadin/server/VaadinPortletService.java b/server/src/com/vaadin/server/VaadinPortletService.java index e59ea7fd5e..2eca07dd4a 100644 --- a/server/src/com/vaadin/server/VaadinPortletService.java +++ b/server/src/com/vaadin/server/VaadinPortletService.java @@ -17,21 +17,30 @@ package com.vaadin.server; import java.io.File; +import java.io.InputStream; import java.net.URL; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import javax.portlet.EventRequest; import javax.portlet.PortletContext; import javax.portlet.PortletRequest; +import javax.portlet.RenderRequest; import com.vaadin.server.VaadinPortlet.RequestType; +import com.vaadin.server.communication.PortletBootstrapHandler; +import com.vaadin.server.communication.PortletDummyRequestHandler; +import com.vaadin.server.communication.PortletListenerNotifier; +import com.vaadin.server.communication.PortletUIInitHandler; import com.vaadin.ui.UI; public class VaadinPortletService extends VaadinService { private final VaadinPortlet portlet; public VaadinPortletService(VaadinPortlet portlet, - DeploymentConfiguration deploymentConfiguration) { + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { super(deploymentConfiguration); this.portlet = portlet; @@ -46,7 +55,25 @@ public class VaadinPortletService extends VaadinService { } } - protected VaadinPortlet getPortlet() { + @Override + protected List<RequestHandler> createRequestHandlers() + throws ServiceException { + List<RequestHandler> handlers = super.createRequestHandlers(); + + handlers.add(new PortletUIInitHandler()); + handlers.add(new PortletListenerNotifier()); + handlers.add(0, new PortletDummyRequestHandler()); + handlers.add(0, new PortletBootstrapHandler()); + + return handlers; + } + + /** + * Retrieves a reference to the portlet associated with this service. + * + * @return A reference to the VaadinPortlet this service is using + */ + public VaadinPortlet getPortlet() { return portlet; } @@ -155,13 +182,19 @@ public class VaadinPortletService extends VaadinService { @Override protected boolean requestCanCreateSession(VaadinRequest request) { - RequestType requestType = getRequestType(request); - if (requestType == RequestType.RENDER) { + if (!(request instanceof VaadinPortletRequest)) { + throw new IllegalArgumentException( + "Request is not a VaadinPortletRequest"); + } + + PortletRequest portletRequest = ((VaadinPortletRequest) request) + .getPortletRequest(); + if (portletRequest instanceof RenderRequest) { // In most cases the first request is a render request that // renders the HTML fragment. This should create a Vaadin // session unless there is already one. return true; - } else if (requestType == RequestType.EVENT) { + } else if (portletRequest instanceof EventRequest) { // A portlet can also be sent an event even though it has not // been rendered, e.g. portlet on one page sends an event to a // portlet on another page and then moves the user to that page. @@ -191,12 +224,6 @@ public class VaadinPortletService extends VaadinService { return type; } - @Override - protected AbstractCommunicationManager createCommunicationManager( - VaadinSession session) { - return new PortletCommunicationManager(session); - } - public static PortletRequest getCurrentPortletRequest() { VaadinRequest currentRequest = VaadinService.getCurrentRequest(); if (currentRequest instanceof VaadinPortletRequest) { @@ -230,6 +257,17 @@ public class VaadinPortletService extends VaadinService { } @Override + public InputStream getThemeResourceAsStream(UI uI, String themeName, + String resource) { + VaadinPortletSession session = (VaadinPortletSession) uI.getSession(); + PortletContext portletContext = session.getPortletSession() + .getPortletContext(); + return portletContext.getResourceAsStream("/" + + VaadinPortlet.THEME_DIR_PATH + '/' + themeName + "/" + + resource); + } + + @Override public String getMainDivId(VaadinSession session, VaadinRequest request, Class<? extends UI> uiClass) { PortletRequest portletRequest = ((VaadinPortletRequest) request) @@ -240,4 +278,20 @@ public class VaadinPortletService extends VaadinService { */ return "v-" + portletRequest.getWindowID(); } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.server.VaadinService#handleSessionExpired(com.vaadin.server + * .VaadinRequest, com.vaadin.server.VaadinResponse) + */ + @Override + protected void handleSessionExpired(VaadinRequest request, + VaadinResponse response) { + // TODO Figure out a better way to deal with + // SessionExpiredExceptions + getLogger().finest("A user session has expired"); + } + } diff --git a/server/src/com/vaadin/server/VaadinService.java b/server/src/com/vaadin/server/VaadinService.java index ada0fac107..af0c280c19 100644 --- a/server/src/com/vaadin/server/VaadinService.java +++ b/server/src/com/vaadin/server/VaadinService.java @@ -16,24 +16,43 @@ package com.vaadin.server; +import java.io.BufferedWriter; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.portlet.PortletContext; import javax.servlet.ServletContext; -import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONException; +import org.json.JSONObject; import com.vaadin.annotations.PreserveOnRefresh; import com.vaadin.event.EventRouter; +import com.vaadin.server.communication.FileUploadHandler; +import com.vaadin.server.communication.HeartbeatHandler; +import com.vaadin.server.communication.PublishedFileHandler; +import com.vaadin.server.communication.SessionRequestHandler; +import com.vaadin.server.communication.UidlRequestHandler; +import com.vaadin.shared.JsonConstants; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.ui.UI; import com.vaadin.util.CurrentInstance; @@ -70,6 +89,8 @@ public abstract class VaadinService implements Serializable { @Deprecated public static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication"; + private static final String REQUEST_START_TIME_ATTRIBUTE = "requestStartTime"; + private final DeploymentConfiguration deploymentConfiguration; private final EventRouter eventRouter = new EventRouter(); @@ -79,6 +100,15 @@ public abstract class VaadinService implements Serializable { private ClassLoader classLoader; + private Iterable<RequestHandler> requestHandlers; + + /** + * Keeps track of whether a warning about missing push support has already + * been logged. This is used to avoid spamming the log with the same message + * every time a new UI is bootstrapped. + */ + private boolean pushWarningEmitted = false; + /** * Creates a new vaadin service based on a deployment configuration * @@ -107,6 +137,45 @@ public abstract class VaadinService implements Serializable { } /** + * Initializes this service. The service should be initialized before it is + * used. + * + * @since 7.1 + * @throws ServiceException + * if a problem occurs when creating the service + */ + public void init() throws ServiceException { + List<RequestHandler> handlers = createRequestHandlers(); + Collections.reverse(handlers); + requestHandlers = Collections.unmodifiableCollection(handlers); + } + + /** + * Called during initialization to add the request handlers for the service. + * Note that the returned list will be reversed so the last handler will be + * called first. This enables overriding this method and using add on the + * returned list to add a custom request handler which overrides any + * predefined handler. + * + * @return The list of request handlers used by this service. + * @throws ServiceException + * if a problem occurs when creating the request handlers + */ + protected List<RequestHandler> createRequestHandlers() + throws ServiceException { + ArrayList<RequestHandler> handlers = new ArrayList<RequestHandler>(); + handlers.add(new SessionRequestHandler()); + handlers.add(new PublishedFileHandler()); + handlers.add(new HeartbeatHandler()); + handlers.add(new FileUploadHandler()); + handlers.add(new UidlRequestHandler()); + handlers.add(new UnsupportedBrowserHandler()); + handlers.add(new ConnectorResourceHandler()); + + return handlers; + } + + /** * Return the URL from where static files, e.g. the widgetset and the theme, * are served. In a standard configuration the VAADIN folder inside the * returned folder is what is used for widgetsets and themes. @@ -329,18 +398,40 @@ public abstract class VaadinService implements Serializable { SESSION_DESTROY_METHOD); } + /** + * Handles destruction of the given session. Internally ensures proper + * locking is done. + * + * @param vaadinSession + * The session to destroy + */ public void fireSessionDestroy(VaadinSession vaadinSession) { - for (UI ui : new ArrayList<UI>(vaadinSession.getUIs())) { - // close() called here for consistency so that it is always called - // before a UI is removed. UI.isClosing() is thus always true in - // UI.detach() and associated detach listeners. - if (!ui.isClosing()) { - ui.close(); + final VaadinSession session = vaadinSession; + session.access(new Runnable() { + @Override + public void run() { + ArrayList<UI> uis = new ArrayList<UI>(session.getUIs()); + for (final UI ui : uis) { + ui.access(new Runnable() { + @Override + public void run() { + /* + * close() called here for consistency so that it is + * always called before a UI is removed. + * UI.isClosing() is thus always true in UI.detach() + * and associated detach listeners. + */ + if (!ui.isClosing()) { + ui.close(); + } + session.removeUI(ui); + } + }); + } + eventRouter.fireEvent(new SessionDestroyEvent( + VaadinService.this, session)); } - vaadinSession.removeUI(ui); - } - - eventRouter.fireEvent(new SessionDestroyEvent(this, vaadinSession)); + }); } /** @@ -358,6 +449,10 @@ public abstract class VaadinService implements Serializable { /** * Attempts to find a Vaadin service session associated with this request. + * <p> + * Handles locking of the session internally to avoid creation of duplicate + * sessions by two threads simultaneously. + * </p> * * @param request * the request to get a vaadin service session for. @@ -381,9 +476,135 @@ public abstract class VaadinService implements Serializable { return vaadinSession; } + /** + * Associates the given lock with this service and the given wrapped + * session. This method should not be called more than once when the lock is + * initialized for the session. + * + * @see #getSessionLock(WrappedSession) + * @param wrappedSession + * The wrapped session the lock is associated with + * @param lock + * The lock object + */ + private void setSessionLock(WrappedSession wrappedSession, Lock lock) { + assert wrappedSession != null : "Can't set a lock for a null session"; + assert wrappedSession.getAttribute(getLockAttributeName()) == null : "Changing the lock for a session is not allowed"; + + wrappedSession.setAttribute(getLockAttributeName(), lock); + } + + /** + * Returns the name used to store the lock in the HTTP session. + * + * @return The attribute name for the lock + */ + private String getLockAttributeName() { + return getServiceName() + ".lock"; + } + + /** + * Gets the lock instance used to lock the VaadinSession associated with the + * given wrapped session. + * <p> + * This method uses the wrapped session instead of VaadinSession to be able + * to lock even before the VaadinSession has been initialized. + * </p> + * + * @param wrappedSession + * The wrapped session + * @return A lock instance used for locking access to the wrapped session + */ + protected Lock getSessionLock(WrappedSession wrappedSession) { + Object lock = wrappedSession.getAttribute(getLockAttributeName()); + + if (lock instanceof ReentrantLock) { + return (ReentrantLock) lock; + } + + if (lock == null) { + return null; + } + + throw new RuntimeException( + "Something else than a ReentrantLock was stored in the " + + getLockAttributeName() + " in the session"); + } + + /** + * Locks the given session for this service instance. Typically you want to + * call {@link VaadinSession#lock()} instead of this method. + * + * @param wrappedSession + * The session to lock + */ + protected void lockSession(WrappedSession wrappedSession) { + Lock lock = getSessionLock(wrappedSession); + if (lock == null) { + /* + * No lock found in the session attribute. Ensure only one lock is + * created and used by everybody by doing double checked locking. + * Assumes there is a memory barrier for the attribute (i.e. that + * the CPU flushes its caches and reads the value directly from main + * memory). + */ + synchronized (VaadinService.class) { + lock = getSessionLock(wrappedSession); + if (lock == null) { + lock = new ReentrantLock(); + setSessionLock(wrappedSession, lock); + } + } + } + lock.lock(); + } + + /** + * Releases the lock for the given session for this service instance. + * Typically you want to call {@link VaadinSession#unlock()} instead of this + * method. + * + * @param wrappedSession + * The session to unlock + */ + protected void unlockSession(WrappedSession wrappedSession) { + assert getSessionLock(wrappedSession) != null; + assert ((ReentrantLock) getSessionLock(wrappedSession)) + .isHeldByCurrentThread() : "Trying to unlock the session but it has not been locked by this thread"; + getSessionLock(wrappedSession).unlock(); + } + private VaadinSession findOrCreateVaadinSession(VaadinRequest request) throws SessionExpiredException, ServiceException { boolean requestCanCreateSession = requestCanCreateSession(request); + WrappedSession wrappedSession = getWrappedSession(request, + requestCanCreateSession); + + lockSession(wrappedSession); + try { + return doFindOrCreateVaadinSession(request, requestCanCreateSession); + } finally { + unlockSession(wrappedSession); + } + + } + + /** + * Finds or creates a Vaadin session. Assumes necessary synchronization has + * been done by the caller to ensure this is not called simultaneously by + * several threads. + * + * @param request + * @param requestCanCreateSession + * @return + * @throws SessionExpiredException + * @throws ServiceException + */ + private VaadinSession doFindOrCreateVaadinSession(VaadinRequest request, + boolean requestCanCreateSession) throws SessionExpiredException, + ServiceException { + assert ((ReentrantLock) getSessionLock(request.getWrappedSession())) + .isHeldByCurrentThread() : "Session has not been locked by this thread"; /* Find an existing session for this request. */ VaadinSession session = getExistingSession(request, @@ -395,10 +616,12 @@ public abstract class VaadinService implements Serializable { * not specifically requested to close or restart it. */ - final boolean restartApplication = (request - .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null); - final boolean closeApplication = (request - .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null); + final boolean restartApplication = hasParameter(request, + URL_PARAMETER_RESTART_APPLICATION) + && !hasParameter(request, + BootstrapHandler.IGNORE_RESTART_PARAM); + final boolean closeApplication = hasParameter(request, + URL_PARAMETER_CLOSE_APPLICATION); if (restartApplication) { closeSession(session, request.getWrappedSession(false)); @@ -429,8 +652,26 @@ public abstract class VaadinService implements Serializable { } + private static boolean hasParameter(VaadinRequest request, + String parameterName) { + return request.getParameter(parameterName) != null; + } + + /** + * Creates and registers a new VaadinSession for this service. Assumes + * proper locking has been taken care of by the caller. + * + * + * @param request + * The request which triggered session creation. + * @return A new VaadinSession instance + * @throws ServiceException + */ private VaadinSession createAndRegisterSession(VaadinRequest request) throws ServiceException { + assert ((ReentrantLock) getSessionLock(request.getWrappedSession())) + .isHeldByCurrentThread() : "Session has not been locked by this thread"; + VaadinSession session = createVaadinSession(request); VaadinSession.setCurrent(session); @@ -441,7 +682,7 @@ public abstract class VaadinService implements Serializable { Locale locale = request.getLocale(); session.setLocale(locale); session.setConfiguration(getDeploymentConfiguration()); - session.setCommunicationManager(createCommunicationManager(session)); + session.setCommunicationManager(new LegacyCommunicationManager(session)); ServletPortletHelper.initDefaultUIProvider(session, this); onVaadinSessionStarted(request, session); @@ -468,23 +709,13 @@ public abstract class VaadinService implements Serializable { } /** - * Create a communication manager to use for the given service session. - * - * @param session - * the service session for which a new communication manager is - * needed - * @return a new communication manager - */ - protected abstract AbstractCommunicationManager createCommunicationManager( - VaadinSession session); - - /** - * Creates a new Vaadin service session. + * Creates a new Vaadin session for this service and request * * @param request - * @return - * @throws ServletException - * @throws MalformedURLException + * The request for which to create a VaadinSession + * @return A new VaadinSession + * @throws ServiceException + * */ protected VaadinSession createVaadinSession(VaadinRequest request) throws ServiceException { @@ -512,12 +743,8 @@ public abstract class VaadinService implements Serializable { protected VaadinSession getExistingSession(VaadinRequest request, boolean allowSessionCreation) throws SessionExpiredException { - // Ensures that the session is still valid - final WrappedSession session = request - .getWrappedSession(allowSessionCreation); - if (session == null) { - throw new SessionExpiredException(); - } + final WrappedSession session = getWrappedSession(request, + allowSessionCreation); VaadinSession vaadinSession = VaadinSession .getForSession(this, session); @@ -530,6 +757,28 @@ public abstract class VaadinService implements Serializable { } /** + * Retrieves the wrapped session for the request. + * + * @param request + * The request for which to retrieve a session + * @param requestCanCreateSession + * true to create a new session if one currently does not exist + * @return The retrieved (or created) wrapped session + * @throws SessionExpiredException + * If the request is not associated to a session and new session + * creation is not allowed + */ + private WrappedSession getWrappedSession(VaadinRequest request, + boolean requestCanCreateSession) throws SessionExpiredException { + final WrappedSession session = request + .getWrappedSession(requestCanCreateSession); + if (session == null) { + throw new SessionExpiredException(); + } + return session; + } + + /** * Checks whether it's valid to create a new service session as a result of * the given request. * @@ -582,12 +831,21 @@ public abstract class VaadinService implements Serializable { */ public void setCurrentInstances(VaadinRequest request, VaadinResponse response) { - CurrentInstance.setInheritable(VaadinService.class, this); + setCurrent(this); CurrentInstance.set(VaadinRequest.class, request); CurrentInstance.set(VaadinResponse.class, response); } /** + * Sets the given Vaadin service as the current service. + * + * @param service + */ + public static void setCurrent(VaadinService service) { + CurrentInstance.setInheritable(VaadinService.class, service); + } + + /** * Gets the currently processed Vaadin request. The current request is * automatically defined when the request is started. The current request * can not be used in e.g. background threads because of the way server @@ -640,6 +898,7 @@ public abstract class VaadinService implements Serializable { * */ public UI findUI(VaadinRequest request) { + // getForSession asserts that the lock is held VaadinSession session = VaadinSession.getForSession(this, request.getWrappedSession()); @@ -647,16 +906,10 @@ public abstract class VaadinService implements Serializable { String uiIdString = request.getParameter(UIConstants.UI_ID_PARAMETER); int uiId = Integer.parseInt(uiIdString); - // Get lock before accessing data in session - session.lock(); - try { - UI ui = session.getUIById(uiId); + UI ui = session.getUIById(uiId); - UI.setCurrent(ui); - return ui; - } finally { - session.unlock(); - } + UI.setCurrent(ui); + return ui; } /** @@ -725,8 +978,12 @@ public abstract class VaadinService implements Serializable { // Ensure VaadinServiceSession knows where it's stored if (value instanceof VaadinSession) { VaadinSession serviceSession = (VaadinSession) value; - serviceSession.storeInSession(serviceSession.getService(), - newSession); + VaadinService service = serviceSession.getService(); + // Use the same lock instance in the new session + service.setSessionLock(newSession, + serviceSession.getLockInstance()); + + serviceSession.storeInSession(service, newSession); serviceSession .setAttribute(REINITIALIZING_SESSION_MARKER, null); } @@ -735,6 +992,19 @@ public abstract class VaadinService implements Serializable { } /** + * TODO PUSH Document + * + * TODO Pass UI or VaadinSession? + * + * @param uI + * @param themeName + * @param resource + * @return + */ + public abstract InputStream getThemeResourceAsStream(UI uI, + String themeName, String resource); + + /** * Creates and returns a unique ID for the DIV where the UI is to be * rendered. * @@ -814,13 +1084,19 @@ public abstract class VaadinService implements Serializable { * * @param session */ - private void removeClosedUIs(VaadinSession session) { - for (UI ui : new ArrayList<UI>(session.getUIs())) { - if (ui.isClosing()) { - getLogger().log(Level.FINER, "Removing closed UI {0}", - ui.getUIId()); - session.removeUI(ui); - } + private void removeClosedUIs(final VaadinSession session) { + ArrayList<UI> uis = new ArrayList<UI>(session.getUIs()); + for (final UI ui : uis) { + ui.access(new Runnable() { + @Override + public void run() { + if (ui.isClosing()) { + getLogger().log(Level.FINER, "Removing closed UI {0}", + ui.getUIId()); + session.removeUI(ui); + } + } + }); } } @@ -938,4 +1214,387 @@ public abstract class VaadinService implements Serializable { private static final Logger getLogger() { return Logger.getLogger(VaadinService.class.getName()); } + + /** + * Called before the framework starts handling a request + * + * @param request + * The request + * @param response + * The response + */ + public void requestStart(VaadinRequest request, VaadinResponse response) { + setCurrentInstances(request, response); + request.setAttribute(REQUEST_START_TIME_ATTRIBUTE, System.nanoTime()); + } + + /** + * Called after the framework has handled a request and the response has + * been written. + * + * @param request + * The request object + * @param response + * The response object + * @param session + * The session which was used during the request or null if the + * request did not use a session + */ + public void requestEnd(VaadinRequest request, VaadinResponse response, + VaadinSession session) { + if (session != null) { + final VaadinSession finalSession = session; + + session.access(new Runnable() { + @Override + public void run() { + cleanupSession(finalSession); + } + }); + + final long duration = (System.nanoTime() - (Long) request + .getAttribute(REQUEST_START_TIME_ATTRIBUTE)) / 1000000; + session.access(new Runnable() { + @Override + public void run() { + finalSession.setLastRequestDuration(duration); + } + }); + } + CurrentInstance.clearAll(); + } + + /** + * Returns the request handlers that are registered with this service. The + * iteration order of the returned collection is the same as the order in + * which the request handlers will be invoked when a request is handled. + * + * @return a collection of request handlers in the order they are invoked + * + * @see #createRequestHandlers() + * + * @since 7.1 + */ + public Iterable<RequestHandler> getRequestHandlers() { + return requestHandlers; + } + + /** + * Handles the incoming request and writes the response into the response + * object. Uses {@link #getRequestHandlers()} for handling the request. + * <p> + * If a session expiration is detected during request handling then each + * {@link RequestHandler request handler} has an opportunity to handle the + * expiration event if it implements {@link SessionExpiredHandler}. If no + * request handler handles session expiration a default expiration message + * will be written. + * </p> + * + * @param request + * The incoming request + * @param response + * The outgoing response + * @throws ServiceException + * Any exception that occurs during response handling will be + * wrapped in a ServiceException + */ + public void handleRequest(VaadinRequest request, VaadinResponse response) + throws ServiceException { + requestStart(request, response); + + VaadinSession vaadinSession = null; + try { + // Find out the service session this request is related to + vaadinSession = findVaadinSession(request); + if (vaadinSession == null) { + return; + } + + for (RequestHandler handler : getRequestHandlers()) { + if (handler.handleRequest(vaadinSession, request, response)) { + return; + } + } + + // Request not handled by any RequestHandler + response.sendError(HttpServletResponse.SC_NOT_FOUND, + "Request was not handled by any registered handler."); + + } catch (final SessionExpiredException e) { + handleSessionExpired(request, response); + } catch (final Throwable e) { + handleExceptionDuringRequest(request, response, vaadinSession, e); + } finally { + requestEnd(request, response, vaadinSession); + } + } + + private void handleExceptionDuringRequest(VaadinRequest request, + VaadinResponse response, VaadinSession vaadinSession, Throwable t) + throws ServiceException { + if (vaadinSession != null) { + vaadinSession.lock(); + } + try { + ErrorHandler errorHandler = ErrorEvent + .findErrorHandler(vaadinSession); + + // if this was an UIDL request, send UIDL back to the client + if (ServletPortletHelper.isUIDLRequest(request)) { + SystemMessages ci = getSystemMessages( + ServletPortletHelper.findLocale(null, vaadinSession, + request), request); + try { + writeStringResponse( + response, + JsonConstants.JSON_CONTENT_TYPE, + createCriticalNotificationJSON( + ci.getInternalErrorCaption(), + ci.getInternalErrorMessage(), null, + ci.getInternalErrorURL())); + } catch (IOException e) { + // An exception occured while writing the response. Log + // it and continue handling only the original error. + getLogger() + .log(Level.WARNING, + "Failed to write critical notification response to the client", + e); + } + if (errorHandler != null) { + errorHandler.error(new ErrorEvent(t)); + } + } else { + if (errorHandler != null) { + errorHandler.error(new ErrorEvent(t)); + } + + // Re-throw other exceptions + throw new ServiceException(t); + } + } finally { + if (vaadinSession != null) { + vaadinSession.unlock(); + } + } + + } + + /** + * Writes the given string as a response using the given content type. + * + * @param response + * The response reference + * @param contentType + * The content type of the response + * @param reponseString + * The actual response + * @throws IOException + * If an error occured while writing the response + */ + public void writeStringResponse(VaadinResponse response, + String contentType, String reponseString) throws IOException { + + response.setContentType(contentType); + + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print(reponseString); + outWriter.close(); + } + + /** + * Called when the session has expired and the request handling is therefore + * aborted. + * + * @param request + * The request + * @param response + * The response + * @throws ServiceException + * Thrown if there was any problem handling the expiration of + * the session + */ + protected void handleSessionExpired(VaadinRequest request, + VaadinResponse response) throws ServiceException { + for (RequestHandler handler : getRequestHandlers()) { + if (handler instanceof SessionExpiredHandler) { + try { + if (((SessionExpiredHandler) handler).handleSessionExpired( + request, response)) { + return; + } + } catch (IOException e) { + throw new ServiceException( + "Handling of session expired failed", e); + } + } + } + + // No request handlers handled the request. Write a normal HTTP response + + try { + // If there is a URL, try to redirect there + SystemMessages systemMessages = getSystemMessages( + ServletPortletHelper.findLocale(null, null, request), + request); + String sessionExpiredURL = systemMessages.getSessionExpiredURL(); + if (sessionExpiredURL != null + && (response instanceof VaadinServletResponse)) { + ((VaadinServletResponse) response) + .sendRedirect(sessionExpiredURL); + } else { + /* + * Session expired as a result of a standard http request and we + * have nowhere to redirect. Reloading would likely cause an + * endless loop. This can at least happen if refreshing a + * resource when the session has expired. + */ + response.sendError(HttpServletResponse.SC_GONE, + "Session expired"); + } + } catch (IOException e) { + throw new ServiceException(e); + } + } + + /** + * Creates a JSON message which, when sent to client as-is, will cause a + * critical error to be shown with the given details. + * + * @param caption + * The caption of the error or null to omit + * @param message + * The error message or null to omit + * @param details + * Additional error details or null to omit + * @param url + * A url to redirect to. If no other details are given then the + * user will be immediately redirected to this URL. Otherwise the + * message will be shown and the browser will redirect to the + * given URL only after the user acknowledges the message. If + * null then the browser will refresh the current page. + * @return A JSON string to be sent to the client + */ + public static String createCriticalNotificationJSON(String caption, + String message, String details, String url) { + String returnString = ""; + try { + if (message == null) { + message = details; + } else if (details != null) { + message += "<br/><br/>" + details; + } + + JSONObject appError = new JSONObject(); + appError.put("caption", caption); + appError.put("message", message); + appError.put("url", url); + + JSONObject meta = new JSONObject(); + 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("meta", meta); + returnString = json.toString(); + } catch (JSONException e) { + getLogger().log(Level.WARNING, + "Error creating critical notification JSON message", e); + } + + return "for(;;);[" + returnString + "]"; + } + + /** + * @deprecated As of 7.0. Will likely change or be removed in a future + * version + */ + @Deprecated + public void criticalNotification(VaadinRequest request, + VaadinResponse response, String caption, String message, + String details, String url) throws IOException { + writeStringResponse(response, JsonConstants.JSON_CONTENT_TYPE, + createCriticalNotificationJSON(caption, message, details, url)); + } + + /** + * Enables push if push support is available and push has not yet been + * enabled. + * + * If push support is not available, a warning explaining the situation will + * be logged at least the first time this method is invoked. + * + * @return <code>true</code> if push can be used; <code>false</code> if push + * is not available. + */ + public boolean ensurePushAvailable() { + if (!pushWarningEmitted) { + pushWarningEmitted = true; + getLogger().log(Level.WARNING, Constants.PUSH_NOT_SUPPORTED_ERROR, + getClass().getSimpleName()); + } + // Not supported by default for now, sublcasses may override + return false; + } + + /** + * Checks that another {@link VaadinSession} instance is not locked. This is + * internally used by {@link VaadinSession#access(Runnable)} and + * {@link UI#access(Runnable)} to help avoid causing deadlocks. + * + * @since 7.1 + * @param session + * the session that is being locked + * @throws IllegalStateException + * 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()) { + throw new IllegalStateException( + "Can't access session while another session is locked by the same thread. This restriction is intended to help avoid deadlocks."); + } + } + + /** + * 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. + * <p> + * This protection is enabled by default, but it might need to be disabled + * to allow a certain type of testing. For these cases, the check can be + * disabled by setting the init parameter + * {@value Constants#SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION} to + * <code>true</code>. + * + * @see DeploymentConfiguration#isXsrfProtectionEnabled() + * + * @since 7.1 + * + * @param session + * the vaadin session for which the check should be done + * @param requestToken + * the CSRF token provided in the request + * @return <code>true</code> if the token is valid or if the protection is + * disabled; <code>false</code> if protection is enabled and the + * token is invalid + */ + public static boolean isCsrfTokenValid(VaadinSession session, + String requestToken) { + + if (session.getService().getDeploymentConfiguration() + .isXsrfProtectionEnabled()) { + String sessionToken = session.getCsrfToken(); + + if (sessionToken == null || !sessionToken.equals(requestToken)) { + return false; + } + } + return true; + } + } diff --git a/server/src/com/vaadin/server/VaadinServlet.java b/server/src/com/vaadin/server/VaadinServlet.java index 35d5fd7cc1..de074941c1 100644 --- a/server/src/com/vaadin/server/VaadinServlet.java +++ b/server/src/com/vaadin/server/VaadinServlet.java @@ -24,7 +24,6 @@ import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; -import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; @@ -35,43 +34,18 @@ import java.util.logging.Logger; import javax.servlet.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.vaadin.sass.internal.ScssStylesheet; -import com.vaadin.server.AbstractCommunicationManager.Callback; -import com.vaadin.shared.ApplicationConstants; -import com.vaadin.ui.UI; +import com.vaadin.server.communication.ServletUIInitHandler; +import com.vaadin.shared.JsonConstants; import com.vaadin.util.CurrentInstance; @SuppressWarnings("serial") public class VaadinServlet extends HttpServlet implements Constants { - private static class AbstractApplicationServletWrapper implements Callback { - - private final VaadinServlet servlet; - - public AbstractApplicationServletWrapper(VaadinServlet servlet) { - this.servlet = servlet; - } - - @Override - public void criticalNotification(VaadinRequest request, - VaadinResponse response, String cap, String msg, - String details, String outOfSyncURL) throws IOException { - servlet.criticalNotification((VaadinServletRequest) request, - ((VaadinServletResponse) response), cap, msg, details, - outOfSyncURL); - } - } - - // TODO Move some (all?) of the constants to a separate interface (shared - // with portlet) - - private final String resourcePath = null; - private VaadinServletService servletService; /** @@ -110,7 +84,11 @@ public class VaadinServlet extends HttpServlet implements Constants { } DeploymentConfiguration deploymentConfiguration = createDeploymentConfiguration(initParameters); - servletService = createServletService(deploymentConfiguration); + try { + servletService = createServletService(deploymentConfiguration); + } catch (ServiceException e) { + throw new ServletException("Could not initialize VaadinServlet", e); + } // Sets current service even though there are no request and response servletService.setCurrentInstances(null, null); @@ -168,8 +146,12 @@ public class VaadinServlet extends HttpServlet implements Constants { } protected VaadinServletService createServletService( - DeploymentConfiguration deploymentConfiguration) { - return new VaadinServletService(this, deploymentConfiguration); + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { + VaadinServletService service = new VaadinServletService(this, + deploymentConfiguration); + service.init(); + return service; } /** @@ -198,7 +180,23 @@ public class VaadinServlet extends HttpServlet implements Constants { } CurrentInstance.clearAll(); setCurrent(this); - service(createVaadinRequest(request), createVaadinResponse(response)); + + VaadinServletRequest vaadinRequest = createVaadinRequest(request); + VaadinServletResponse vaadinResponse = createVaadinResponse(response); + if (!ensureCookiesEnabled(vaadinRequest, vaadinResponse)) { + return; + } + + if (isStaticResourceRequest(request)) { + serveStaticResources(request, response); + return; + } + try { + getService().handleRequest(vaadinRequest, vaadinResponse); + } catch (ServiceException e) { + throw new ServletException(e); + } + } /** @@ -218,7 +216,7 @@ public class VaadinServlet extends HttpServlet implements Constants { */ protected boolean handleContextRootWithoutSlash(HttpServletRequest request, HttpServletResponse response) throws IOException { - if ("/".equals(request.getPathInfo()) + if ((request.getPathInfo() == null || "/".equals(request.getPathInfo())) && "".equals(request.getServletPath()) && !request.getRequestURI().endsWith("/")) { /* @@ -237,119 +235,6 @@ public class VaadinServlet extends HttpServlet implements Constants { } } - private void service(VaadinServletRequest request, - VaadinServletResponse response) throws ServletException, - IOException { - RequestTimer requestTimer = new RequestTimer(); - requestTimer.start(); - - getService().setCurrentInstances(request, response); - - AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper( - this); - - RequestType requestType = getRequestType(request); - if (!ensureCookiesEnabled(requestType, request, response)) { - return; - } - - if (requestType == RequestType.STATIC_FILE) { - serveStaticResources(request, response); - return; - } - - VaadinSession vaadinSession = null; - - try { - // If a duplicate "close application" URL is received for an - // application that is not open, redirect to the application's main - // page. - // This is needed as e.g. Spring Security remembers the last - // URL from the application, which is the logout URL, and repeats - // it. - // We can tell apart a real onunload request from a repeated one - // based on the real one having content (at least the UIDL security - // key). - if (requestType == RequestType.UIDL - && request.getParameterMap().containsKey( - ApplicationConstants.PARAM_UNLOADBURST) - && request.getContentLength() < 1 - && getService().getExistingSession(request, false) == null) { - redirectToApplication(request, response); - return; - } - - // Find out the service session this request is related to - vaadinSession = getService().findVaadinSession(request); - if (vaadinSession == null) { - return; - } - - CommunicationManager communicationManager = (CommunicationManager) vaadinSession - .getCommunicationManager(); - - if (requestType == RequestType.PUBLISHED_FILE) { - communicationManager.servePublishedFile(request, response); - return; - } else if (requestType == RequestType.HEARTBEAT) { - communicationManager.handleHeartbeatRequest(request, response, - vaadinSession); - return; - } - - /* Update browser information from the request */ - vaadinSession.getBrowser().updateRequestDetails(request); - - /* Handle the request */ - if (requestType == RequestType.FILE_UPLOAD) { - // UI is resolved in communication manager - communicationManager.handleFileUpload(vaadinSession, request, - response); - return; - } else if (requestType == RequestType.UIDL) { - UI uI = getService().findUI(request); - if (uI == null) { - throw new ServletException(ERROR_NO_UI_FOUND); - } - // Handles AJAX UIDL requests - communicationManager.handleUidlRequest(request, response, - servletWrapper, uI); - - // Ensure that the browser does not cache UIDL responses. - // iOS 6 Safari requires this (#9732) - response.setHeader("Cache-Control", "no-cache"); - - return; - } else if (requestType == RequestType.BROWSER_DETAILS) { - // Browser details - not related to a specific UI - communicationManager.handleBrowserDetailsRequest(request, - response, vaadinSession); - return; - } - - if (communicationManager.handleOtherRequest(request, response)) { - return; - } - - // Request not handled by any RequestHandler -> 404 - response.sendError(HttpServletResponse.SC_NOT_FOUND); - - } catch (final SessionExpiredException e) { - // Session has expired, notify user - handleServiceSessionExpired(request, response); - } catch (final GeneralSecurityException e) { - handleServiceSecurityException(request, response); - } catch (final Throwable e) { - handleServiceException(request, response, vaadinSession, e); - } finally { - if (vaadinSession != null) { - getService().cleanupSession(vaadinSession); - requestTimer.stop(vaadinSession); - } - CurrentInstance.clearAll(); - } - } - private VaadinServletResponse createVaadinResponse( HttpServletResponse response) { return new VaadinServletResponse(response, getService()); @@ -391,10 +276,9 @@ public class VaadinServlet extends HttpServlet implements Constants { * @return false if cookies are disabled, true otherwise * @throws IOException */ - private boolean ensureCookiesEnabled(RequestType requestType, - VaadinServletRequest request, VaadinServletResponse response) - throws IOException { - if (requestType == RequestType.UIDL) { + private boolean ensureCookiesEnabled(VaadinServletRequest request, + VaadinServletResponse response) throws IOException { + if (ServletPortletHelper.isUIDLRequest(request)) { // In all other but the first UIDL request a cookie should be // returned by the browser. // This can be removed if cookieless mode (#3228) is supported @@ -403,10 +287,13 @@ public class VaadinServlet extends HttpServlet implements Constants { SystemMessages systemMessages = getService().getSystemMessages( ServletPortletHelper.findLocale(null, null, request), request); - criticalNotification(request, response, - systemMessages.getCookiesDisabledCaption(), - systemMessages.getCookiesDisabledMessage(), null, - systemMessages.getCookiesDisabledURL()); + getService().writeStringResponse( + response, + JsonConstants.JSON_CONTENT_TYPE, + VaadinService.createCriticalNotificationJSON( + systemMessages.getCookiesDisabledCaption(), + systemMessages.getCookiesDisabledMessage(), + null, systemMessages.getCookiesDisabledURL())); return false; } } @@ -437,39 +324,19 @@ public class VaadinServlet extends HttpServlet implements Constants { * @throws IOException * if the writing failed due to input/output error. * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @deprecated As of 7.0. This method is retained only for backwards + * compatibility and for {@link GAEVaadinServlet}. */ @Deprecated protected void criticalNotification(VaadinServletRequest request, - HttpServletResponse response, String caption, String message, + VaadinServletResponse response, String caption, String message, String details, String url) throws IOException { if (ServletPortletHelper.isUIDLRequest(request)) { - - if (caption != null) { - caption = "\"" + JsonPaintTarget.escapeJSON(caption) + "\""; - } - if (details != null) { - if (message == null) { - message = details; - } else { - message += "<br/><br/>" + details; - } - } - - if (message != null) { - message = "\"" + JsonPaintTarget.escapeJSON(message) + "\""; - } - if (url != null) { - url = "\"" + JsonPaintTarget.escapeJSON(url) + "\""; - } - - String output = "for(;;);[{\"changes\":[], \"meta\" : {" - + "\"appError\": {" + "\"caption\":" + caption + "," - + "\"message\" : " + message + "," + "\"url\" : " + url - + "}}, \"resources\": {}, \"locales\":[]}]"; - writeResponse(response, "application/json; charset=UTF-8", output); + String output = VaadinService.createCriticalNotificationJSON( + caption, message, details, url); + getService().writeStringResponse(response, + JsonConstants.JSON_CONTENT_TYPE, output); } else { // Create an HTML reponse with the error String output = ""; @@ -492,10 +359,9 @@ public class VaadinServlet extends HttpServlet implements Constants { if (url != null) { output += "</a>"; } - writeResponse(response, "text/html; charset=UTF-8", output); - + getService().writeStringResponse(response, + "text/html; charset=UTF-8", output); } - } /** @@ -511,7 +377,7 @@ public class VaadinServlet extends HttpServlet implements Constants { private void writeResponse(HttpServletResponse response, String contentType, String output) throws IOException { response.setContentType(contentType); - final ServletOutputStream out = response.getOutputStream(); + final OutputStream out = response.getOutputStream(); // Set the response type final PrintWriter outWriter = new PrintWriter(new BufferedWriter( new OutputStreamWriter(out, "UTF-8"))); @@ -555,33 +421,6 @@ public class VaadinServlet extends HttpServlet implements Constants { return resultPath; } - private void handleServiceException(VaadinServletRequest request, - VaadinServletResponse response, VaadinSession vaadinSession, - Throwable e) throws IOException, ServletException { - ErrorHandler errorHandler = ErrorEvent.findErrorHandler(vaadinSession); - - // if this was an UIDL request, response UIDL back to client - if (getRequestType(request) == RequestType.UIDL) { - SystemMessages ci = getService().getSystemMessages( - ServletPortletHelper.findLocale(null, vaadinSession, - request), request); - criticalNotification(request, response, - ci.getInternalErrorCaption(), ci.getInternalErrorMessage(), - null, ci.getInternalErrorURL()); - if (errorHandler != null) { - errorHandler.error(new ErrorEvent(e)); - } - } else { - if (errorHandler != null) { - errorHandler.error(new ErrorEvent(e)); - } - - // Re-throw other exceptions - throw new ServletException(e); - } - - } - /** * A helper method to strip away characters that might somehow be used for * XSS attacs. Leaves at least alphanumeric characters intact. Also removes @@ -626,74 +465,9 @@ public class VaadinServlet extends HttpServlet implements Constants { return DEFAULT_THEME_NAME; } - /** - * @param request - * @param response - * @throws IOException - * @throws ServletException - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version - */ - @Deprecated - void handleServiceSessionExpired(VaadinServletRequest request, - VaadinServletResponse response) throws IOException, - ServletException { - - if (isOnUnloadRequest(request)) { - /* - * Request was an unload request (e.g. window close event) and the - * client expects no response if it fails. - */ - return; - } - - try { - SystemMessages ci = getService().getSystemMessages( - ServletPortletHelper.findLocale(null, null, request), - request); - RequestType requestType = getRequestType(request); - if (requestType == RequestType.UIDL) { - /* - * Invalidate session (weird to have session if we're saying - * that it's expired, and worse: portal integration will fail - * since the session is not created by the portal. - * - * Session must be invalidated before criticalNotification as it - * commits the response. - */ - request.getSession().invalidate(); - - // send uidl redirect - criticalNotification(request, response, - ci.getSessionExpiredCaption(), - ci.getSessionExpiredMessage(), null, - ci.getSessionExpiredURL()); - - } else if (requestType == RequestType.HEARTBEAT) { - response.sendError(HttpServletResponse.SC_GONE, - "Session expired"); - } else { - // 'plain' http req - e.g. browser reload; - // just go ahead redirect the browser - response.sendRedirect(ci.getSessionExpiredURL()); - } - } catch (SystemMessageException ee) { - throw new ServletException(ee); - } - - } - private void handleServiceSecurityException(VaadinServletRequest request, VaadinServletResponse response) throws IOException, ServletException { - if (isOnUnloadRequest(request)) { - /* - * Request was an unload request (e.g. window close event) and the - * client expects no response if it fails. - */ - return; - } try { /* @@ -702,20 +476,17 @@ public class VaadinServlet extends HttpServlet implements Constants { */ SystemMessages ci = getService().getSystemMessages( request.getLocale(), request); - RequestType requestType = getRequestType(request); - if (requestType == RequestType.UIDL) { + if (ServletPortletHelper.isUIDLRequest(request)) { // send uidl redirect - criticalNotification(request, response, - ci.getCommunicationErrorCaption(), - ci.getCommunicationErrorMessage(), - INVALID_SECURITY_KEY_MSG, ci.getCommunicationErrorURL()); - /* - * Invalidate session. Portal integration will fail otherwise - * since the session is not created by the portal. - */ - request.getSession().invalidate(); - - } else if (requestType == RequestType.HEARTBEAT) { + getService().writeStringResponse( + response, + JsonConstants.JSON_CONTENT_TYPE, + VaadinService.createCriticalNotificationJSON( + ci.getCommunicationErrorCaption(), + ci.getCommunicationErrorMessage(), + INVALID_SECURITY_KEY_MSG, + ci.getCommunicationErrorURL())); + } else if (ServletPortletHelper.isHeartbeatRequest(request)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden"); } else { @@ -744,9 +515,8 @@ public class VaadinServlet extends HttpServlet implements Constants { private boolean serveStaticResources(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - // FIXME What does 10 refer to? String pathInfo = request.getPathInfo(); - if (pathInfo == null || pathInfo.length() <= 10) { + if (pathInfo == null) { return false; } @@ -1118,10 +888,12 @@ public class VaadinServlet extends HttpServlet implements Constants { /** * * @author Vaadin Ltd - * @since 7.0.0 + * @since 7.0 * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @deprecated As of 7.0. This is no longer used and only provided for + * backwards compatibility. Each {@link RequestHandler} can + * individually decide whether it wants to handle a request or + * not. */ @Deprecated protected enum RequestType { @@ -1132,8 +904,10 @@ public class VaadinServlet extends HttpServlet implements Constants { * @param request * @return * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @deprecated As of 7.0. This is no longer used and only provided for + * backwards compatibility. Each {@link RequestHandler} can + * individually decide whether it wants to handle a request or + * not. */ @Deprecated protected RequestType getRequestType(VaadinServletRequest request) { @@ -1141,7 +915,7 @@ public class VaadinServlet extends HttpServlet implements Constants { return RequestType.FILE_UPLOAD; } else if (ServletPortletHelper.isPublishedFileRequest(request)) { return RequestType.PUBLISHED_FILE; - } else if (isBrowserDetailsRequest(request)) { + } else if (ServletUIInitHandler.isUIInitRequest(request)) { return RequestType.BROWSER_DETAILS; } else if (ServletPortletHelper.isUIDLRequest(request)) { return RequestType.UIDL; @@ -1156,14 +930,9 @@ public class VaadinServlet extends HttpServlet implements Constants { } - private static boolean isBrowserDetailsRequest(HttpServletRequest request) { - return "POST".equals(request.getMethod()) - && request.getParameter("v-browserDetails") != null; - } - - private boolean isStaticResourceRequest(HttpServletRequest request) { + protected boolean isStaticResourceRequest(HttpServletRequest request) { String pathInfo = request.getPathInfo(); - if (pathInfo == null || pathInfo.length() <= 10) { + if (pathInfo == null) { return false; } @@ -1178,10 +947,6 @@ public class VaadinServlet extends HttpServlet implements Constants { return false; } - private boolean isOnUnloadRequest(HttpServletRequest request) { - return request.getParameter(ApplicationConstants.PARAM_UNLOADBURST) != null; - } - /** * Remove any heading or trailing "what" from the "string". * @@ -1301,4 +1066,5 @@ public class VaadinServlet extends HttpServlet implements Constants { private static final Logger getLogger() { return Logger.getLogger(VaadinServlet.class.getName()); } + } diff --git a/server/src/com/vaadin/server/VaadinServletService.java b/server/src/com/vaadin/server/VaadinServletService.java index 71f47ea217..3b39f17849 100644 --- a/server/src/com/vaadin/server/VaadinServletService.java +++ b/server/src/com/vaadin/server/VaadinServletService.java @@ -17,19 +17,38 @@ package com.vaadin.server; import java.io.File; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; -import com.vaadin.server.VaadinServlet.RequestType; +import org.atmosphere.util.Version; + +import com.vaadin.server.communication.PushRequestHandler; +import com.vaadin.server.communication.ServletBootstrapHandler; +import com.vaadin.server.communication.ServletUIInitHandler; import com.vaadin.ui.UI; public class VaadinServletService extends VaadinService { private final VaadinServlet servlet; + private final static boolean atmosphereAvailable = checkAtmosphereSupport(); + + /** + * Keeps track of whether a warning about missing push support has already + * been logged. This is used to avoid spamming the log with the same message + * every time a new UI is bootstrapped. + */ + private boolean pushWarningLogged = false; + public VaadinServletService(VaadinServlet servlet, - DeploymentConfiguration deploymentConfiguration) { + DeploymentConfiguration deploymentConfiguration) + throws ServiceException { super(deploymentConfiguration); this.servlet = servlet; @@ -44,7 +63,40 @@ public class VaadinServletService extends VaadinService { } } - protected VaadinServlet getServlet() { + private static boolean checkAtmosphereSupport() { + try { + String rawVersion = Version.getRawVersion(); + if (!Constants.REQUIRED_ATMOSPHERE_VERSION.equals(rawVersion)) { + getLogger().log( + Level.WARNING, + Constants.INVALID_ATMOSPHERE_VERSION_WARNING, + new Object[] { Constants.REQUIRED_ATMOSPHERE_VERSION, + rawVersion }); + } + return true; + } catch (NoClassDefFoundError e) { + return false; + } + } + + @Override + protected List<RequestHandler> createRequestHandlers() + throws ServiceException { + List<RequestHandler> handlers = super.createRequestHandlers(); + handlers.add(0, new ServletBootstrapHandler()); + handlers.add(new ServletUIInitHandler()); + if (atmosphereAvailable) { + handlers.add(new PushRequestHandler(this)); + } + return handlers; + } + + /** + * Retrieves a reference to the servlet associated with this service. + * + * @return A reference to the VaadinServlet this service is using + */ + public VaadinServlet getServlet() { return servlet; } @@ -127,12 +179,11 @@ public class VaadinServletService extends VaadinService { @Override protected boolean requestCanCreateSession(VaadinRequest request) { - RequestType requestType = getRequestType(request); - if (requestType == RequestType.BROWSER_DETAILS) { + if (ServletUIInitHandler.isUIInitRequest(request)) { // This is the first request if you are embedding by writing the // embedding code yourself return true; - } else if (requestType == RequestType.OTHER) { + } else if (isOtherRequest(request)) { /* * I.e URIs that are not RPC calls or static (theme) files. */ @@ -142,25 +193,16 @@ public class VaadinServletService extends VaadinService { return false; } - /** - * Gets the request type for the request. - * - * @param request - * the request to get a request type for - * @return the request type - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version - */ - @Deprecated - protected RequestType getRequestType(VaadinRequest request) { - RequestType type = (RequestType) request.getAttribute(RequestType.class - .getName()); - if (type == null) { - type = getServlet().getRequestType((VaadinServletRequest) request); - request.setAttribute(RequestType.class.getName(), type); - } - return type; + private boolean isOtherRequest(VaadinRequest request) { + // TODO This should be refactored in some way. It should not be + // necessary to check all these types. + return (!ServletPortletHelper.isAppRequest(request) + && !ServletUIInitHandler.isUIInitRequest(request) + && !ServletPortletHelper.isFileUploadRequest(request) + && !ServletPortletHelper.isHeartbeatRequest(request) + && !ServletPortletHelper.isPublishedFileRequest(request) + && !ServletPortletHelper.isUIDLRequest(request) && !ServletPortletHelper + .isPushRequest(request)); } @Override @@ -169,12 +211,6 @@ public class VaadinServletService extends VaadinService { return getServlet().getApplicationUrl((VaadinServletRequest) request); } - @Override - protected AbstractCommunicationManager createCommunicationManager( - VaadinSession session) { - return new CommunicationManager(session); - } - public static HttpServletRequest getCurrentServletRequest() { VaadinRequest currentRequest = VaadinService.getCurrentRequest(); if (currentRequest instanceof VaadinServletRequest) { @@ -194,6 +230,18 @@ public class VaadinServletService extends VaadinService { } @Override + public InputStream getThemeResourceAsStream(UI uI, String themeName, + String resource) { + VaadinServletService service = (VaadinServletService) uI.getSession() + .getService(); + ServletContext servletContext = service.getServlet() + .getServletContext(); + return servletContext.getResourceAsStream("/" + + VaadinServlet.THEME_DIR_PATH + '/' + themeName + "/" + + resource); + } + + @Override public String getMainDivId(VaadinSession session, VaadinRequest request, Class<? extends UI> uiClass) { String appId = null; @@ -220,4 +268,22 @@ public class VaadinServletService extends VaadinService { appId = appId + "-" + hashCode; return appId; } + + private static final Logger getLogger() { + return Logger.getLogger(VaadinServletService.class.getName()); + } + + @Override + public boolean ensurePushAvailable() { + if (atmosphereAvailable) { + return true; + } else { + if (!pushWarningLogged) { + pushWarningLogged = true; + getLogger().log(Level.WARNING, + Constants.ATMOSPHERE_MISSING_ERROR); + } + return false; + } + } } diff --git a/server/src/com/vaadin/server/VaadinSession.java b/server/src/com/vaadin/server/VaadinSession.java index d3619ebabf..317ea6cf7b 100644 --- a/server/src/com/vaadin/server/VaadinSession.java +++ b/server/src/com/vaadin/server/VaadinSession.java @@ -25,6 +25,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.UUID; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; @@ -39,6 +40,7 @@ import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterFactory; import com.vaadin.data.util.converter.DefaultConverterFactory; import com.vaadin.event.EventRouter; +import com.vaadin.shared.communication.PushMode; import com.vaadin.ui.AbstractField; import com.vaadin.ui.Table; import com.vaadin.ui.UI; @@ -73,8 +75,6 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { .findMethod(BootstrapListener.class, "modifyBootstrapPage", BootstrapPageResponse.class); - private final Lock lock = new ReentrantLock(); - /** * Configuration for the session. */ @@ -110,7 +110,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { protected WebBrowser browser = new WebBrowser(); - private AbstractCommunicationManager communicationManager; + private LegacyCommunicationManager communicationManager; private long cumulativeRequestDuration = 0; @@ -128,6 +128,8 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { private transient VaadinService service; + private transient Lock lock; + /** * Create a new service session tied to a Vaadin service * @@ -162,6 +164,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { + "This might happen if a session is deserialized but never used before it expires."); } else if (VaadinService.getCurrentRequest() != null && getCurrent() == this) { + assert hasLock(); // Ignore if the session is being moved to a different backing // session if (getAttribute(VaadinService.REINITIALIZING_SESSION_MARKER) == Boolean.TRUE) { @@ -192,6 +195,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ @Deprecated public WebBrowser getBrowser() { + assert hasLock(); return browser; } @@ -200,6 +204,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * milliseconds. */ public long getCumulativeRequestDuration() { + assert hasLock(); return cumulativeRequestDuration; } @@ -211,6 +216,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * The time spent in the last request, in milliseconds. */ public void setLastRequestDuration(long time) { + assert hasLock(); lastRequestDuration = time; cumulativeRequestDuration += time; } @@ -220,6 +226,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * milliseconds. */ public long getLastRequestDuration() { + assert hasLock(); return lastRequestDuration; } @@ -232,6 +239,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * */ public void setLastRequestTimestamp(long timestamp) { + assert hasLock(); lastRequestTimestamp = timestamp; } @@ -242,6 +250,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the epoch. */ public long getLastRequestTimestamp() { + assert hasLock(); return lastRequestTimestamp; } @@ -252,6 +261,11 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return the wrapped session for this context */ public WrappedSession getSession() { + /* + * This is used to fetch the underlying session and there is no need for + * having a lock when doing this. On the contrary this is sometimes done + * to be able to lock the session. + */ return session; } @@ -262,65 +276,97 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * version */ @Deprecated - public AbstractCommunicationManager getCommunicationManager() { + public LegacyCommunicationManager getCommunicationManager() { + assert hasLock(); return communicationManager; } /** + * Loads the VaadinSession for the given service and WrappedSession from the + * HTTP session. + * * @param service - * TODO + * The service the VaadinSession is associated with * @param underlyingSession - * @return - * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * The wrapped HTTP session for the user + * @return A VaadinSession instance for the service, session combination or + * null if none was found. + * @deprecated As of 7.0. Should be moved to a separate session storage + * class some day. */ @Deprecated public static VaadinSession getForSession(VaadinService service, WrappedSession underlyingSession) { - Object attribute = underlyingSession.getAttribute(VaadinSession.class - .getName() + "." + service.getServiceName()); - if (attribute instanceof VaadinSession) { - VaadinSession vaadinSession = (VaadinSession) attribute; - vaadinSession.session = underlyingSession; - vaadinSession.service = service; - return vaadinSession; + assert hasLock(service, underlyingSession); + + VaadinSession vaadinSession = (VaadinSession) underlyingSession + .getAttribute(getSessionAttributeName(service)); + if (vaadinSession == null) { + return null; } - return null; + vaadinSession.session = underlyingSession; + vaadinSession.service = service; + vaadinSession.refreshLock(); + return vaadinSession; } /** + * Removes this VaadinSession from the HTTP session. * * @param service - * TODO - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * The service this session is associated with + * @deprecated As of 7.0. Should be moved to a separate session storage + * class some day. */ @Deprecated public void removeFromSession(VaadinService service) { - assert (getForSession(service, session) == this); - session.setAttribute( - VaadinSession.class.getName() + "." + service.getServiceName(), - null); + assert hasLock(); + session.removeAttribute(getSessionAttributeName(service)); } /** - * @param session + * Retrieves the name of the attribute used for storing a VaadinSession for + * the given service. * - * @deprecated As of 7.0. Will likely change or be removed in a future - * version + * @param service + * The service associated with the sessio + * @return The attribute name used for storing the session + */ + private static String getSessionAttributeName(VaadinService service) { + return VaadinSession.class.getName() + "." + service.getServiceName(); + } + + /** + * Stores this VaadinSession in the HTTP session. + * + * @param service + * The service this session is associated with + * @param session + * The HTTP session this VaadinSession should be stored in + * @deprecated As of 7.0. Should be moved to a separate session storage + * class some day. */ @Deprecated public void storeInSession(VaadinService service, WrappedSession session) { - session.setAttribute( - VaadinSession.class.getName() + "." + service.getServiceName(), - this); + assert hasLock(service, session); + session.setAttribute(getSessionAttributeName(service), this); this.session = session; + refreshLock(); + } + + /** + * Updates the transient session lock from VaadinService. + */ + private void refreshLock() { + assert lock == null || lock == service.getSessionLock(session) : "Cannot change the lock from one instance to another"; + assert hasLock(service, session); + lock = service.getSessionLock(session); } public void setCommunicationManager( - AbstractCommunicationManager communicationManager) { + LegacyCommunicationManager communicationManager) { + assert hasLock(); if (communicationManager == null) { throw new IllegalArgumentException("Can not set to null"); } @@ -329,6 +375,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { } public void setConfiguration(DeploymentConfiguration configuration) { + assert hasLock(); if (configuration == null) { throw new IllegalArgumentException("Can not set to null"); } @@ -342,6 +389,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return the deployment configuration */ public DeploymentConfiguration getConfiguration() { + assert hasLock(); return configuration; } @@ -354,6 +402,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return the locale of this session. */ public Locale getLocale() { + assert hasLock(); if (locale != null) { return locale; } @@ -371,6 +420,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * */ public void setLocale(Locale locale) { + assert hasLock(); this.locale = locale; } @@ -380,6 +430,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return the current error handler */ public ErrorHandler getErrorHandler() { + assert hasLock(); return errorHandler; } @@ -389,6 +440,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @param errorHandler */ public void setErrorHandler(ErrorHandler errorHandler) { + assert hasLock(); this.errorHandler = errorHandler; } @@ -401,6 +453,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return The converter factory used in the session */ public ConverterFactory getConverterFactory() { + assert hasLock(); return converterFactory; } @@ -426,6 +479,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * The converter factory used in the session */ public void setConverterFactory(ConverterFactory converterFactory) { + assert hasLock(); this.converterFactory = converterFactory; } @@ -446,6 +500,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @since 7.0 */ public void addRequestHandler(RequestHandler handler) { + assert hasLock(); requestHandlers.addFirst(handler); } @@ -458,6 +513,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @since 7.0 */ public void removeRequestHandler(RequestHandler handler) { + assert hasLock(); requestHandlers.remove(handler); } @@ -475,6 +531,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @since 7.0 */ public Collection<RequestHandler> getRequestHandlers() { + assert hasLock(); return Collections.unmodifiableCollection(requestHandlers); } @@ -528,11 +585,14 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @since 7.0 */ public Collection<UI> getUIs() { + assert hasLock(); return Collections.unmodifiableCollection(uIs.values()); } private int connectorIdSequence = 0; + private final String csrfToken = UUID.randomUUID().toString(); + /** * Generate an id for the given Connector. Connectors must not call this * method more than once, the first time they need an id. @@ -546,6 +606,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ @Deprecated public String createConnectorId(ClientConnector connector) { + assert hasLock(); return String.valueOf(connectorIdSequence++); } @@ -560,10 +621,32 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return The UI with the given id or null if not found */ public UI getUIById(int uiId) { + assert hasLock(); return uIs.get(uiId); } /** + * Checks if the current thread has exclusive access to this VaadinSession + * + * @return true if the thread has exclusive access, false otherwise + */ + public boolean hasLock() { + ReentrantLock l = ((ReentrantLock) getLockInstance()); + return l.isHeldByCurrentThread(); + } + + /** + * Checks if the current thread has exclusive access to the given + * WrappedSession. + * + * @return true if this thread has exclusive access, false otherwise + */ + private static boolean hasLock(VaadinService service, WrappedSession session) { + ReentrantLock l = (ReentrantLock) service.getSessionLock(session); + return l.isHeldByCurrentThread(); + } + + /** * Adds a listener that will be invoked when the bootstrap HTML is about to * be generated. This can be used to modify the contents of the HTML that * loads the Vaadin application in the browser and the HTTP headers that are @@ -576,6 +659,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the bootstrap listener to add */ public void addBootstrapListener(BootstrapListener listener) { + assert hasLock(); eventRouter.addListener(BootstrapFragmentResponse.class, listener, BOOTSTRAP_FRAGMENT_METHOD); eventRouter.addListener(BootstrapPageResponse.class, listener, @@ -591,6 +675,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the bootstrap listener to remove */ public void removeBootstrapListener(BootstrapListener listener) { + assert hasLock(); eventRouter.removeListener(BootstrapFragmentResponse.class, listener, BOOTSTRAP_FRAGMENT_METHOD); eventRouter.removeListener(BootstrapPageResponse.class, listener, @@ -611,6 +696,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { */ @Deprecated public void modifyBootstrapResponse(BootstrapResponse response) { + assert hasLock(); eventRouter.fireEvent(response); } @@ -622,6 +708,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the UI to remove */ public void removeUI(UI ui) { + assert hasLock(); int id = ui.getUIId(); ui.setSession(null); uIs.remove(id); @@ -646,6 +733,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @since 7.0.0 */ public GlobalResourceHandler getGlobalResourceHandler(boolean createOnDemand) { + assert hasLock(); if (globalResourceHandler == null && createOnDemand) { globalResourceHandler = new GlobalResourceHandler(); addRequestHandler(globalResourceHandler); @@ -677,9 +765,24 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { /** * Locks this session to protect its data from concurrent access. Accessing * the UI state from outside the normal request handling should always lock - * the session and unlock it when done. To ensure that the lock is always - * released, you should typically wrap the code in a <code>try</code> block - * and unlock the session in <code>finally</code>: + * the session and unlock it when done. The preferred way to ensure locking + * is done correctly is to wrap your code using {@link UI#access(Runnable)} + * (or {@link VaadinSession#access(Runnable)} if you are only touching the + * session and not any UI), e.g.: + * + * <pre> + * myUI.access(new Runnable() { + * @Override + * public void run() { + * // Here it is safe to update the UI. + * // UI.getCurrent can also be used + * myUI.getContent().setCaption("Changed safely"); + * } + * }); + * </pre> + * + * If you for whatever reason want to do locking manually, you should do it + * like: * * <pre> * session.lock(); @@ -689,7 +792,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * session.unlock(); * } * </pre> - * <p> + * * This method will block until the lock can be retrieved. * <p> * {@link #getLockInstance()} can be used if more control over the locking @@ -697,6 +800,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * * @see #unlock() * @see #getLockInstance() + * @see #hasLock() */ public void lock() { getLockInstance().lock(); @@ -705,11 +809,29 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { /** * Unlocks this session. This method should always be used in a finally * block after {@link #lock()} to ensure that the lock is always released. + * <p> + * If {@link #getPushMode() the push mode} is {@link PushMode#AUTOMATIC + * automatic}, pushes the changes in all UIs in this session to their + * respective clients. * - * @see #unlock() + * @see #lock() + * @see UI#push() */ public void unlock() { - getLockInstance().unlock(); + assert hasLock(); + try { + if (((ReentrantLock) getLockInstance()).getHoldCount() == 1) { + // Only push if the reentrant lock will actually be released by + // this unlock() invocation. + for (UI ui : getUIs()) { + if (ui.getPushMode() == PushMode.AUTOMATIC) { + ui.push(); + } + } + } + } finally { + getLockInstance().unlock(); + } } /** @@ -728,6 +850,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * remove a previous association. */ public void setAttribute(String name, Object value) { + assert hasLock(); if (name == null) { throw new IllegalArgumentException("name can not be null"); } @@ -759,6 +882,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * remove a previous association. */ public <T> void setAttribute(Class<T> type, T value) { + assert hasLock(); if (type == null) { throw new IllegalArgumentException("type can not be null"); } @@ -783,6 +907,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * it has been set to null. */ public Object getAttribute(String name) { + assert hasLock(); if (name == null) { throw new IllegalArgumentException("name can not be null"); } @@ -808,6 +933,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * it has been set to null. */ public <T> T getAttribute(Class<T> type) { + assert hasLock(); if (type == null) { throw new IllegalArgumentException("type can not be null"); } @@ -825,6 +951,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return a unique UI id */ public int getNextUIid() { + assert hasLock(); return nextUIId++; } @@ -838,6 +965,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return the mapping between window names and UI ids for this session. */ public Map<String, Integer> getPreserveOnRefreshUIs() { + assert hasLock(); return retainOnRefreshUIs; } @@ -848,6 +976,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the initialized UI to add. */ public void addUI(UI ui) { + assert hasLock(); if (ui.getUIId() == -1) { throw new IllegalArgumentException( "Can not add an UI that has not been initialized."); @@ -867,6 +996,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the UI provider that should be added */ public void addUIProvider(UIProvider uiProvider) { + assert hasLock(); uiProviders.addFirst(uiProvider); } @@ -877,6 +1007,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * the UI provider that should be removed */ public void removeUIProvider(UIProvider uiProvider) { + assert hasLock(); uiProviders.remove(uiProvider); } @@ -886,6 +1017,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * @return an unmodifiable list of UI providers */ public List<UIProvider> getUIProviders() { + assert hasLock(); return Collections.unmodifiableList(uiProviders); } @@ -910,6 +1042,7 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * */ public void close() { + assert hasLock(); closing = true; } @@ -918,13 +1051,84 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable { * * @see #close() * - * @return + * @return true if this session is marked to be closed, false otherwise */ public boolean isClosing() { + assert hasLock(); return closing; } private static final Logger getLogger() { return Logger.getLogger(VaadinSession.class.getName()); } + + /** + * Provides exclusive access to this session from outside a request handling + * thread. + * <p> + * The given runnable is executed while holding the session lock to ensure + * exclusive access to this session. The session and related thread locals + * are set properly before executing the runnable. + * </p> + * <p> + * RPC handlers for components inside this session do not need this method + * as the session is automatically locked by the framework during request + * handling. + * </p> + * <p> + * Note that calling this method while another session is locked by the + * current thread will cause an exception. This is to prevent deadlock + * situations when two threads have locked one session each and are both + * waiting for the lock for the other session. + * </p> + * + * @param runnable + * the runnable which accesses the session + * + * @throws IllegalStateException + * if the current thread holds the lock for another session + * + * + * @see #lock() + * @see #getCurrent() + * @see UI#access(Runnable) + */ + public void access(Runnable runnable) { + VaadinService.verifyNoOtherSessionLocked(this); + + Map<Class<?>, CurrentInstance> old = null; + lock(); + try { + old = CurrentInstance.setThreadLocals(this); + runnable.run(); + } finally { + unlock(); + if (old != null) { + CurrentInstance.restoreThreadLocals(old); + } + } + + } + + /** + * @deprecated As of 7.1.0.beta1, use {@link #access(Runnable)} instead. + * This method will be removed before the final 7.1.0 release. + */ + @Deprecated + public void runSafely(Runnable runnable) { + access(runnable); + } + + /** + * Gets the CSRF token (aka double submit cookie) that is used to protect + * against Cross Site Request Forgery attacks. + * + * @since 7.1 + * @return the csrf token string + */ + public String getCsrfToken() { + assert hasLock(); + return csrfToken; + } + } diff --git a/server/src/com/vaadin/server/WebBrowser.java b/server/src/com/vaadin/server/WebBrowser.java index 4122f053ae..8038bbc207 100644 --- a/server/src/com/vaadin/server/WebBrowser.java +++ b/server/src/com/vaadin/server/WebBrowser.java @@ -319,6 +319,17 @@ public class WebBrowser implements Serializable { * entirely accurate due to varying network latencies, but should provide a * close-enough value for most cases. Also note that the returned Date * object uses servers default time zone, not the clients. + * <p> + * To get the actual date and time shown in the end users computer, you can + * do something like: + * + * <pre> + * WebBrowser browser = ...; + * SimpleTimeZone timeZone = new SimpleTimeZone(browser.getTimezoneOffset(), "Fake client time zone"); + * DateFormat format = DateFormat.getDateTimeInstance(); + * format.setTimeZone(timeZone); + * myLabel.setValue(format.format(browser.getCurrentDate())); + * </pre> * * @return the current date and time of the browser. * @see #isDSTInEffect() @@ -413,7 +424,7 @@ public class WebBrowser implements Serializable { * @param request * the Vaadin request to read the information from */ - void updateRequestDetails(VaadinRequest request) { + public void updateRequestDetails(VaadinRequest request) { locale = request.getLocale(); address = request.getRemoteAddr(); secureConnection = request.isSecure(); diff --git a/server/src/com/vaadin/server/AbstractStreamingEvent.java b/server/src/com/vaadin/server/communication/AbstractStreamingEvent.java index b7bf4e042f..b97a60fd56 100644 --- a/server/src/com/vaadin/server/AbstractStreamingEvent.java +++ b/server/src/com/vaadin/server/communication/AbstractStreamingEvent.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; import com.vaadin.server.StreamVariable.StreamingEvent; diff --git a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java new file mode 100644 index 0000000000..0bba65ff1d --- /dev/null +++ b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java @@ -0,0 +1,247 @@ +/* + * 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.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.atmosphere.cpr.AtmosphereResource; +import org.atmosphere.cpr.AtmosphereResource.TRANSPORT; +import org.json.JSONException; + +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.ui.UI; + +/** + * {@link PushConnection} implementation using the Atmosphere push support that + * is by default included in Vaadin. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class AtmospherePushConnection implements Serializable, PushConnection { + + /** + * Represents a message that can arrive as multiple fragments. + */ + protected static class FragmentedMessage { + private final StringBuilder message = new StringBuilder(); + private final int messageLength; + + public FragmentedMessage(Reader reader) throws IOException { + // Messages are prefixed by the total message length plus '|' + String length = ""; + int c; + while ((c = reader.read()) != -1 + && c != ApplicationConstants.WEBSOCKET_MESSAGE_DELIMITER) { + length += (char) c; + } + try { + messageLength = Integer.parseInt(length); + } catch (NumberFormatException e) { + throw new IOException("Invalid message length " + length, e); + } + } + + /** + * Appends all the data from the given Reader to this message and + * returns whether the message was completed. + * + * @param reader + * The Reader from which to read. + * @return true if this message is complete, false otherwise. + * @throws IOException + */ + public boolean append(Reader reader) throws IOException { + char[] buffer = new char[ApplicationConstants.WEBSOCKET_BUFFER_SIZE]; + int read; + while ((read = reader.read(buffer)) != -1) { + message.append(buffer, 0, read); + assert message.length() <= messageLength : "Received message " + + message.length() + "chars, expected " + messageLength; + } + return message.length() == messageLength; + } + + public Reader getReader() { + return new StringReader(message.toString()); + } + } + + private UI ui; + private transient AtmosphereResource resource; + private transient Future<String> outgoingMessage; + private transient FragmentedMessage incomingMessage; + + public AtmospherePushConnection(UI ui) { + this.ui = ui; + } + + @Override + public void push() { + assert isConnected(); + try { + push(true); + } catch (IOException e) { + // TODO Error handling + throw new RuntimeException("Push failed", e); + } + } + + /** + * Pushes pending state changes and client RPC calls to the client. + * + * @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, false, async); + } catch (JSONException e) { + throw new IOException("Error writing UIDL", e); + } + sendMessage("for(;;);[{" + writer.toString() + "}]"); + } + + /** + * Sends the given message to the current client. + * + * @param message + * The message to send + */ + void sendMessage(String message) { + // "Broadcast" the changes to the single client only + outgoingMessage = getResource().getBroadcaster().broadcast(message, + getResource()); + } + + /** + * Reads and buffers a (possibly partial) message. If a complete message was + * received, or if the call resulted in the completion of a partially + * received message, returns a {@link Reader} yielding the complete message. + * Otherwise, returns null. + * + * @param reader + * A Reader from which to read the (partial) message + * @return A Reader yielding a complete message or null if the message is + * not yet complete. + * @throws IOException + */ + protected Reader receiveMessage(Reader reader) throws IOException { + + if (resource.transport() != TRANSPORT.WEBSOCKET) { + return reader; + } + + if (incomingMessage == null) { + // No existing partially received message + incomingMessage = new FragmentedMessage(reader); + } + + if (incomingMessage.append(reader)) { + // Message is complete + Reader completeReader = incomingMessage.getReader(); + incomingMessage = null; + return completeReader; + } else { + // Only received a partial message + return null; + } + } + + /** + * Associates this connection with the given AtmosphereResource. If there is + * a push pending, commits it. + * + * @param resource + * The AtmosphereResource representing the push channel. + * @throws IOException + */ + protected void connect(AtmosphereResource resource) throws IOException { + this.resource = resource; + } + + /** + * Returns whether this connection is currently open. + */ + @Override + public boolean isConnected() { + return resource != null + && resource.getBroadcaster().getAtmosphereResources() + .contains(resource); + } + + /** + * @return the UI associated with this connection. + */ + protected UI getUI() { + return ui; + } + + /** + * @return The AtmosphereResource associated with this connection or null if + * connection not open. + */ + protected AtmosphereResource getResource() { + return resource; + } + + @Override + public void disconnect() { + if (outgoingMessage != null) { + // Wait for the last message to be sent before closing the + // connection (assumes that futures are completed in order) + try { + outgoingMessage.get(1000, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + getLogger() + .log(Level.INFO, + "Timeout waiting for messages to be sent to client before disconnect"); + } catch (Exception e) { + getLogger() + .log(Level.INFO, + "Error waiting for messages to be sent to client before disconnect"); + } + outgoingMessage = null; + } + + resource.resume(); + assert !resource.getBroadcaster().getAtmosphereResources() + .contains(resource); + resource = null; + } + + /** + * @since + * @return + */ + private static Logger getLogger() { + return Logger.getLogger(AtmospherePushConnection.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/ClientRpcWriter.java b/server/src/com/vaadin/server/communication/ClientRpcWriter.java new file mode 100644 index 0000000000..285adac7a5 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ClientRpcWriter.java @@ -0,0 +1,141 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.ClientMethodInvocation; +import com.vaadin.server.EncodeResult; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.PaintException; +import com.vaadin.shared.communication.ClientRpc; +import com.vaadin.ui.UI; + +/** + * Serializes {@link ClientRpc client RPC} invocations to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ClientRpcWriter implements Serializable { + + /** + * Writes a JSON object containing all pending client RPC invocations in the + * given UI. + * + * @param ui + * The {@link UI} whose RPC calls to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection<ClientMethodInvocation> pendingInvocations = collectPendingRpcCalls(ui + .getConnectorTracker().getDirtyVisibleConnectors()); + + JSONArray rpcCalls = new JSONArray(); + for (ClientMethodInvocation invocation : pendingInvocations) { + // add invocation to rpcCalls + try { + JSONArray invocationJson = new JSONArray(); + invocationJson.put(invocation.getConnector().getConnectorId()); + invocationJson.put(invocation.getInterfaceName()); + invocationJson.put(invocation.getMethodName()); + JSONArray paramJson = new JSONArray(); + for (int i = 0; i < invocation.getParameterTypes().length; ++i) { + Type parameterType = invocation.getParameterTypes()[i]; + Object referenceParameter = null; + // TODO Use default values for RPC parameter types + // if (!JsonCodec.isInternalType(parameterType)) { + // try { + // referenceParameter = parameterType.newInstance(); + // } catch (Exception e) { + // logger.log(Level.WARNING, + // "Error creating reference object for parameter of type " + // + parameterType.getName()); + // } + // } + EncodeResult encodeResult = JsonCodec.encode( + invocation.getParameters()[i], referenceParameter, + parameterType, ui.getConnectorTracker()); + paramJson.put(encodeResult.getEncodedValue()); + } + invocationJson.put(paramJson); + rpcCalls.put(invocationJson); + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize RPC method call parameters for connector " + + invocation.getConnector().getConnectorId() + + " method " + invocation.getInterfaceName() + + "." + invocation.getMethodName() + ": " + + e.getMessage(), e); + } + } + writer.write(rpcCalls.toString()); + } + + /** + * Collects all pending RPC calls from listed {@link ClientConnector}s and + * clears their RPC queues. + * + * @param rpcPendingQueue + * list of {@link ClientConnector} of interest + * @return ordered list of pending RPC calls + */ + private Collection<ClientMethodInvocation> collectPendingRpcCalls( + Collection<ClientConnector> rpcPendingQueue) { + List<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>(); + for (ClientConnector connector : rpcPendingQueue) { + List<ClientMethodInvocation> paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List<ClientMethodInvocation> oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList<ClientMethodInvocation>( + totalCalls); + + // merge two ordered comparable lists + for (int destIndex = 0, oldIndex = 0, paintableIndex = 0; destIndex < totalCalls; destIndex++) { + if (paintableIndex >= paintablePendingRpc.size() + || (oldIndex < oldPendingRpc.size() && ((Comparable<ClientMethodInvocation>) oldPendingRpc + .get(oldIndex)) + .compareTo(paintablePendingRpc + .get(paintableIndex)) <= 0)) { + pendingInvocations.add(oldPendingRpc.get(oldIndex++)); + } else { + pendingInvocations.add(paintablePendingRpc + .get(paintableIndex++)); + } + } + } + } + return pendingInvocations; + } +} diff --git a/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java b/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java new file mode 100644 index 0000000000..467bddbdce --- /dev/null +++ b/server/src/com/vaadin/server/communication/ConnectorHierarchyWriter.java @@ -0,0 +1,81 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.server.PaintException; +import com.vaadin.ui.UI; + +/** + * Serializes a connector hierarchy to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ConnectorHierarchyWriter implements Serializable { + + /** + * Writes a JSON object containing the connector hierarchy (parent-child + * mappings) of the dirty connectors in the given UI. + * + * @param ui + * The {@link UI} whose hierarchy to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection<ClientConnector> dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject hierarchyInfo = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorId = connector.getConnectorId(); + JSONArray children = new JSONArray(); + + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(connector)) { + if (LegacyCommunicationManager + .isConnectorVisibleToClient(child)) { + children.put(child.getConnectorId()); + } + } + try { + hierarchyInfo.put(connectorId, children); + } catch (JSONException e) { + throw new PaintException( + "Failed to send hierarchy information about " + + connectorId + " to the client: " + + e.getMessage(), e); + } + } + writer.write(hierarchyInfo.toString()); + } +} diff --git a/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java b/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java new file mode 100644 index 0000000000..eaa1c83ff2 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ConnectorTypeWriter.java @@ -0,0 +1,73 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.ui.UI; + +/** + * Serializes connector type mappings to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ConnectorTypeWriter implements Serializable { + + /** + * Writes a JSON object containing connector-ID-to-type-ID mappings for each + * dirty Connector in the given UI. + * + * @param ui + * The {@link UI} containing dirty connectors + * @param writer + * The {@link Writer} used to write the JSON. + * @param target + * The paint target containing the connector type IDs. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer, PaintTarget target) + throws IOException { + + Collection<ClientConnector> dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject connectorTypes = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + String connectorType = target.getTag(connector); + try { + connectorTypes.put(connector.getConnectorId(), connectorType); + } catch (JSONException e) { + throw new PaintException( + "Failed to send connector type for connector " + + connector.getConnectorId() + ": " + + e.getMessage(), e); + } + } + writer.write(connectorTypes.toString()); + } +} diff --git a/server/src/com/vaadin/server/communication/FileUploadHandler.java b/server/src/com/vaadin/server/communication/FileUploadHandler.java new file mode 100644 index 0000000000..e875a4e861 --- /dev/null +++ b/server/src/com/vaadin/server/communication/FileUploadHandler.java @@ -0,0 +1,645 @@ +/* + * 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.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.NoInputStreamException; +import com.vaadin.server.NoOutputStreamException; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.StreamVariable; +import com.vaadin.server.StreamVariable.StreamingEndEvent; +import com.vaadin.server.StreamVariable.StreamingErrorEvent; +import com.vaadin.server.UploadException; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; + +/** + * Handles a file upload request submitted via an Upload component. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class FileUploadHandler implements RequestHandler { + + /** + * Stream that extracts content from another stream until the boundary + * string is encountered. + * + * Public only for unit tests, should be considered private for all other + * purposes. + */ + public static class SimpleMultiPartInputStream extends InputStream { + + /** + * Counter of how many characters have been matched to boundary string + * from the stream + */ + int matchedCount = -1; + + /** + * Used as pointer when returning bytes after partly matched boundary + * string. + */ + int curBoundaryIndex = 0; + /** + * The byte found after a "promising start for boundary" + */ + private int bufferedByte = -1; + private boolean atTheEnd = false; + + private final char[] boundary; + + private final InputStream realInputStream; + + public SimpleMultiPartInputStream(InputStream realInputStream, + String boundaryString) { + boundary = (CRLF + DASHDASH + boundaryString).toCharArray(); + this.realInputStream = realInputStream; + } + + @Override + public int read() throws IOException { + if (atTheEnd) { + // End boundary reached, nothing more to read + return -1; + } else if (bufferedByte >= 0) { + /* Purge partially matched boundary if there was such */ + return getBuffered(); + } else if (matchedCount != -1) { + /* + * Special case where last "failed" matching ended with first + * character from boundary string + */ + return matchForBoundary(); + } else { + int fromActualStream = realInputStream.read(); + if (fromActualStream == -1) { + // unexpected end of stream + throw new IOException( + "The multipart stream ended unexpectedly"); + } + if (boundary[0] == fromActualStream) { + /* + * If matches the first character in boundary string, start + * checking if the boundary is fetched. + */ + return matchForBoundary(); + } + return fromActualStream; + } + } + + /** + * Reads the input to expect a boundary string. Expects that the first + * character has already been matched. + * + * @return -1 if the boundary was matched, else returns the first byte + * from boundary + * @throws IOException + */ + private int matchForBoundary() throws IOException { + matchedCount = 0; + /* + * Going to "buffered mode". Read until full boundary match or a + * different character. + */ + while (true) { + matchedCount++; + if (matchedCount == boundary.length) { + /* + * The whole boundary matched so we have reached the end of + * file + */ + atTheEnd = true; + return -1; + } + int fromActualStream = realInputStream.read(); + if (fromActualStream != boundary[matchedCount]) { + /* + * Did not find full boundary, cache the mismatching byte + * and start returning the partially matched boundary. + */ + bufferedByte = fromActualStream; + return getBuffered(); + } + } + } + + /** + * Returns the partly matched boundary string and the byte following + * that. + * + * @return + * @throws IOException + */ + private int getBuffered() throws IOException { + int b; + if (matchedCount == 0) { + // The boundary has been returned, return the buffered byte. + b = bufferedByte; + bufferedByte = -1; + matchedCount = -1; + } else { + b = boundary[curBoundaryIndex++]; + if (curBoundaryIndex == matchedCount) { + // The full boundary has been returned, remaining is the + // char that did not match the boundary. + + curBoundaryIndex = 0; + if (bufferedByte != boundary[0]) { + /* + * next call for getBuffered will return the + * bufferedByte that came after the partial boundary + * match + */ + matchedCount = 0; + } else { + /* + * Special case where buffered byte again matches the + * boundaryString. This could be the start of the real + * end boundary. + */ + matchedCount = 0; + bufferedByte = -1; + } + } + } + if (b == -1) { + throw new IOException("The multipart stream ended unexpectedly"); + } + return b; + } + } + + private static class UploadInterruptedException extends Exception { + public UploadInterruptedException() { + super("Upload interrupted by other thread"); + } + } + + private static final int LF = "\n".getBytes()[0]; + + private static final String CRLF = "\r\n"; + + private static final String UTF8 = "UTF-8"; + + private static final String DASHDASH = "--"; + + /* Same as in apache commons file upload library that was previously used. */ + private static final int MAX_UPLOAD_BUFFER_SIZE = 4 * 1024; + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + if (!ServletPortletHelper.isFileUploadRequest(request)) { + return false; + } + + /* + * URI pattern: APP/UPLOAD/[UIID]/[PID]/[NAME]/[SECKEY] See + * #createReceiverUrl + */ + + String pathInfo = request.getPathInfo(); + // strip away part until the data we are interested starts + int startOfData = pathInfo + .indexOf(ServletPortletHelper.UPLOAD_URL_PREFIX) + + ServletPortletHelper.UPLOAD_URL_PREFIX.length(); + String uppUri = pathInfo.substring(startOfData); + String[] parts = uppUri.split("/", 4); // 0= UIid, 1 = cid, 2= name, 3 + // = sec key + String uiId = parts[0]; + String connectorId = parts[1]; + String variableName = parts[2]; + + // These are retrieved while session is locked + ClientConnector source; + StreamVariable streamVariable; + + session.lock(); + try { + UI uI = session.getUIById(Integer.parseInt(uiId)); + UI.setCurrent(uI); + + streamVariable = uI.getConnectorTracker().getStreamVariable( + connectorId, variableName); + String secKey = uI.getConnectorTracker().getSeckey(streamVariable); + if (!secKey.equals(parts[3])) { + // TODO Should rethink error handling + return true; + } + + source = session.getCommunicationManager().getConnector(uI, + connectorId); + } finally { + session.unlock(); + } + + String contentType = request.getContentType(); + if (contentType.contains("boundary")) { + // Multipart requests contain boundary string + doHandleSimpleMultipartFileUpload(session, request, response, + streamVariable, variableName, source, + contentType.split("boundary=")[1]); + } else { + // if boundary string does not exist, the posted file is from + // XHR2.post(File) + doHandleXhrFilePost(session, request, response, streamVariable, + variableName, source, request.getContentLength()); + } + return true; + } + + private static String readLine(InputStream stream) throws IOException { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + int readByte = stream.read(); + while (readByte != LF) { + bout.write(readByte); + readByte = stream.read(); + } + byte[] bytes = bout.toByteArray(); + return new String(bytes, 0, bytes.length - 1, UTF8); + } + + /** + * Method used to stream content from a multipart request (either from + * servlet or portlet request) to given StreamVariable. + * <p> + * This method takes care of locking the session as needed and does not + * assume the caller has locked the session. This allows the session to be + * locked only when needed and not when handling the upload data. + * </p> + * + * @param session + * The session containing the stream variable + * @param request + * The upload request + * @param response + * The upload response + * @param streamVariable + * The destination stream variable + * @param variableName + * The name of the destination stream variable + * @param owner + * The owner of the stream variable + * @param boundary + * The mime boundary used in the upload request + * @throws IOException + * If there is a problem reading the request or writing the + * response + */ + protected void doHandleSimpleMultipartFileUpload(VaadinSession session, + VaadinRequest request, VaadinResponse response, + StreamVariable streamVariable, String variableName, + ClientConnector owner, String boundary) throws IOException { + // multipart parsing, supports only one file for request, but that is + // fine for our current terminal + + final InputStream inputStream = request.getInputStream(); + + int contentLength = request.getContentLength(); + + boolean atStart = false; + boolean firstFileFieldFound = false; + + String rawfilename = "unknown"; + String rawMimeType = "application/octet-stream"; + + /* + * Read the stream until the actual file starts (empty line). Read + * filename and content type from multipart headers. + */ + while (!atStart) { + String readLine = readLine(inputStream); + contentLength -= (readLine.getBytes(UTF8).length + CRLF.length()); + if (readLine.startsWith("Content-Disposition:") + && readLine.indexOf("filename=") > 0) { + rawfilename = readLine.replaceAll(".*filename=", ""); + char quote = rawfilename.charAt(0); + rawfilename = rawfilename.substring(1); + rawfilename = rawfilename.substring(0, + rawfilename.indexOf(quote)); + firstFileFieldFound = true; + } else if (firstFileFieldFound && readLine.equals("")) { + atStart = true; + } else if (readLine.startsWith("Content-Type")) { + rawMimeType = readLine.split(": ")[1]; + } + } + + contentLength -= (boundary.length() + CRLF.length() + 2 + * DASHDASH.length() + CRLF.length()); + + /* + * Reads bytes from the underlying stream. Compares the read bytes to + * the boundary string and returns -1 if met. + * + * The matching happens so that if the read byte equals to the first + * char of boundary string, the stream goes to "buffering mode". In + * buffering mode bytes are read until the character does not match the + * corresponding from boundary string or the full boundary string is + * found. + * + * Note, if this is someday needed elsewhere, don't shoot yourself to + * foot and split to a top level helper class. + */ + InputStream simpleMultiPartReader = new SimpleMultiPartInputStream( + inputStream, boundary); + + /* + * Should report only the filename even if the browser sends the path + */ + final String filename = removePath(rawfilename); + final String mimeType = rawMimeType; + + try { + handleFileUploadValidationAndData(session, simpleMultiPartReader, + streamVariable, filename, mimeType, contentLength, owner, + variableName); + } catch (UploadException e) { + session.getCommunicationManager().handleConnectorRelatedException( + owner, e); + } + sendUploadResponse(request, response); + + } + + private void handleFileUploadValidationAndData(VaadinSession session, + InputStream inputStream, StreamVariable streamVariable, + String filename, String mimeType, int contentLength, + ClientConnector connector, String variableName) + throws UploadException { + session.lock(); + try { + if (connector == null) { + throw new UploadException( + "File upload ignored because the connector for the stream variable was not found"); + } + if (!connector.isConnectorEnabled()) { + throw new UploadException("Warning: file upload ignored for " + + connector.getConnectorId() + + " because the component was disabled"); + } + if ((connector instanceof Component) + && ((Component) connector).isReadOnly()) { + // Only checked for legacy reasons + throw new UploadException( + "File upload ignored because the component is read-only"); + } + } finally { + session.unlock(); + } + try { + boolean forgetVariable = streamToReceiver(session, inputStream, + streamVariable, filename, mimeType, contentLength); + if (forgetVariable) { + cleanStreamVariable(session, connector, variableName); + } + } catch (Exception e) { + session.lock(); + try { + session.getCommunicationManager() + .handleConnectorRelatedException(connector, e); + } finally { + session.unlock(); + } + } + } + + /** + * Used to stream plain file post (aka XHR2.post(File)) + * <p> + * This method takes care of locking the session as needed and does not + * assume the caller has locked the session. This allows the session to be + * locked only when needed and not when handling the upload data. + * </p> + * + * @param session + * The session containing the stream variable + * @param request + * The upload request + * @param response + * The upload response + * @param streamVariable + * The destination stream variable + * @param variableName + * The name of the destination stream variable + * @param owner + * The owner of the stream variable + * @param contentLength + * The length of the request content + * @throws IOException + * If there is a problem reading the request or writing the + * response + */ + protected void doHandleXhrFilePost(VaadinSession session, + VaadinRequest request, VaadinResponse response, + StreamVariable streamVariable, String variableName, + ClientConnector owner, int contentLength) throws IOException { + + // These are unknown in filexhr ATM, maybe add to Accept header that + // is accessible in portlets + final String filename = "unknown"; + final String mimeType = filename; + final InputStream stream = request.getInputStream(); + + try { + handleFileUploadValidationAndData(session, stream, streamVariable, + filename, mimeType, contentLength, owner, variableName); + } catch (UploadException e) { + session.getCommunicationManager().handleConnectorRelatedException( + owner, e); + } + sendUploadResponse(request, response); + } + + /** + * @param in + * @param streamVariable + * @param filename + * @param type + * @param contentLength + * @return true if the streamvariable has informed that the terminal can + * forget this variable + * @throws UploadException + */ + protected final boolean streamToReceiver(VaadinSession session, + final InputStream in, StreamVariable streamVariable, + String filename, String type, int contentLength) + throws UploadException { + if (streamVariable == null) { + throw new IllegalStateException( + "StreamVariable for the post not found"); + } + + OutputStream out = null; + int totalBytes = 0; + StreamingStartEventImpl startedEvent = new StreamingStartEventImpl( + filename, type, contentLength); + try { + boolean listenProgress; + session.lock(); + try { + streamVariable.streamingStarted(startedEvent); + out = streamVariable.getOutputStream(); + listenProgress = streamVariable.listenProgress(); + } finally { + session.unlock(); + } + + // Gets the output target stream + if (out == null) { + throw new NoOutputStreamException(); + } + + if (null == in) { + // No file, for instance non-existent filename in html upload + throw new NoInputStreamException(); + } + + final byte buffer[] = new byte[MAX_UPLOAD_BUFFER_SIZE]; + int bytesReadToBuffer = 0; + while ((bytesReadToBuffer = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesReadToBuffer); + totalBytes += bytesReadToBuffer; + if (listenProgress) { + // update progress if listener set and contentLength + // received + session.lock(); + try { + StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl( + filename, type, contentLength, totalBytes); + streamVariable.onProgress(progressEvent); + } finally { + session.unlock(); + } + } + if (streamVariable.isInterrupted()) { + throw new UploadInterruptedException(); + } + } + + // upload successful + out.close(); + StreamingEndEvent event = new StreamingEndEventImpl(filename, type, + totalBytes); + session.lock(); + try { + streamVariable.streamingFinished(event); + } finally { + session.unlock(); + } + + } catch (UploadInterruptedException e) { + // Download interrupted by application code + tryToCloseStream(out); + StreamingErrorEvent event = new StreamingErrorEventImpl(filename, + type, contentLength, totalBytes, e); + session.lock(); + try { + streamVariable.streamingFailed(event); + } finally { + session.unlock(); + } + // Note, we are not throwing interrupted exception forward as it is + // not a terminal level error like all other exception. + } catch (final Exception e) { + tryToCloseStream(out); + session.lock(); + try { + StreamingErrorEvent event = new StreamingErrorEventImpl( + filename, type, contentLength, totalBytes, e); + streamVariable.streamingFailed(event); + // throw exception for terminal to be handled (to be passed to + // terminalErrorHandler) + throw new UploadException(e); + } finally { + session.unlock(); + } + } + return startedEvent.isDisposed(); + } + + static void tryToCloseStream(OutputStream out) { + try { + // try to close output stream (e.g. file handle) + if (out != null) { + out.close(); + } + } catch (IOException e1) { + // NOP + } + } + + /** + * Removes any possible path information from the filename and returns the + * filename. Separators / and \\ are used. + * + * @param name + * @return + */ + private static String removePath(String filename) { + if (filename != null) { + filename = filename.replaceAll("^.*[/\\\\]", ""); + } + + return filename; + } + + /** + * TODO document + * + * @param request + * @param response + * @throws IOException + */ + protected void sendUploadResponse(VaadinRequest request, + VaadinResponse response) throws IOException { + response.setContentType("text/html"); + final OutputStream out = response.getOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>download handled</body></html>"); + outWriter.flush(); + out.close(); + } + + private void cleanStreamVariable(VaadinSession session, + final ClientConnector owner, final String variableName) { + session.access(new Runnable() { + @Override + public void run() { + owner.getUI() + .getConnectorTracker() + .cleanStreamVariable(owner.getConnectorId(), + variableName); + } + }); + } +} diff --git a/server/src/com/vaadin/server/communication/HeartbeatHandler.java b/server/src/com/vaadin/server/communication/HeartbeatHandler.java new file mode 100644 index 0000000000..16c21224ab --- /dev/null +++ b/server/src/com/vaadin/server/communication/HeartbeatHandler.java @@ -0,0 +1,90 @@ +/* + * 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.io.IOException; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.SessionExpiredHandler; +import com.vaadin.server.SynchronizedRequestHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.ui.UI; + +/** + * Handles heartbeat requests. Heartbeat requests are periodically sent by the + * client-side to inform the server that the UI sending the heartbeat is still + * alive (the browser window is open, the connection is up) even when there are + * no UIDL requests for a prolonged period of time. UIs that do not receive + * either heartbeat or UIDL requests are eventually removed from the session and + * garbage collected. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class HeartbeatHandler extends SynchronizedRequestHandler implements + SessionExpiredHandler { + + /** + * Handles a heartbeat request for the given session. Reads the GET + * parameter named {@link UIConstants#UI_ID_PARAMETER} to identify the UI. + * If the UI is found in the session, sets it + * {@link UI#getLastHeartbeatTimestamp() heartbeat timestamp} to the current + * time. Otherwise, writes a HTTP Not Found error to the response. + */ + @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()); + // Ensure that the browser does not cache heartbeat responses. + // iOS 6 Safari requires this (#10370) + response.setHeader("Cache-Control", "no-cache"); + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "UI not found"); + } + + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.server.SessionExpiredHandler#handleSessionExpired(com.vaadin + * .server.VaadinRequest, com.vaadin.server.VaadinResponse) + */ + @Override + public boolean handleSessionExpired(VaadinRequest request, + VaadinResponse response) throws IOException { + if (!ServletPortletHelper.isHeartbeatRequest(request)) { + return false; + } + + response.sendError(HttpServletResponse.SC_GONE, "Session expired"); + return true; + } +} diff --git a/server/src/com/vaadin/server/communication/LegacyUidlWriter.java b/server/src/com/vaadin/server/communication/LegacyUidlWriter.java new file mode 100644 index 0000000000..ad99a2d8b5 --- /dev/null +++ b/server/src/com/vaadin/server/communication/LegacyUidlWriter.java @@ -0,0 +1,118 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Logger; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.LegacyPaint; +import com.vaadin.server.PaintTarget; +import com.vaadin.ui.Component; +import com.vaadin.ui.LegacyComponent; +import com.vaadin.ui.UI; + +/** + * Serializes legacy UIDL changes to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class LegacyUidlWriter implements Serializable { + + /** + * Writes a JSON array containing the changes of all dirty + * {@link LegacyComponent}s in the given UI. + * + * @param ui + * The {@link UI} whose legacy changes to write + * @param writer + * The {@link Writer} to write the JSON with + * @param target + * The {@link PaintTarget} to use + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer, PaintTarget target) + throws IOException { + + Collection<ClientConnector> dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + List<Component> legacyComponents = new ArrayList<Component>(); + for (ClientConnector connector : dirtyVisibleConnectors) { + // All Components that want to use paintContent must implement + // LegacyComponent + if (connector instanceof LegacyComponent) { + legacyComponents.add((Component) connector); + } + } + sortByHierarchy(legacyComponents); + + writer.write("["); + for (Component c : legacyComponents) { + getLogger().fine( + "Painting LegacyComponent " + c.getClass().getName() + "@" + + Integer.toHexString(c.hashCode())); + target.startTag("change"); + final String pid = c.getConnectorId(); + target.addAttribute("pid", pid); + LegacyPaint.paint(c, target); + target.endTag("change"); + } + writer.write("]"); + } + + private void sortByHierarchy(List<Component> paintables) { + // Vaadin 6 requires parents to be painted before children as component + // containers rely on that their updateFromUIDL method has been called + // before children start calling e.g. updateCaption + Collections.sort(paintables, new Comparator<Component>() { + @Override + public int compare(Component c1, Component c2) { + int depth1 = 0; + while (c1.getParent() != null) { + depth1++; + c1 = c1.getParent(); + } + int depth2 = 0; + while (c2.getParent() != null) { + depth2++; + c2 = c2.getParent(); + } + if (depth1 < depth2) { + return -1; + } + if (depth1 > depth2) { + return 1; + } + return 0; + } + }); + } + + private static final Logger getLogger() { + return Logger.getLogger(LegacyUidlWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/LocaleWriter.java b/server/src/com/vaadin/server/communication/LocaleWriter.java new file mode 100644 index 0000000000..c05649da19 --- /dev/null +++ b/server/src/com/vaadin/server/communication/LocaleWriter.java @@ -0,0 +1,204 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; +import java.util.logging.Logger; + +/** + * Serializes locale information to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + * @deprecated See <a href="http://dev.vaadin.com/ticket/11378">ticket + * #11378</a>. + */ +@Deprecated +public class LocaleWriter implements Serializable { + + /** + * Writes a JSON object containing localized strings of the given locales. + * + * @param locales + * The list of {@link Locale}s to write. + * @param writer + * The {@link Writer} used to write the JSON. + * @throws IOException + * If the serialization fails. + * + */ + public void write(List<String> locales, Writer writer) throws IOException { + + // Send locale informations to client + writer.write("["); + // TODO locales are currently sent on each request; this will be fixed + // by implementing #11378. + for (int pendingLocalesIndex = 0; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) { + + final Locale l = generateLocale(locales.get(pendingLocalesIndex)); + // Locale name + writer.write("{\"name\":\"" + l.toString() + "\","); + + /* + * Month names (both short and full) + */ + final DateFormatSymbols dfs = new DateFormatSymbols(l); + final String[] short_months = dfs.getShortMonths(); + final String[] months = dfs.getMonths(); + writer.write("\"smn\":[\"" + + // ShortMonthNames + short_months[0] + "\",\"" + short_months[1] + "\",\"" + + short_months[2] + "\",\"" + short_months[3] + "\",\"" + + short_months[4] + "\",\"" + short_months[5] + "\",\"" + + short_months[6] + "\",\"" + short_months[7] + "\",\"" + + short_months[8] + "\",\"" + short_months[9] + "\",\"" + + short_months[10] + "\",\"" + short_months[11] + "\"" + + "],"); + writer.write("\"mn\":[\"" + + // MonthNames + months[0] + "\",\"" + months[1] + "\",\"" + months[2] + + "\",\"" + months[3] + "\",\"" + months[4] + "\",\"" + + months[5] + "\",\"" + months[6] + "\",\"" + months[7] + + "\",\"" + months[8] + "\",\"" + months[9] + "\",\"" + + months[10] + "\",\"" + months[11] + "\"" + "],"); + + /* + * Weekday names (both short and full) + */ + final String[] short_days = dfs.getShortWeekdays(); + final String[] days = dfs.getWeekdays(); + writer.write("\"sdn\":[\"" + + // ShortDayNames + short_days[1] + "\",\"" + short_days[2] + "\",\"" + + short_days[3] + "\",\"" + short_days[4] + "\",\"" + + short_days[5] + "\",\"" + short_days[6] + "\",\"" + + short_days[7] + "\"" + "],"); + writer.write("\"dn\":[\"" + + // DayNames + days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\"" + + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\"" + + days[7] + "\"" + "],"); + + /* + * First day of week (0 = sunday, 1 = monday) + */ + final Calendar cal = new GregorianCalendar(l); + writer.write("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ","); + + /* + * Date formatting (MM/DD/YYYY etc.) + */ + + DateFormat dateFormat = DateFormat.getDateTimeInstance( + DateFormat.SHORT, DateFormat.SHORT, l); + if (!(dateFormat instanceof SimpleDateFormat)) { + getLogger().warning( + "Unable to get default date pattern for locale " + + l.toString()); + dateFormat = new SimpleDateFormat(); + } + final String df = ((SimpleDateFormat) dateFormat).toPattern(); + + int timeStart = df.indexOf("H"); + if (timeStart < 0) { + timeStart = df.indexOf("h"); + } + final int ampm_first = df.indexOf("a"); + // E.g. in Korean locale AM/PM is before h:mm + // TODO should take that into consideration on client-side as well, + // now always h:mm a + if (ampm_first > 0 && ampm_first < timeStart) { + timeStart = ampm_first; + } + // Hebrew locale has time before the date + final boolean timeFirst = timeStart == 0; + String dateformat; + if (timeFirst) { + int dateStart = df.indexOf(' '); + if (ampm_first > dateStart) { + dateStart = df.indexOf(' ', ampm_first); + } + dateformat = df.substring(dateStart + 1); + } else { + dateformat = df.substring(0, timeStart - 1); + } + + writer.write("\"df\":\"" + dateformat.trim() + "\","); + + /* + * Time formatting (24 or 12 hour clock and AM/PM suffixes) + */ + final String timeformat = df.substring(timeStart, df.length()); + /* + * Doesn't return second or milliseconds. + * + * We use timeformat to determine 12/24-hour clock + */ + final boolean twelve_hour_clock = timeformat.indexOf("a") > -1; + // TODO there are other possibilities as well, like 'h' in french + // (ignore them, too complicated) + final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "." + : ":"; + // outWriter.print("\"tf\":\"" + timeformat + "\","); + writer.write("\"thc\":" + twelve_hour_clock + ","); + writer.write("\"hmd\":\"" + hour_min_delimiter + "\""); + if (twelve_hour_clock) { + final String[] ampm = dfs.getAmPmStrings(); + writer.write(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1] + + "\"]"); + } + writer.write("}"); + if (pendingLocalesIndex < locales.size() - 1) { + writer.write(","); + } + } + writer.write("]"); // Close locales + } + + /** + * Constructs a {@link Locale} instance to be sent to the client based on a + * short locale description string. + * + * @see #requireLocale(String) + * + * @param value + * @return + */ + private Locale generateLocale(String value) { + final String[] temp = value.split("_"); + if (temp.length == 1) { + return new Locale(temp[0]); + } else if (temp.length == 2) { + return new Locale(temp[0], temp[1]); + } else { + return new Locale(temp[0], temp[1], temp[2]); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(LocaleWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/MetadataWriter.java b/server/src/com/vaadin/server/communication/MetadataWriter.java new file mode 100644 index 0000000000..1a3f0e946a --- /dev/null +++ b/server/src/com/vaadin/server/communication/MetadataWriter.java @@ -0,0 +1,148 @@ +/* + * 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.io.IOException; +import java.io.PrintWriter; +import java.io.Serializable; +import java.io.Writer; +import java.util.List; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.ComponentSizeValidator; +import com.vaadin.server.ComponentSizeValidator.InvalidLayout; +import com.vaadin.server.SystemMessages; +import com.vaadin.ui.UI; +import com.vaadin.ui.Window; + +/** + * Serializes miscellaneous metadata to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class MetadataWriter implements Serializable { + + private int timeoutInterval = -1; + + /** + * Writes a JSON object containing metadata related to the given UI. + * + * @param ui + * The UI whose metadata to write. + * @param writer + * The writer used. + * @param repaintAll + * Whether the client should repaint everything. + * @param analyzeLayouts + * Whether detected layout problems should be reported in client + * and server console. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. + * @param hilightedConnector + * The connector that should be highlighted on the client or null + * if none. + * @param messages + * a {@link SystemMessages} containing client-side error + * messages. + * @throws IOException + * If the serialization fails. + * + */ + public void write(UI ui, Writer writer, boolean repaintAll, + boolean analyzeLayouts, boolean async, + ClientConnector hilightedConnector, SystemMessages messages) + throws IOException { + + List<InvalidLayout> invalidComponentRelativeSizes = null; + + if (analyzeLayouts) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes(ui.getContent(), null, null); + + // Also check any existing subwindows + if (ui.getWindows() != null) { + for (Window subWindow : ui.getWindows()) { + invalidComponentRelativeSizes = ComponentSizeValidator + .validateComponentRelativeSizes( + subWindow.getContent(), + invalidComponentRelativeSizes, null); + } + } + } + + writer.write("{"); + + boolean metaOpen = false; + if (repaintAll) { + metaOpen = true; + writer.write("\"repaintAll\":true"); + if (analyzeLayouts) { + writer.write(", \"invalidLayouts\":"); + writer.write("["); + if (invalidComponentRelativeSizes != null) { + boolean first = true; + for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) { + if (!first) { + writer.write(","); + } else { + first = false; + } + invalidLayout.reportErrors(new PrintWriter(writer), + System.err); + } + } + writer.write("]"); + } + if (hilightedConnector != null) { + writer.write(", \"hl\":\""); + writer.write(hilightedConnector.getConnectorId()); + writer.write("\""); + } + } + + if (async) { + if (metaOpen) { + writer.write(", "); + } + writer.write("\"async\":true"); + } + + // meta instruction for client to enable auto-forward to + // sessionExpiredURL after timer expires. + if (messages != null && messages.getSessionExpiredMessage() == null + && messages.getSessionExpiredCaption() == null + && messages.isSessionExpiredNotificationEnabled()) { + int newTimeoutInterval = ui.getSession().getSession() + .getMaxInactiveInterval(); + if (repaintAll || (timeoutInterval != newTimeoutInterval)) { + String escapedURL = messages.getSessionExpiredURL() == null ? "" + : messages.getSessionExpiredURL().replace("/", "\\/"); + if (metaOpen) { + writer.write(","); + } + writer.write("\"timedRedirect\":{\"interval\":" + + (newTimeoutInterval + 15) + ",\"url\":\"" + + escapedURL + "\"}"); + metaOpen = true; + } + timeoutInterval = newTimeoutInterval; + } + writer.write("}"); + } +} diff --git a/server/src/com/vaadin/server/communication/PortletBootstrapHandler.java b/server/src/com/vaadin/server/communication/PortletBootstrapHandler.java new file mode 100644 index 0000000000..2458951ada --- /dev/null +++ b/server/src/com/vaadin/server/communication/PortletBootstrapHandler.java @@ -0,0 +1,113 @@ +/* + * 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.io.IOException; + +import javax.portlet.MimeResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceURL; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.BootstrapHandler; +import com.vaadin.server.PaintException; +import com.vaadin.server.VaadinPortlet; +import com.vaadin.server.VaadinPortletRequest; +import com.vaadin.server.VaadinPortletResponse; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; + +public class PortletBootstrapHandler extends BootstrapHandler { + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + PortletRequest portletRequest = ((VaadinPortletRequest) request) + .getPortletRequest(); + if (portletRequest instanceof RenderRequest) { + return super.handleRequest(session, request, response); + } else { + return false; + } + } + + @Override + protected String getServiceUrl(BootstrapContext context) { + ResourceURL portletResourceUrl = getRenderResponse(context) + .createResourceURL(); + portletResourceUrl.setResourceID(VaadinPortlet.RESOURCE_URL_ID); + return portletResourceUrl.toString(); + } + + private RenderResponse getRenderResponse(BootstrapContext context) { + PortletResponse response = ((VaadinPortletResponse) context + .getResponse()).getPortletResponse(); + + RenderResponse renderResponse = (RenderResponse) response; + return renderResponse; + } + + @Override + protected void appendMainScriptTagContents(BootstrapContext context, + StringBuilder builder) throws JSONException, IOException { + // fixed base theme to use - all portal pages with Vaadin + // applications will load this exactly once + String portalTheme = ((VaadinPortletRequest) context.getRequest()) + .getPortalProperty(VaadinPortlet.PORTAL_PARAMETER_VAADIN_THEME); + if (portalTheme != null && !portalTheme.equals(context.getThemeName())) { + String portalThemeUri = getThemeUri(context, portalTheme); + // XSS safe - originates from portal properties + builder.append("vaadin.loadTheme('" + portalThemeUri + "');"); + } + + super.appendMainScriptTagContents(context, builder); + } + + @Override + protected String getMainDivStyle(BootstrapContext context) { + VaadinService vaadinService = context.getRequest().getService(); + return vaadinService.getDeploymentConfiguration() + .getApplicationOrSystemProperty( + VaadinPortlet.PORTLET_PARAMETER_STYLE, null); + } + + @Override + protected JSONObject getApplicationParameters(BootstrapContext context) + throws JSONException, PaintException { + JSONObject parameters = super.getApplicationParameters(context); + VaadinPortletResponse response = (VaadinPortletResponse) context + .getResponse(); + MimeResponse portletResponse = (MimeResponse) response + .getPortletResponse(); + ResourceURL resourceURL = portletResponse.createResourceURL(); + resourceURL.setResourceID("v-browserDetails"); + parameters.put("browserDetailsUrl", resourceURL.toString()); + + // Always send path info as a query parameter + parameters + .put(ApplicationConstants.SERVICE_URL_PATH_AS_PARAMETER, true); + + return parameters; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/server/communication/PortletDummyRequestHandler.java b/server/src/com/vaadin/server/communication/PortletDummyRequestHandler.java new file mode 100644 index 0000000000..8383cf607b --- /dev/null +++ b/server/src/com/vaadin/server/communication/PortletDummyRequestHandler.java @@ -0,0 +1,82 @@ +/* + * 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.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import javax.portlet.PortletResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.VaadinPortletResponse; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; + +/** + * Request handler which provides a dummy HTML response to any resource request + * with the resource id DUMMY. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PortletDummyRequestHandler implements RequestHandler { + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + if (!isDummyRequest(request)) { + return false; + } + + /* + * This dummy page is used by action responses to redirect to, in order + * to prevent the boot strap code from being rendered into strange + * places such as iframes. + */ + PortletResponse portletResponse = ((VaadinPortletResponse) response) + .getPortletResponse(); + if (portletResponse instanceof ResourceResponse) { + ((ResourceResponse) portletResponse).setContentType("text/html"); + } + + final OutputStream out = ((ResourceResponse) response) + .getPortletOutputStream(); + final PrintWriter outWriter = new PrintWriter(new BufferedWriter( + new OutputStreamWriter(out, "UTF-8"))); + outWriter.print("<html><body>dummy page</body></html>"); + outWriter.close(); + + return true; + } + + public static boolean isDummyRequest(VaadinRequest request) { + ResourceRequest resourceRequest = PortletUIInitHandler + .getResourceRequest(request); + if (resourceRequest == null) { + return false; + } + + return resourceRequest.getResourceID() != null + && resourceRequest.getResourceID().equals("DUMMY"); + } + +} diff --git a/server/src/com/vaadin/server/communication/PortletListenerNotifier.java b/server/src/com/vaadin/server/communication/PortletListenerNotifier.java new file mode 100644 index 0000000000..5c03a6f4dc --- /dev/null +++ b/server/src/com/vaadin/server/communication/PortletListenerNotifier.java @@ -0,0 +1,89 @@ +/* + * 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.io.IOException; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.EventRequest; +import javax.portlet.EventResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.ResourceRequest; +import javax.portlet.ResourceResponse; + +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.SynchronizedRequestHandler; +import com.vaadin.server.VaadinPortletRequest; +import com.vaadin.server.VaadinPortletResponse; +import com.vaadin.server.VaadinPortletSession; +import com.vaadin.server.VaadinPortletSession.PortletListener; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; + +/** + * Notifies {@link PortletListener}s of a received portlet request. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PortletListenerNotifier extends SynchronizedRequestHandler { + + /** + * Fires portlet request events to any {@link PortletListener}s registered + * to the given session using + * {@link VaadinPortletSession#addPortletListener(PortletListener)}. The + * PortletListener method corresponding to the request type is invoked. + */ + @Override + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) throws IOException { + + VaadinPortletSession sess = (VaadinPortletSession) session; + PortletRequest req = ((VaadinPortletRequest) request) + .getPortletRequest(); + PortletResponse resp = ((VaadinPortletResponse) response) + .getPortletResponse(); + + // Finds the right UI + UI uI = null; + if (ServletPortletHelper.isUIDLRequest(request)) { + uI = session.getService().findUI(request); + } + + if (request instanceof RenderRequest) { + sess.firePortletRenderRequest(uI, (RenderRequest) req, + (RenderResponse) resp); + } else if (request instanceof ActionRequest) { + sess.firePortletActionRequest(uI, (ActionRequest) req, + (ActionResponse) resp); + } else if (request instanceof EventRequest) { + sess.firePortletEventRequest(uI, (EventRequest) req, + (EventResponse) resp); + } else if (request instanceof ResourceRequest) { + sess.firePortletResourceRequest(uI, (ResourceRequest) req, + (ResourceResponse) resp); + } + + return false; + } +} diff --git a/server/src/com/vaadin/server/communication/PortletUIInitHandler.java b/server/src/com/vaadin/server/communication/PortletUIInitHandler.java new file mode 100644 index 0000000000..d5d1e6b98d --- /dev/null +++ b/server/src/com/vaadin/server/communication/PortletUIInitHandler.java @@ -0,0 +1,63 @@ +/* + * 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 javax.portlet.PortletRequest; +import javax.portlet.ResourceRequest; + +import com.vaadin.server.VaadinPortletRequest; +import com.vaadin.server.VaadinRequest; + +public class PortletUIInitHandler extends UIInitHandler { + + @Override + protected boolean isInitRequest(VaadinRequest request) { + return isUIInitRequest(request); + } + + public static boolean isUIInitRequest(VaadinRequest request) { + ResourceRequest resourceRequest = getResourceRequest(request); + if (resourceRequest == null) { + return false; + } + + return UIInitHandler.BROWSER_DETAILS_PARAMETER.equals(resourceRequest + .getResourceID()); + } + + /** + * Returns the {@link ResourceRequest} for the given request or null if none + * could be found. + * + * @param request + * The original request, must be a {@link VaadinPortletRequest} + * @return The resource request from the request parameter or null + */ + static ResourceRequest getResourceRequest(VaadinRequest request) { + if (!(request instanceof VaadinPortletRequest)) { + throw new IllegalArgumentException( + "Request must a VaadinPortletRequest"); + } + PortletRequest portletRequest = ((VaadinPortletRequest) request) + .getPortletRequest(); + if (!(portletRequest instanceof ResourceRequest)) { + return null; + } + + return (ResourceRequest) portletRequest; + + } +} diff --git a/server/src/com/vaadin/server/communication/PublishedFileHandler.java b/server/src/com/vaadin/server/communication/PublishedFileHandler.java new file mode 100644 index 0000000000..8fe0f7085f --- /dev/null +++ b/server/src/com/vaadin/server/communication/PublishedFileHandler.java @@ -0,0 +1,151 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import javax.servlet.http.HttpServletResponse; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.server.Constants; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; + +/** + * Serves a connector resource from the classpath if the resource has previously + * been registered by calling + * {@link LegacyCommunicationManager#registerDependency(String, Class)}. Sending + * arbitrary files from the classpath is prevented by only accepting resource + * names that have explicitly been registered. Resources can currently only be + * registered by including a {@link JavaScript} or {@link StyleSheet} annotation + * on a Connector class. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PublishedFileHandler implements RequestHandler { + + /** + * Writes the connector resource identified by the request URI to the + * response. If a published resource corresponding to the URI path is not + * found, writes a HTTP Not Found error to the response. + */ + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + if (!ServletPortletHelper.isPublishedFileRequest(request)) { + return false; + } + + String pathInfo = request.getPathInfo(); + // + 2 to also remove beginning and ending slashes + String fileName = pathInfo + .substring(ApplicationConstants.PUBLISHED_FILE_PATH.length() + 2); + + final String mimetype = response.getService().getMimeType(fileName); + + // Security check: avoid accidentally serving from the UI of the + // classpath instead of relative to the context class + if (fileName.startsWith("/")) { + getLogger().warning( + "Published file request starting with / rejected: " + + fileName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // Check that the resource name has been registered + session.lock(); + Class<?> context; + try { + context = session.getCommunicationManager().getDependencies() + .get(fileName); + } finally { + session.unlock(); + } + + // Security check: don't serve resource if the name hasn't been + // registered in the map + if (context == null) { + getLogger().warning( + "Rejecting published file request for file that has not been published: " + + fileName); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // Resolve file relative to the location of the context class + InputStream in = context.getResourceAsStream(fileName); + if (in == null) { + getLogger().warning( + fileName + " published by " + context.getName() + + " not found. Verify that the file " + + context.getPackage().getName().replace('.', '/') + + '/' + fileName + + " is available on the classpath."); + response.sendError(HttpServletResponse.SC_NOT_FOUND, fileName); + return true; + } + + // TODO Check and set cache headers + + OutputStream out = null; + try { + if (mimetype != null) { + response.setContentType(mimetype); + } + + out = response.getOutputStream(); + + final byte[] buffer = new byte[Constants.DEFAULT_BUFFER_SIZE]; + + int bytesRead = 0; + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + out.flush(); + } finally { + try { + in.close(); + } catch (Exception e) { + // Do nothing + } + if (out != null) { + try { + out.close(); + } catch (Exception e) { + // Do nothing + } + } + } + + return true; + } + + private static final Logger getLogger() { + return Logger.getLogger(PublishedFileHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/PushConnection.java b/server/src/com/vaadin/server/communication/PushConnection.java new file mode 100644 index 0000000000..4e043f565f --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushConnection.java @@ -0,0 +1,48 @@ +/* + * 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 com.vaadin.ui.UI; + +/** + * Represents a bidirectional ("push") connection between a single UI and its + * client-side. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public interface PushConnection { + + /** + * Pushes pending state changes and client RPC calls to the client. 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. + */ + public void push(); + + /** + * Disconnects the connection. + */ + public void disconnect(); + + /** + * Returns whether this connection is currently open. + */ + public boolean isConnected(); + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/server/communication/PushHandler.java b/server/src/com/vaadin/server/communication/PushHandler.java new file mode 100644 index 0000000000..e740db410d --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -0,0 +1,380 @@ +/* + * 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.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.atmosphere.cpr.AtmosphereHandler; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResource; +import org.atmosphere.cpr.AtmosphereResource.TRANSPORT; +import org.atmosphere.cpr.AtmosphereResourceEvent; +import org.json.JSONException; + +import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ServiceException; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.SessionExpiredException; +import com.vaadin.server.SystemMessages; +import com.vaadin.server.VaadinRequest; +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; + +/** + * Establishes bidirectional ("push") communication channels + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushHandler implements AtmosphereHandler { + + /** + * Callback interface used internally to process an event with the + * corresponding UI properly locked. + */ + private interface PushEventCallback { + public void run(AtmosphereResource resource, UI ui) throws IOException; + } + + /** + * Callback used when we receive a request to establish a push channel for a + * UI. Associate the AtmosphereResource with the UI and leave the connection + * open by calling resource.suspend(). If there is a pending push, send it + * now. + */ + private static PushEventCallback establishCallback = new PushEventCallback() { + @Override + public void run(AtmosphereResource resource, UI ui) throws IOException { + getLogger().log(Level.FINER, + "New push connection with transport {0}", + resource.transport()); + resource.getResponse().setContentType("text/plain; charset=UTF-8"); + + 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); + } + } + + String requestToken = resource.getRequest().getParameter( + ApplicationConstants.CSRF_TOKEN_PARAMETER); + if (!VaadinService.isCsrfTokenValid(session, requestToken)) { + getLogger() + .log(Level.WARNING, + "Invalid CSRF token in new connection received from {0}", + resource.getRequest().getRemoteHost()); + // Refresh on client side, create connection just for + // sending a message + sendRefreshAndDisconnect(resource); + return; + } + + resource.suspend(); + + AtmospherePushConnection connection = new AtmospherePushConnection( + ui); + connection.connect(resource); + + ui.setPushConnection(connection); + } + }; + + /** + * Callback used when we receive a UIDL request through Atmosphere. If the + * push channel is bidirectional (websockets), the request was sent via the + * same channel. Otherwise, the client used a separate AJAX request. Handle + * the request and send changed UI state via the push channel (we do not + * respond to the request directly.) + */ + private static PushEventCallback receiveCallback = new PushEventCallback() { + @Override + public void run(AtmosphereResource resource, UI ui) throws IOException { + AtmosphereRequest req = resource.getRequest(); + + AtmospherePushConnection connection = getConnectionForUI(ui); + + assert connection != null : "Got push from the client " + + "even though the connection does not seem to be " + + "valid. This might happen if a HttpSession is " + + "serialized and deserialized while the push " + + "connection is kept open or if the UI has a " + + "connection of unexpected type."; + + Reader reader = connection.receiveMessage(req.getReader()); + if (reader == null) { + // The whole message was not yet received + return; + } + + // Should be set up by caller + VaadinRequest vaadinRequest = VaadinService.getCurrentRequest(); + assert vaadinRequest != null; + + try { + new ServerRpcHandler().handleRpc(ui, reader, vaadinRequest); + connection.push(false); + } catch (JSONException e) { + getLogger().log(Level.SEVERE, "Error writing JSON to response", + e); + // Refresh on client side + sendRefreshAndDisconnect(resource); + } catch (InvalidUIDLSecurityKeyException e) { + getLogger().log(Level.WARNING, + "Invalid security key received from {0}", + resource.getRequest().getRemoteHost()); + // Refresh on client side + sendRefreshAndDisconnect(resource); + } + } + }; + + /** + * Callback used when a connection is closed by the client. + */ + PushEventCallback disconnectCallback = new PushEventCallback() { + @Override + public void run(AtmosphereResource resource, UI ui) throws IOException { + PushMode pushMode = ui.getPushMode(); + AtmospherePushConnection pushConnection = getConnectionForUI(ui); + + String id = resource.uuid(); + + if (pushConnection == 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, just clean up some stuff + * and be done with it + */ + getLogger().log(Level.FINEST, + "Connection closed for resource {0}", id); + } else { + /* + * Unexpected cancel, e.g. if the user closes the browser + * tab. + */ + getLogger() + .log(Level.FINE, + "Connection unexpectedly closed for resource {0} with transport {1}", + new Object[] { id, resource.transport() }); + } + ui.setPushConnection(null); + } + } + }; + + private static final String LONG_PADDING; + + static { + char[] array = new char[4096]; + Arrays.fill(array, '-'); + LONG_PADDING = String.copyValueOf(array); + + } + private VaadinServletService service; + + public PushHandler(VaadinServletService service) { + this.service = service; + } + + /** + * Find the UI for the atmosphere resource, lock it and invoke the callback. + * + * @param resource + * the atmosphere resource for the current request + * @param callback + * the push callback to call when a UI is found and locked + */ + private void callWithUi(final AtmosphereResource resource, + final PushEventCallback callback) { + AtmosphereRequest req = resource.getRequest(); + VaadinServletRequest vaadinRequest = new VaadinServletRequest(req, + service); + VaadinSession session = null; + + service.requestStart(vaadinRequest, null); + try { + try { + session = service.findVaadinSession(vaadinRequest); + } catch (ServiceException e) { + getLogger().log(Level.SEVERE, + "Could not get session. This should never happen", e); + } catch (SessionExpiredException e) { + SystemMessages msg = service.getSystemMessages( + ServletPortletHelper.findLocale(null, null, + vaadinRequest), vaadinRequest); + try { + resource.getResponse() + .getWriter() + .write(VaadinService + .createCriticalNotificationJSON( + msg.getSessionExpiredCaption(), + msg.getSessionExpiredMessage(), + null, msg.getSessionExpiredURL())); + } catch (IOException e1) { + getLogger() + .log(Level.WARNING, + "Failed to notify client about unavailable session", + e); + } + return; + } + + session.lock(); + try { + VaadinSession.setCurrent(session); + // Sets UI.currentInstance + final UI ui = service.findUI(vaadinRequest); + if (ui == null) { + // This a request through an already open push connection to + // a UI which no longer exists. + resource.getResponse() + .getWriter() + .write(UidlRequestHandler.getUINotFoundErrorJSON( + service, vaadinRequest)); + // End the connection + resource.resume(); + return; + } + + callback.run(resource, ui); + } catch (IOException e) { + getLogger().log(Level.INFO, + "An error occured while writing a push response", e); + } finally { + session.unlock(); + } + } finally { + service.requestEnd(vaadinRequest, null, session); + } + } + + @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) { + AtmospherePushConnection apc = (AtmospherePushConnection) pushConnection; + if (apc.isConnected()) { + return apc; + } + } + + return null; + } + + @Override + public void onStateChange(AtmosphereResourceEvent event) throws IOException { + AtmosphereResource resource = event.getResource(); + + String id = resource.uuid(); + if (event.isCancelled()) { + callWithUi(resource, disconnectCallback); + } 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 SSE: + case WEBSOCKET: + break; + case STREAMING: + writer.flush(); + break; + case JSONP: + case LONG_POLLING: + resource.resume(); + break; + default: + getLogger().log(Level.SEVERE, "Unknown transport {0}", + resource.transport()); + } + } + } + + @Override + public void destroy() { + } + + /** + * Sends a refresh message to the given atmosphere resource. Uses an + * AtmosphereResource instead of an AtmospherePushConnection even though it + * might be possible to look up the AtmospherePushConnection from the UI to + * ensure border cases work correctly, especially when there temporarily are + * two push connections which try to use the same UI. Using the + * AtmosphereResource directly guarantees the message goes to the correct + * recipient. + * + * @param resource + * The atmosphere resource to send refresh to + * + */ + private static void sendRefreshAndDisconnect(AtmosphereResource resource) + throws IOException { + AtmospherePushConnection connection = new AtmospherePushConnection(null); + connection.connect(resource); + connection.sendMessage(VaadinService.createCriticalNotificationJSON( + null, null, null, null)); + connection.disconnect(); + } + + private static final Logger getLogger() { + return Logger.getLogger(PushHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java new file mode 100644 index 0000000000..8360e08af9 --- /dev/null +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -0,0 +1,134 @@ +/* + * 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.io.IOException; + +import javax.servlet.ServletException; + +import org.atmosphere.client.TrackMessageSizeInterceptor; +import org.atmosphere.cpr.ApplicationConfig; +import org.atmosphere.cpr.AtmosphereFramework; +import org.atmosphere.cpr.AtmosphereRequest; +import org.atmosphere.cpr.AtmosphereResponse; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.ServiceException; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.SessionExpiredHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinServletRequest; +import com.vaadin.server.VaadinServletResponse; +import com.vaadin.server.VaadinServletService; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; + +/** + * Handles requests to open a push (bidirectional) communication channel between + * the client and the server. After the initial request, communication through + * the push channel is managed by {@link PushHandler}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class PushRequestHandler implements RequestHandler, + SessionExpiredHandler { + + private AtmosphereFramework atmosphere; + private PushHandler pushHandler; + + public PushRequestHandler(VaadinServletService service) + throws ServiceException { + + atmosphere = new AtmosphereFramework(); + + pushHandler = new PushHandler(service); + atmosphere.addAtmosphereHandler("/*", pushHandler); + atmosphere.addInitParameter(ApplicationConfig.PROPERTY_SESSION_SUPPORT, + "true"); + + final String bufferSize = String + .valueOf(ApplicationConstants.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"); + + // Required to ensure the client-side knows at which points to split the + // message stream into individual messages when using certain transports + atmosphere.interceptor(new TrackMessageSizeInterceptor()); + + try { + atmosphere.init(service.getServlet().getServletConfig()); + } catch (ServletException e) { + throw new ServiceException("Could not read atmosphere settings", e); + } + } + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + + if (!ServletPortletHelper.isPushRequest(request)) { + return false; + } + + if (request instanceof VaadinServletRequest) { + try { + atmosphere.doCometSupport(AtmosphereRequest + .wrap((VaadinServletRequest) request), + AtmosphereResponse + .wrap((VaadinServletResponse) response)); + } catch (ServletException e) { + // TODO PUSH decide how to handle + throw new RuntimeException(e); + } + } else { + throw new IllegalArgumentException( + "Portlets not currently supported"); + } + + return true; + } + + public void destroy() { + atmosphere.destroy(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.server.SessionExpiredHandler#handleSessionExpired(com.vaadin + * .server.VaadinRequest, com.vaadin.server.VaadinResponse) + */ + @Override + public boolean handleSessionExpired(VaadinRequest request, + VaadinResponse response) throws IOException { + // Websockets request must be handled by accepting the websocket + // connection and then sending session expired so we let + // PushRequestHandler handle it + return handleRequest(null, request, response); + } +} diff --git a/server/src/com/vaadin/server/communication/ResourceWriter.java b/server/src/com/vaadin/server/communication/ResourceWriter.java new file mode 100644 index 0000000000..080027943f --- /dev/null +++ b/server/src/com/vaadin/server/communication/ResourceWriter.java @@ -0,0 +1,113 @@ +/* + * 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.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.io.Writer; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.server.JsonPaintTarget; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.ui.CustomLayout; +import com.vaadin.ui.UI; + +/** + * Serializes resources to JSON. Currently only used for {@link CustomLayout} + * templates. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ResourceWriter implements Serializable { + + /** + * Writes a JSON object containing registered resources. + * + * @param ui + * The {@link UI} whose resources to write. + * @param writer + * The {@link Writer} to use. + * @param target + * The {@link JsonPaintTarget} containing the resources. + * @throws IOException + */ + public void write(UI ui, Writer writer, JsonPaintTarget target) + throws IOException { + + // TODO PUSH Refactor so that this is not needed + LegacyCommunicationManager manager = ui.getSession() + .getCommunicationManager(); + + // Precache custom layouts + + // TODO We should only precache the layouts that are not + // cached already (plagiate from usedPaintableTypes) + + writer.write("{"); + int resourceIndex = 0; + for (final Iterator<Object> i = target.getUsedResources().iterator(); i + .hasNext();) { + final String resource = (String) i.next(); + InputStream is = null; + try { + is = ui.getSession() + .getService() + .getThemeResourceAsStream(ui, manager.getTheme(ui), + resource); + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Failed to get theme resource stream.", e); + } + if (is != null) { + + writer.write((resourceIndex++ > 0 ? ", " : "") + "\"" + + resource + "\" : "); + final StringBuffer layout = new StringBuffer(); + + try { + final InputStreamReader r = new InputStreamReader(is, + "UTF-8"); + final char[] buffer = new char[20000]; + int charsRead = 0; + while ((charsRead = r.read(buffer)) > 0) { + layout.append(buffer, 0, charsRead); + } + r.close(); + } catch (final java.io.IOException e) { + // FIXME: Handle exception + getLogger().log(Level.INFO, "Resource transfer failed", e); + } + writer.write("\"" + + JsonPaintTarget.escapeJSON(layout.toString()) + "\""); + } else { + // FIXME: Handle exception + getLogger().severe("CustomLayout not found: " + resource); + } + } + writer.write("}"); + } + + private static final Logger getLogger() { + return Logger.getLogger(ResourceWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/ServerRpcHandler.java b/server/src/com/vaadin/server/communication/ServerRpcHandler.java new file mode 100644 index 0000000000..3cc85909ee --- /dev/null +++ b/server/src/com/vaadin/server/communication/ServerRpcHandler.java @@ -0,0 +1,469 @@ +/* + * 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.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; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ServerRpcManager; +import com.vaadin.server.ServerRpcManager.RpcInvocationException; +import com.vaadin.server.ServerRpcMethodInvocation; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VariableOwner; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.Connector; +import com.vaadin.shared.communication.LegacyChangeVariablesInvocation; +import com.vaadin.shared.communication.MethodInvocation; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.communication.UidlValue; +import com.vaadin.ui.Component; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.UI; + +/** + * Handles a client-to-server message containing serialized {@link ServerRpc + * server RPC} invocations. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class ServerRpcHandler implements Serializable { + + /* Variable records indexes */ + public static final char VAR_BURST_SEPARATOR = '\u001d'; + + public static final char VAR_ESCAPE_CHARACTER = '\u001b'; + + private static final int MAX_BUFFER_SIZE = 64 * 1024; + + /** + * Reads JSON containing zero or more serialized RPC calls (including legacy + * variable changes) and executes the calls. + * + * @param ui + * The {@link UI} receiving the calls. Cannot be null. + * @param reader + * The {@link Reader} used to read the JSON. + * @param request + * @throws IOException + * If reading the message fails. + * @throws InvalidUIDLSecurityKeyException + * If the received security key does not match the one stored in + * the session. + * @throws JSONException + * If deserializing the JSON fails. + */ + public void handleRpc(UI ui, Reader reader, VaadinRequest request) + throws IOException, InvalidUIDLSecurityKeyException, JSONException { + ui.getSession().setLastRequestTimestamp(System.currentTimeMillis()); + + String changes = 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) { + // The client sometimes sends empty messages, this is probably a bug + return; + } + + // Security: double cookie submission pattern unless disabled by + // property + if (!VaadinService.isCsrfTokenValid(ui.getSession(), bursts[0])) { + throw new InvalidUIDLSecurityKeyException(""); + } + handleBurst(ui, unescapeBurst(bursts[1])); + } + + /** + * 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. + * + * 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 + */ + private void handleBurst(UI uI, String burst) { + // TODO PUSH Refactor so that this is not needed + LegacyCommunicationManager manager = uI.getSession() + .getCommunicationManager(); + + try { + Set<Connector> enabledConnectors = new HashSet<Connector>(); + + List<MethodInvocation> invocations = parseInvocations( + uI.getConnectorTracker(), burst); + for (MethodInvocation invocation : invocations) { + final ClientConnector connector = manager.getConnector(uI, + invocation.getConnectorId()); + + if (connector != null && connector.isConnectorEnabled()) { + enabledConnectors.add(connector); + } + } + + for (int i = 0; i < invocations.size(); i++) { + MethodInvocation invocation = invocations.get(i); + + final ClientConnector connector = manager.getConnector(uI, + invocation.getConnectorId()); + if (connector == null) { + getLogger() + .log(Level.WARNING, + "Received RPC call for unknown connector with id {0} (tried to invoke {1}.{2})", + new Object[] { invocation.getConnectorId(), + invocation.getInterfaceName(), + invocation.getMethodName() }); + continue; + } + + if (!enabledConnectors.contains(connector)) { + + if (invocation instanceof LegacyChangeVariablesInvocation) { + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + // TODO convert window close to a separate RPC call and + // handle above - not a variable change + + // Handle special case where window-close is called + // after the window has been removed from the + // application or the application has closed + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + if (changes.size() == 1 && changes.containsKey("close") + && Boolean.TRUE.equals(changes.get("close"))) { + // Silently ignore this + continue; + } + } + + // Connector is disabled, log a warning and move to the next + String msg = "Ignoring RPC call for disabled connector " + + connector.getClass().getName(); + if (connector instanceof Component) { + String caption = ((Component) connector).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } + getLogger().warning(msg); + continue; + } + // DragAndDropService has null UI + if (connector.getUI() != null && connector.getUI().isClosing()) { + String msg = "Ignoring RPC call for connector " + + connector.getClass().getName(); + if (connector instanceof Component) { + String caption = ((Component) connector).getCaption(); + if (caption != null) { + msg += ", caption=" + caption; + } + } + msg += " in closed UI"; + getLogger().warning(msg); + continue; + + } + + if (invocation instanceof ServerRpcMethodInvocation) { + try { + ServerRpcManager.applyInvocation(connector, + (ServerRpcMethodInvocation) invocation); + } catch (RpcInvocationException e) { + manager.handleConnectorRelatedException(connector, e); + } + } else { + + // All code below is for legacy variable changes + LegacyChangeVariablesInvocation legacyInvocation = (LegacyChangeVariablesInvocation) invocation; + Map<String, Object> changes = legacyInvocation + .getVariableChanges(); + try { + if (connector instanceof VariableOwner) { + // The source parameter is never used anywhere + changeVariables(null, (VariableOwner) connector, + changes); + } else { + throw new IllegalStateException( + "Received legacy variable change for " + + connector.getClass().getName() + + " (" + + connector.getConnectorId() + + ") which is not a VariableOwner. The client-side connector sent these legacy varaibles: " + + changes.keySet()); + } + } catch (Exception e) { + manager.handleConnectorRelatedException(connector, e); + } + } + } + } catch (JSONException e) { + getLogger().warning( + "Unable to parse RPC call from the client: " + + e.getMessage()); + throw new RuntimeException(e); + } + } + + /** + * Parse a message burst from the client into a list of MethodInvocation + * instances. + * + * @param connectorTracker + * The ConnectorTracker used to lookup connectors + * @param burst + * message string (JSON) + * @return list of MethodInvocation to perform + * @throws JSONException + */ + private List<MethodInvocation> parseInvocations( + ConnectorTracker connectorTracker, String burst) + throws JSONException { + JSONArray invocationsJson = new JSONArray(burst); + + ArrayList<MethodInvocation> invocations = new ArrayList<MethodInvocation>(); + + MethodInvocation previousInvocation = null; + // parse JSON to MethodInvocations + for (int i = 0; i < invocationsJson.length(); ++i) { + + JSONArray invocationJson = invocationsJson.getJSONArray(i); + + MethodInvocation invocation = parseInvocation(invocationJson, + previousInvocation, connectorTracker); + 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 + // rejected because of an error. + invocations.add(invocation); + previousInvocation = invocation; + } + } + return invocations; + } + + private MethodInvocation parseInvocation(JSONArray invocationJson, + MethodInvocation previousInvocation, + ConnectorTracker connectorTracker) throws JSONException { + String connectorId = invocationJson.getString(0); + String interfaceName = invocationJson.getString(1); + String methodName = invocationJson.getString(2); + + 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(); + return null; + } + + JSONArray parametersJson = invocationJson.getJSONArray(3); + + if (LegacyChangeVariablesInvocation.isLegacyVariableChange( + interfaceName, methodName)) { + if (!(previousInvocation instanceof LegacyChangeVariablesInvocation)) { + previousInvocation = null; + } + + return parseLegacyChangeVariablesInvocation(connectorId, + interfaceName, methodName, + (LegacyChangeVariablesInvocation) previousInvocation, + parametersJson, connectorTracker); + } else { + return parseServerRpcInvocation(connectorId, interfaceName, + methodName, parametersJson, connectorTracker); + } + + } + + private LegacyChangeVariablesInvocation parseLegacyChangeVariablesInvocation( + String connectorId, String interfaceName, String methodName, + LegacyChangeVariablesInvocation previousInvocation, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + if (parametersJson.length() != 2) { + throw new JSONException( + "Invalid parameters in legacy change variables call. Expected 2, was " + + parametersJson.length()); + } + String variableName = parametersJson.getString(0); + UidlValue uidlValue = (UidlValue) JsonCodec.decodeInternalType( + UidlValue.class, true, parametersJson.get(1), connectorTracker); + + Object value = uidlValue.getValue(); + + if (previousInvocation != null + && previousInvocation.getConnectorId().equals(connectorId)) { + previousInvocation.setVariableChange(variableName, value); + return null; + } else { + return new LegacyChangeVariablesInvocation(connectorId, + variableName, value); + } + } + + private ServerRpcMethodInvocation parseServerRpcInvocation( + String connectorId, String interfaceName, String methodName, + JSONArray parametersJson, ConnectorTracker connectorTracker) + throws JSONException { + ClientConnector connector = connectorTracker.getConnector(connectorId); + + ServerRpcManager<?> rpcManager = connector.getRpcManager(interfaceName); + if (rpcManager == null) { + /* + * Security: Don't even decode the json parameters if no RpcManager + * corresponding to the received method invocation has been + * registered. + */ + getLogger().warning( + "Ignoring RPC call to " + interfaceName + "." + methodName + + " in connector " + connector.getClass().getName() + + "(" + connectorId + + ") as no RPC implementation is regsitered"); + return null; + } + + // Use interface from RpcManager instead of loading the class based on + // the string name to avoid problems with OSGi + Class<? extends ServerRpc> rpcInterface = rpcManager.getRpcInterface(); + + ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation( + connectorId, rpcInterface, methodName, parametersJson.length()); + + Object[] parameters = new Object[parametersJson.length()]; + Type[] declaredRpcMethodParameterTypes = invocation.getMethod() + .getGenericParameterTypes(); + + for (int j = 0; j < parametersJson.length(); ++j) { + Object parameterValue = parametersJson.get(j); + Type parameterType = declaredRpcMethodParameterTypes[j]; + parameters[j] = JsonCodec.decodeInternalOrCustomType(parameterType, + parameterValue, connectorTracker); + } + invocation.setParameters(parameters); + return invocation; + } + + protected void changeVariables(Object source, VariableOwner owner, + Map<String, Object> m) { + 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); + char[] buffer = new char[MAX_BUFFER_SIZE]; + + while (true) { + int read = reader.read(buffer); + if (read == -1) { + break; + } + sb.append(buffer, 0, read); + } + + return sb.toString(); + } + + private static final Logger getLogger() { + return Logger.getLogger(ServerRpcHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/ServletBootstrapHandler.java b/server/src/com/vaadin/server/communication/ServletBootstrapHandler.java new file mode 100644 index 0000000000..4b6517c30f --- /dev/null +++ b/server/src/com/vaadin/server/communication/ServletBootstrapHandler.java @@ -0,0 +1,48 @@ +/* + * 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 com.vaadin.server.BootstrapHandler; +import com.vaadin.server.VaadinServlet; +import com.vaadin.server.VaadinServletService; + +public class ServletBootstrapHandler extends BootstrapHandler { + @Override + protected String getServiceUrl(BootstrapContext context) { + String pathInfo = context.getRequest().getPathInfo(); + if (pathInfo == null) { + return null; + } else { + /* + * Make a relative URL to the servlet by adding one ../ for each + * path segment in pathInfo (i.e. the part of the requested path + * that comes after the servlet mapping) + */ + return VaadinServletService.getCancelingRelativePath(pathInfo); + } + } + + @Override + public String getThemeName(BootstrapContext context) { + String themeName = context.getRequest().getParameter( + VaadinServlet.URL_PARAMETER_THEME); + if (themeName == null) { + themeName = super.getThemeName(context); + } + return themeName; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/server/communication/ServletUIInitHandler.java b/server/src/com/vaadin/server/communication/ServletUIInitHandler.java new file mode 100644 index 0000000000..6286c161f2 --- /dev/null +++ b/server/src/com/vaadin/server/communication/ServletUIInitHandler.java @@ -0,0 +1,33 @@ +/* + * 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 com.vaadin.server.VaadinRequest; + +public class ServletUIInitHandler extends UIInitHandler { + + @Override + protected boolean isInitRequest(VaadinRequest request) { + return isUIInitRequest(request); + } + + public static boolean isUIInitRequest(VaadinRequest request) { + return "POST".equals(request.getMethod()) + && request + .getParameter(UIInitHandler.BROWSER_DETAILS_PARAMETER) != null; + } + +} diff --git a/server/src/com/vaadin/server/communication/SessionRequestHandler.java b/server/src/com/vaadin/server/communication/SessionRequestHandler.java new file mode 100644 index 0000000000..244cb0121d --- /dev/null +++ b/server/src/com/vaadin/server/communication/SessionRequestHandler.java @@ -0,0 +1,70 @@ +/* + * 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.io.IOException; +import java.util.ArrayList; + +import com.vaadin.server.RequestHandler; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; + +/** + * Handles a request by passing it to each registered {@link RequestHandler} in + * the session in turn until one produces a response. This method is used for + * requests that have not been handled by any specific functionality in the + * servlet/portlet. + * <p> + * The request handlers are invoked in the reverse order in which they were + * added to the session until a response has been produced. This means that the + * most recently added handler is used first and the first request handler that + * was added to the session is invoked towards the end unless any previous + * handler has already produced a response. + * </p> + * <p> + * The session is not locked during execution of the request handlers. The + * request handler can itself decide if it needs to lock the session or not. + * </p> + * + * @see VaadinSession#addRequestHandler(RequestHandler) + * @see RequestHandler + * + * @since 7.1 + */ +public class SessionRequestHandler implements RequestHandler { + + @Override + public boolean handleRequest(VaadinSession session, VaadinRequest request, + VaadinResponse response) throws IOException { + // Use a copy to avoid ConcurrentModificationException + session.lock(); + ArrayList<RequestHandler> requestHandlers; + try { + requestHandlers = new ArrayList<RequestHandler>( + session.getRequestHandlers()); + } finally { + session.unlock(); + } + for (RequestHandler handler : requestHandlers) { + if (handler.handleRequest(session, request, response)) { + return true; + } + } + // If not handled + return false; + } +} diff --git a/server/src/com/vaadin/server/communication/SharedStateWriter.java b/server/src/com/vaadin/server/communication/SharedStateWriter.java new file mode 100644 index 0000000000..fdf834387f --- /dev/null +++ b/server/src/com/vaadin/server/communication/SharedStateWriter.java @@ -0,0 +1,75 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.Collection; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.PaintException; +import com.vaadin.shared.communication.SharedState; +import com.vaadin.ui.UI; + +/** + * Serializes {@link SharedState shared state} changes to JSON. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class SharedStateWriter implements Serializable { + + /** + * Writes a JSON object containing the pending state changes of the dirty + * connectors of the given UI. + * + * @param ui + * The UI whose state changes should be written. + * @param writer + * The writer to use. + * @throws IOException + * If the serialization fails. + */ + public void write(UI ui, Writer writer) throws IOException { + + Collection<ClientConnector> dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + + JSONObject sharedStates = new JSONObject(); + for (ClientConnector connector : dirtyVisibleConnectors) { + // encode and send shared state + try { + JSONObject stateJson = connector.encodeState(); + + if (stateJson != null && stateJson.length() != 0) { + sharedStates.put(connector.getConnectorId(), stateJson); + } + } catch (JSONException e) { + throw new PaintException( + "Failed to serialize shared state for connector " + + connector.getClass().getName() + " (" + + connector.getConnectorId() + "): " + + e.getMessage(), e); + } + } + writer.write(sharedStates.toString()); + } +} diff --git a/server/src/com/vaadin/server/StreamingEndEventImpl.java b/server/src/com/vaadin/server/communication/StreamingEndEventImpl.java index 756cadee6b..f8cfb160be 100644 --- a/server/src/com/vaadin/server/StreamingEndEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingEndEventImpl.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; import com.vaadin.server.StreamVariable.StreamingEndEvent; diff --git a/server/src/com/vaadin/server/StreamingErrorEventImpl.java b/server/src/com/vaadin/server/communication/StreamingErrorEventImpl.java index 53e25399cd..9d9a19e4fe 100644 --- a/server/src/com/vaadin/server/StreamingErrorEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingErrorEventImpl.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; import com.vaadin.server.StreamVariable.StreamingErrorEvent; diff --git a/server/src/com/vaadin/server/StreamingProgressEventImpl.java b/server/src/com/vaadin/server/communication/StreamingProgressEventImpl.java index 610cd30c13..69f3bfb29c 100644 --- a/server/src/com/vaadin/server/StreamingProgressEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingProgressEventImpl.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; import com.vaadin.server.StreamVariable.StreamingProgressEvent; diff --git a/server/src/com/vaadin/server/StreamingStartEventImpl.java b/server/src/com/vaadin/server/communication/StreamingStartEventImpl.java index 3cd41bbb6d..bd16f08801 100644 --- a/server/src/com/vaadin/server/StreamingStartEventImpl.java +++ b/server/src/com/vaadin/server/communication/StreamingStartEventImpl.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.server; +package com.vaadin.server.communication; import com.vaadin.server.StreamVariable.StreamingStartEvent; diff --git a/server/src/com/vaadin/server/communication/UIInitHandler.java b/server/src/com/vaadin/server/communication/UIInitHandler.java new file mode 100644 index 0000000000..e4b5360b49 --- /dev/null +++ b/server/src/com/vaadin/server/communication/UIInitHandler.java @@ -0,0 +1,304 @@ +/* + * 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.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; + +import org.json.JSONException; +import org.json.JSONObject; + +import com.vaadin.annotations.PreserveOnRefresh; +import com.vaadin.server.LegacyApplicationUIProvider; +import com.vaadin.server.SynchronizedRequestHandler; +import com.vaadin.server.UIClassSelectionEvent; +import com.vaadin.server.UICreateEvent; +import com.vaadin.server.UIProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ApplicationConstants; +import com.vaadin.shared.communication.PushMode; +import com.vaadin.shared.ui.ui.UIConstants; +import com.vaadin.ui.UI; + +/** + * Handles an initial request from the client to initialize a {@link UI}. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public abstract class UIInitHandler extends SynchronizedRequestHandler { + + public static final String BROWSER_DETAILS_PARAMETER = "v-browserDetails"; + + protected abstract boolean isInitRequest(VaadinRequest request); + + @Override + public boolean synchronizedHandleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) throws IOException { + if (!isInitRequest(request)) { + return false; + } + + StringWriter stringWriter = new StringWriter(); + + try { + assert UI.getCurrent() == null; + + // Set browser information from the request + session.getBrowser().updateRequestDetails(request); + + UI uI = getBrowserDetailsUI(request, session); + + session.getCommunicationManager().repaintAll(uI); + + JSONObject params = new JSONObject(); + params.put(UIConstants.UI_ID_PARAMETER, uI.getUIId()); + String initialUIDL = getInitialUidl(request, uI); + params.put("uidl", initialUIDL); + + stringWriter.write(params.toString()); + } catch (JSONException e) { + throw new IOException("Error producing initial UIDL", e); + } finally { + stringWriter.close(); + } + + return commitJsonResponse(request, response, stringWriter.toString()); + } + + /** + * Commit the JSON response. We can't write immediately to the output stream + * as we want to write only a critical notification if something goes wrong + * during the response handling. + * + * @param request + * The request that resulted in this response + * @param response + * The response to write to + * @param json + * The JSON to write + * @return true if the JSON was written successfully, false otherwise + * @throws IOException + * If there was an exception while writing to the output + */ + 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"); + + // Ensure that the browser does not cache UIDL responses. + // iOS 6 Safari requires this (#9732) + response.setHeader("Cache-Control", "no-cache"); + + // NOTE! GateIn requires, for some weird reason, getOutputStream + // to be used instead of getWriter() (it seems to interpret + // application/json as a binary content type) + OutputStreamWriter outputWriter = new OutputStreamWriter( + response.getOutputStream(), "UTF-8"); + try { + outputWriter.write(json); + // NOTE GateIn requires the buffers to be flushed to work + outputWriter.flush(); + } finally { + outputWriter.close(); + } + + return true; + } + + private UI getBrowserDetailsUI(VaadinRequest request, VaadinSession session) { + VaadinService vaadinService = request.getService(); + + List<UIProvider> uiProviders = session.getUIProviders(); + + UIClassSelectionEvent classSelectionEvent = new UIClassSelectionEvent( + request); + + UIProvider provider = null; + Class<? extends UI> uiClass = null; + for (UIProvider p : uiProviders) { + // Check for existing LegacyWindow + if (p instanceof LegacyApplicationUIProvider) { + LegacyApplicationUIProvider legacyProvider = (LegacyApplicationUIProvider) p; + + UI existingUi = legacyProvider + .getExistingUI(classSelectionEvent); + if (existingUi != null) { + reinitUI(existingUi, request); + return existingUi; + } + } + + uiClass = p.getUIClass(classSelectionEvent); + if (uiClass != null) { + provider = p; + break; + } + } + + if (provider == null || uiClass == null) { + 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 + + Integer retainedUIId = retainOnRefreshUIs.get(windowName); + + if (retainedUIId != null) { + UI retainedUI = session.getUIById(retainedUIId.intValue()); + if (uiClass.isInstance(retainedUI)) { + reinitUI(retainedUI, request); + return retainedUI; + } else { + getLogger().info( + "Not using retained UI in " + windowName + + " because retained UI was of type " + + retainedUI.getClass() + " but " + uiClass + + " is expected for the request."); + } + } + } + + // No existing UI found - go on by creating and initializing one + + Integer uiId = Integer.valueOf(session.getNextUIid()); + + // Explicit Class.cast to detect if the UIProvider does something + // unexpected + UICreateEvent event = new UICreateEvent(request, uiClass, uiId); + UI ui = uiClass.cast(provider.createInstance(event)); + + // Initialize some fields for a newly created UI + if (ui.getSession() != session) { + // Session already set for LegacyWindow + ui.setSession(session); + } + + PushMode pushMode = provider.getPushMode(event); + if (pushMode == null) { + pushMode = session.getService().getDeploymentConfiguration() + .getPushMode(); + } + ui.setPushMode(pushMode); + + // Set thread local here so it is available in init + UI.setCurrent(ui); + + ui.doInit(request, uiId.intValue()); + + 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); + } + } + + return ui; + } + + /** + * Updates a UI that has already been initialized but is now loaded again, + * e.g. because of {@link PreserveOnRefresh}. + * + * @param ui + * @param request + */ + 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); + } + } + + /** + * Generates the initial UIDL message that can e.g. be included in a html + * page to avoid a separate round trip just for getting the UIDL. + * + * @param request + * the request that caused the initialization + * @param uI + * the UI for which the UIDL should be generated + * @return a string with the initial UIDL message + * @throws JSONException + * if an exception occurs while encoding output + * @throws IOException + */ + protected String getInitialUidl(VaadinRequest request, UI uI) + throws JSONException, IOException { + StringWriter writer = new StringWriter(); + try { + writer.write("{"); + + VaadinSession session = uI.getSession(); + if (session.getConfiguration().isXsrfProtectionEnabled()) { + writer.write(getSecurityKeyUIDL(session)); + } + new UidlWriter().write(uI, writer, true, false, false); + writer.write("}"); + + String initialUIDL = writer.toString(); + getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL); + return initialUIDL; + } finally { + writer.close(); + } + } + + /** + * Gets the security key (and generates one if needed) as UIDL. + * + * @param session + * the vaadin session to which the security key belongs + * @return the security key UIDL or "" if the feature is turned off + */ + private static String getSecurityKeyUIDL(VaadinSession session) { + String seckey = session.getCsrfToken(); + + return "\"" + ApplicationConstants.UIDL_SECURITY_TOKEN_ID + "\":\"" + + seckey + "\","; + } + + private static final Logger getLogger() { + return Logger.getLogger(UIInitHandler.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/communication/UidlRequestHandler.java b/server/src/com/vaadin/server/communication/UidlRequestHandler.java new file mode 100644 index 0000000000..73ff92f8bd --- /dev/null +++ b/server/src/com/vaadin/server/communication/UidlRequestHandler.java @@ -0,0 +1,306 @@ +/* + * 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.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.Constants; +import com.vaadin.server.LegacyCommunicationManager.InvalidUIDLSecurityKeyException; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.SessionExpiredHandler; +import com.vaadin.server.SynchronizedRequestHandler; +import com.vaadin.server.SystemMessages; +import com.vaadin.server.VaadinRequest; +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.Version; +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; + +/** + * Processes a UIDL request from the client. + * + * Uses {@link ServerRpcHandler} to execute client-to-server RPC invocations and + * {@link UidlWriter} to write state changes and client RPC calls back to the + * client. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UidlRequestHandler extends SynchronizedRequestHandler implements + SessionExpiredHandler { + + public static final String UIDL_PATH = "UIDL/"; + + private ServerRpcHandler rpcHandler = new ServerRpcHandler(); + + public UidlRequestHandler() { + } + + @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 + // really don't want to see it in the server logs though + response.getWriter().write( + getUINotFoundErrorJSON(session.getService(), request)); + return true; + } + + checkWidgetsetVersion(request); + String requestThemeName = request.getParameter("theme"); + ClientConnector highlightedConnector; + // repaint requested or session has timed out and new one is created + boolean repaintAll; + + // TODO PUSH repaintAll, analyzeLayouts, highlightConnector should be + // part of the message payload to make the functionality transport + // agnostic + + repaintAll = (request + .getParameter(ApplicationConstants.URL_PARAMETER_REPAINT_ALL) != null); + + boolean analyzeLayouts = false; + if (repaintAll) { + // analyzing can be done only with repaintAll + analyzeLayouts = (request + .getParameter(ApplicationConstants.PARAM_ANALYZE_LAYOUTS) != null); + + String pid = request + .getParameter(ApplicationConstants.PARAM_HIGHLIGHT_CONNECTOR); + if (pid != null) { + highlightedConnector = uI.getConnectorTracker().getConnector( + pid); + highlightConnector(highlightedConnector); + } + } + + StringWriter stringWriter = new StringWriter(); + + try { + rpcHandler.handleRpc(uI, request.getReader(), request); + + if (repaintAll) { + session.getCommunicationManager().repaintAll(uI); + } + + writeUidl(request, response, uI, stringWriter, repaintAll, + analyzeLayouts); + } catch (JSONException e) { + getLogger().log(Level.SEVERE, "Error writing JSON to response", e); + // Refresh on client side + response.getWriter().write( + VaadinService.createCriticalNotificationJSON(null, null, + null, null)); + return true; + } catch (InvalidUIDLSecurityKeyException e) { + getLogger().log(Level.WARNING, + "Invalid security key received from {0}", + request.getRemoteHost()); + // Refresh on client side + response.getWriter().write( + VaadinService.createCriticalNotificationJSON(null, null, + null, null)); + return true; + } finally { + stringWriter.close(); + requestThemeName = null; + } + + return UIInitHandler.commitJsonResponse(request, response, + stringWriter.toString()); + } + + /** + * Checks that the version reported by the client (widgetset) matches that + * of the server. + * + * @param request + */ + private void checkWidgetsetVersion(VaadinRequest request) { + String widgetsetVersion = request.getParameter("v-wsver"); + if (widgetsetVersion == null) { + // Only check when the widgetset version is reported. It is reported + // in the first UIDL request (not the initial request as it is a + // plain GET /) + return; + } + + if (!Version.getFullVersion().equals(widgetsetVersion)) { + getLogger().warning( + String.format(Constants.WIDGETSET_MISMATCH_INFO, + Version.getFullVersion(), widgetsetVersion)); + } + } + + private void writeUidl(VaadinRequest request, VaadinResponse response, + UI ui, Writer writer, boolean repaintAll, boolean analyzeLayouts) + throws IOException, JSONException { + openJsonMessage(writer, response); + + new UidlWriter().write(ui, writer, repaintAll, analyzeLayouts, false); + + closeJsonMessage(writer); + } + + protected void closeJsonMessage(Writer outWriter) throws IOException { + outWriter.write("}]"); + } + + /** + * Writes the opening of JSON message to be sent to client. + * + * @param outWriter + * @param response + * @throws IOException + */ + protected void openJsonMessage(Writer outWriter, VaadinResponse response) + throws IOException { + // some dirt to prevent cross site scripting + outWriter.write("for(;;);[{"); + } + + // TODO Does this belong here? + protected void highlightConnector(ClientConnector highlightedConnector) { + StringBuilder sb = new StringBuilder(); + sb.append("*** Debug details of a connector: *** \n"); + sb.append("Type: "); + sb.append(highlightedConnector.getClass().getName()); + sb.append("\nId:"); + sb.append(highlightedConnector.getConnectorId()); + if (highlightedConnector instanceof Component) { + Component component = (Component) highlightedConnector; + if (component.getCaption() != null) { + sb.append("\nCaption:"); + sb.append(component.getCaption()); + } + } + printHighlightedConnectorHierarchy(sb, highlightedConnector); + getLogger().info(sb.toString()); + } + + // TODO Does this belong here? + protected void printHighlightedConnectorHierarchy(StringBuilder sb, + ClientConnector connector) { + LinkedList<ClientConnector> h = new LinkedList<ClientConnector>(); + h.add(connector); + ClientConnector parent = connector.getParent(); + while (parent != null) { + h.addFirst(parent); + parent = parent.getParent(); + } + + sb.append("\nConnector hierarchy:\n"); + VaadinSession session2 = connector.getUI().getSession(); + sb.append(session2.getClass().getName()); + sb.append("("); + sb.append(session2.getClass().getSimpleName()); + sb.append(".java"); + sb.append(":1)"); + int l = 1; + for (ClientConnector connector2 : h) { + sb.append("\n"); + for (int i = 0; i < l; i++) { + sb.append(" "); + } + l++; + Class<? extends ClientConnector> connectorClass = connector2 + .getClass(); + Class<?> topClass = connectorClass; + while (topClass.getEnclosingClass() != null) { + topClass = topClass.getEnclosingClass(); + } + sb.append(connectorClass.getName()); + sb.append("("); + sb.append(topClass.getSimpleName()); + sb.append(".java:1)"); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(UidlRequestHandler.class.getName()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.server.SessionExpiredHandler#handleSessionExpired(com.vaadin + * .server.VaadinRequest, com.vaadin.server.VaadinResponse) + */ + @Override + public boolean handleSessionExpired(VaadinRequest request, + VaadinResponse response) throws IOException { + if (!ServletPortletHelper.isUIDLRequest(request)) { + return false; + } + VaadinService service = request.getService(); + SystemMessages systemMessages = service.getSystemMessages( + ServletPortletHelper.findLocale(null, null, request), request); + + service.writeStringResponse(response, JsonConstants.JSON_CONTENT_TYPE, + VaadinService.createCriticalNotificationJSON( + systemMessages.getSessionExpiredCaption(), + systemMessages.getSessionExpiredMessage(), null, + systemMessages.getSessionExpiredURL())); + + return true; + } + + /** + * Returns the JSON which should be returned to the client when a request + * for a non-existent UI arrives. + * + * @param service + * The VaadinService + * @param vaadinRequest + * The request which triggered this, or null if not available + * @since 7.1 + * @return A JSON string + */ + static String getUINotFoundErrorJSON(VaadinService service, + VaadinRequest vaadinRequest) { + SystemMessages ci = service.getSystemMessages( + vaadinRequest.getLocale(), vaadinRequest); + // Session Expired is not really the correct message as the + // session exists but the requested UI does not. + // Using Communication Error for now. + String json = VaadinService.createCriticalNotificationJSON( + ci.getCommunicationErrorCaption(), + ci.getCommunicationErrorMessage(), null, + ci.getCommunicationErrorURL()); + + return json; + } + +} diff --git a/server/src/com/vaadin/server/communication/UidlWriter.java b/server/src/com/vaadin/server/communication/UidlWriter.java new file mode 100644 index 0000000000..fbe2fb86d5 --- /dev/null +++ b/server/src/com/vaadin/server/communication/UidlWriter.java @@ -0,0 +1,317 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONArray; +import org.json.JSONException; + +import com.vaadin.annotations.JavaScript; +import com.vaadin.annotations.StyleSheet; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.JsonPaintTarget; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.server.LegacyCommunicationManager.ClientCache; +import com.vaadin.server.SystemMessages; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.UI; + +/** + * Serializes pending server-side changes to UI state to JSON. This includes + * shared state, client RPC invocations, connector hierarchy changes, connector + * type information among others. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UidlWriter implements Serializable { + + /** + * Writes a JSON object containing all pending changes to the given UI. + * + * @param ui + * The {@link UI} whose changes to write + * @param writer + * The writer to use + * @param repaintAll + * Whether the client should re-render the whole UI. + * @param analyzeLayouts + * Whether detected layout problems should be logged. + * @param async + * True if this message is sent by the server asynchronously, + * false if it is a response to a client message. + * + * @throws IOException + * If the writing fails. + * @throws JSONException + * If the JSON serialization fails. + */ + public void write(UI ui, Writer writer, boolean repaintAll, + boolean analyzeLayouts, boolean async) throws IOException, + JSONException { + ArrayList<ClientConnector> dirtyVisibleConnectors = ui + .getConnectorTracker().getDirtyVisibleConnectors(); + VaadinSession session = ui.getSession(); + LegacyCommunicationManager manager = session.getCommunicationManager(); + // Paints components + ConnectorTracker uiConnectorTracker = ui.getConnectorTracker(); + getLogger().log(Level.FINE, "* Creating response to client"); + + getLogger().log( + Level.FINE, + "Found " + dirtyVisibleConnectors.size() + + " dirty connectors to paint"); + for (ClientConnector connector : dirtyVisibleConnectors) { + boolean initialized = uiConnectorTracker + .isClientSideInitialized(connector); + connector.beforeClientResponse(!initialized); + } + + uiConnectorTracker.setWritingResponse(true); + try { + writer.write("\"changes\" : "); + + JsonPaintTarget paintTarget = new JsonPaintTarget(manager, writer, + !repaintAll); + + new LegacyUidlWriter().write(ui, writer, paintTarget); + + paintTarget.close(); + writer.write(", "); // close changes + + // send shared state to client + + // for now, send the complete state of all modified and new + // components + + // Ideally, all this would be sent before "changes", but that causes + // complications with legacy components that create sub-components + // in their paint phase. Nevertheless, this will be processed on the + // client after component creation but before legacy UIDL + // processing. + + writer.write("\"state\":"); + new SharedStateWriter().write(ui, writer); + writer.write(", "); // close states + + // TODO This should be optimized. The type only needs to be + // sent once for each connector id + on refresh. Use the same cache + // as + // widget mapping + + writer.write("\"types\":"); + new ConnectorTypeWriter().write(ui, writer, paintTarget); + writer.write(", "); // close states + + // Send update hierarchy information to the client. + + // This could be optimized aswell to send only info if hierarchy has + // actually changed. Much like with the shared state. Note though + // that an empty hierarchy is information aswell (e.g. change from 1 + // child to 0 children) + + writer.write("\"hierarchy\":"); + new ConnectorHierarchyWriter().write(ui, writer); + writer.write(", "); // close hierarchy + + // send server to client RPC calls for components in the UI, in call + // order + + // collect RPC calls from components in the UI in the order in + // which they were performed, remove the calls from components + + writer.write("\"rpc\" : "); + new ClientRpcWriter().write(ui, writer); + writer.write(", "); // close rpc + + uiConnectorTracker.markAllConnectorsClean(); + + writer.write("\"meta\" : "); + + SystemMessages messages = ui.getSession().getService() + .getSystemMessages(ui.getLocale(), null); + // TODO hilightedConnector + new MetadataWriter().write(ui, writer, repaintAll, analyzeLayouts, + async, null, messages); + writer.write(", "); + + writer.write("\"resources\" : "); + new ResourceWriter().write(ui, writer, paintTarget); + + Collection<Class<? extends ClientConnector>> usedClientConnectors = paintTarget + .getUsedClientConnectors(); + boolean typeMappingsOpen = false; + ClientCache clientCache = manager.getClientCache(ui); + + List<Class<? extends ClientConnector>> newConnectorTypes = new ArrayList<Class<? extends ClientConnector>>(); + + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (clientCache.cache(class1)) { + // client does not know the mapping key for this type, send + // mapping to client + newConnectorTypes.add(class1); + + if (!typeMappingsOpen) { + typeMappingsOpen = true; + writer.write(", \"typeMappings\" : { "); + } else { + writer.write(" , "); + } + String canonicalName = class1.getCanonicalName(); + writer.write("\""); + writer.write(canonicalName); + writer.write("\" : "); + writer.write(manager.getTagForType(class1)); + } + } + if (typeMappingsOpen) { + writer.write(" }"); + } + + // TODO PUSH Refactor to TypeInheritanceWriter or something + boolean typeInheritanceMapOpen = false; + if (typeMappingsOpen) { + // send the whole type inheritance map if any new mappings + for (Class<? extends ClientConnector> class1 : usedClientConnectors) { + if (!ClientConnector.class.isAssignableFrom(class1 + .getSuperclass())) { + continue; + } + if (!typeInheritanceMapOpen) { + typeInheritanceMapOpen = true; + writer.write(", \"typeInheritanceMap\" : { "); + } else { + writer.write(" , "); + } + writer.write("\""); + writer.write(manager.getTagForType(class1)); + writer.write("\" : "); + writer.write(manager + .getTagForType((Class<? extends ClientConnector>) class1 + .getSuperclass())); + } + if (typeInheritanceMapOpen) { + writer.write(" }"); + } + } + + // TODO Refactor to DependencyWriter or something + /* + * Ensure super classes come before sub classes to get script + * dependency order right. Sub class @JavaScript might assume that + * + * @JavaScript defined by super class is already loaded. + */ + Collections.sort(newConnectorTypes, new Comparator<Class<?>>() { + @Override + public int compare(Class<?> o1, Class<?> o2) { + // TODO optimize using Class.isAssignableFrom? + return hierarchyDepth(o1) - hierarchyDepth(o2); + } + + private int hierarchyDepth(Class<?> type) { + if (type == Object.class) { + return 0; + } else { + return hierarchyDepth(type.getSuperclass()) + 1; + } + } + }); + + List<String> scriptDependencies = new ArrayList<String>(); + List<String> styleDependencies = new ArrayList<String>(); + + for (Class<? extends ClientConnector> class1 : newConnectorTypes) { + JavaScript jsAnnotation = class1 + .getAnnotation(JavaScript.class); + if (jsAnnotation != null) { + for (String uri : jsAnnotation.value()) { + scriptDependencies.add(manager.registerDependency(uri, + class1)); + } + } + + StyleSheet styleAnnotation = class1 + .getAnnotation(StyleSheet.class); + if (styleAnnotation != null) { + for (String uri : styleAnnotation.value()) { + styleDependencies.add(manager.registerDependency(uri, + class1)); + } + } + } + + // Include script dependencies in output if there are any + if (!scriptDependencies.isEmpty()) { + writer.write(", \"scriptDependencies\": " + + new JSONArray(scriptDependencies).toString()); + } + + // Include style dependencies in output if there are any + if (!styleDependencies.isEmpty()) { + writer.write(", \"styleDependencies\": " + + new JSONArray(styleDependencies).toString()); + } + + // add any pending locale definitions requested by the client + writer.write(", \"locales\": "); + manager.printLocaleDeclarations(writer); + + if (manager.getDragAndDropService() != null) { + manager.getDragAndDropService().printJSONResponse(writer); + } + + for (ClientConnector connector : dirtyVisibleConnectors) { + uiConnectorTracker.markClientSideInitialized(connector); + } + + assert (uiConnectorTracker.getDirtyConnectors().isEmpty()) : "Connectors have been marked as dirty during the end of the paint phase. This is most certainly not intended."; + + writePerformanceData(ui, writer); + } finally { + uiConnectorTracker.setWritingResponse(false); + uiConnectorTracker.cleanConnectorMap(); + } + } + + /** + * Adds the performance timing data (used by TestBench 3) to the UIDL + * response. + * + * @throws IOException + */ + private void writePerformanceData(UI ui, Writer writer) throws IOException { + writer.write(String.format(", \"timings\":[%d, %d]", ui.getSession() + .getCumulativeRequestDuration(), ui.getSession() + .getLastRequestDuration())); + } + + private static final Logger getLogger() { + return Logger.getLogger(UidlWriter.class.getName()); + } +} diff --git a/server/src/com/vaadin/server/themeutils/SASSAddonImportFileCreator.java b/server/src/com/vaadin/server/themeutils/SASSAddonImportFileCreator.java new file mode 100644 index 0000000000..f199c347eb --- /dev/null +++ b/server/src/com/vaadin/server/themeutils/SASSAddonImportFileCreator.java @@ -0,0 +1,200 @@ +/* + * 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.themeutils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import com.vaadin.server.widgetsetutils.ClassPathExplorer; +import com.vaadin.server.widgetsetutils.ClassPathExplorer.LocationInfo; + +/** + * Helper class for managing the addon imports and creating an a SCSS file for + * importing all your addon themes. The helper method searches the classpath for + * Vaadin addons and uses the 'Vaadin-Themes' metadata to create the imports. + * + * <p> + * The addons.scss is always overwritten when this tool is invoked. + * </p> + * + * @since 7.1 + */ +public class SASSAddonImportFileCreator { + + private static final String ADDON_IMPORTS_FILE = "addons.scss"; + + private static final String ADDON_IMPORTS_FILE_TEXT = "This file is automatically managed and " + + "will be overwritten from time to time."; + + /** + * + * @param args + * Theme directory where the addons.scss file should be created + */ + public static void main(String[] args) throws IOException { + if (args.length == 0) { + printUsage(); + } else { + String themeDirectory = args[0]; + updateTheme(themeDirectory); + } + } + + /** + * Updates a themes addons.scss with the addon themes found on the classpath + * + * @param themeDirectory + * The target theme directory + */ + public static void updateTheme(String themeDirectory) throws IOException { + + File addonImports = new File(themeDirectory, ADDON_IMPORTS_FILE); + + if (!addonImports.exists()) { + + // Ensure directory exists + addonImports.getParentFile().mkdirs(); + + // Ensure file exists + addonImports.createNewFile(); + } + + LocationInfo info = ClassPathExplorer + .getAvailableWidgetSetsAndStylesheets(); + + try { + PrintStream printStream = new PrintStream(new FileOutputStream( + addonImports)); + + printStream.println("/* " + ADDON_IMPORTS_FILE_TEXT + " */"); + + printStream.println("/* Do not manually edit this file. */"); + + printStream.println(); + + Map<String, URL> addonThemes = info.getAddonStyles(); + + // Sort addon styles so that CSS imports are first and SCSS import + // last + List<String> paths = new ArrayList<String>(addonThemes.keySet()); + Collections.sort(paths, new Comparator<String>() { + + @Override + public int compare(String path1, String path2) { + if (path1.toLowerCase().endsWith(".css") + && path2.toLowerCase().endsWith(".scss")) { + return -1; + } + if (path1.toLowerCase().endsWith(".scss") + && path2.toLowerCase().endsWith(".css")) { + return 1; + } + return 0; + } + }); + + List<String> mixins = new ArrayList<String>(); + for (String path : paths) { + mixins.addAll(addImport(printStream, path, + addonThemes.get(path))); + printStream.println(); + } + + createAddonsMixin(printStream, mixins); + + } catch (FileNotFoundException e) { + // Should not happen since file is checked before this + e.printStackTrace(); + } + } + + private static List<String> addImport(PrintStream stream, String file, + URL location) { + + // Add import comment + printImportComment(stream, location); + + List<String> foundMixins = new ArrayList<String>(); + + if (file.endsWith(".css")) { + stream.print("@import url(\"../../../" + file + "\");\n"); + } else { + // Assume SASS + stream.print("@import \"../../../" + file + "\";\n"); + + // Convention is to name the mixin after the stylesheet. Strip + // .scss from filename + String mixin = file.substring(file.lastIndexOf("/") + 1, + file.length() - ".scss".length()); + + foundMixins.add(mixin); + } + + stream.println(); + + return foundMixins; + } + + private static void printImportComment(PrintStream stream, URL location) { + + // file:/absolute/path/to/addon.jar!/ + String path = location.getPath(); + + try { + // Try to parse path for better readability + path = path.substring(path.lastIndexOf(":") + 1, + path.lastIndexOf("!")); + + // Extract jar archive filename + path = path.substring(path.lastIndexOf("/") + 1); + + } catch (Exception e) { + // Parsing failed but no worries, we then use whatever + // location.getPath() returns + } + + stream.println("/* Provided by " + path + " */"); + } + + private static void createAddonsMixin(PrintStream stream, + List<String> mixins) { + + stream.println("/* Import and include this mixin into your project theme to include the addon themes */"); + stream.println("@mixin addons {"); + for (String addon : mixins) { + stream.println("\t@include " + addon + ";"); + } + stream.println("}"); + stream.println(); + } + + private static void printUsage() { + String className = SASSAddonImportFileCreator.class.getSimpleName(); + PrintStream o = System.out; + o.println(className + " usage:"); + o.println(); + o.println("./" + className + " [Path to target theme folder]"); + } +} diff --git a/server/src/com/vaadin/server/widgetsetutils/ClassPathExplorer.java b/server/src/com/vaadin/server/widgetsetutils/ClassPathExplorer.java new file mode 100644 index 0000000000..cc04e50b3c --- /dev/null +++ b/server/src/com/vaadin/server/widgetsetutils/ClassPathExplorer.java @@ -0,0 +1,538 @@ +/* + * 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.widgetsetutils; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility class to collect widgetset related information from classpath. + * Utility will seek all directories from classpaths, and jar files having + * "Vaadin-Widgetsets" key in their manifest file. + * <p> + * Used by WidgetMapGenerator and ide tools to implement some monkey coding for + * you. + * <p> + * Developer notice: If you end up reading this comment, I guess you have faced + * a sluggish performance of widget compilation or unreliable detection of + * components in your classpaths. The thing you might be able to do is to use + * annotation processing tool like apt to generate the needed information. Then + * either use that information in {@link WidgetMapGenerator} or create the + * appropriate monkey code for gwt directly in annotation processor and get rid + * of {@link WidgetMapGenerator}. Using annotation processor might be a good + * idea when dropping Java 1.5 support (integrated to javac in 6). + * + */ +public class ClassPathExplorer { + + private static final String VAADIN_ADDON_VERSION_ATTRIBUTE = "Vaadin-Package-Version"; + + /** + * File filter that only accepts directories. + */ + private final static FileFilter DIRECTORIES_ONLY = new FileFilter() { + @Override + public boolean accept(File f) { + if (f.exists() && f.isDirectory()) { + return true; + } else { + return false; + } + } + }; + + /** + * Contains information about widgetsets and themes found on the classpath + * + * @since 7.1 + */ + public static class LocationInfo { + + private final Map<String, URL> widgetsets; + + private final Map<String, URL> addonStyles; + + public LocationInfo(Map<String, URL> widgetsets, Map<String, URL> themes) { + this.widgetsets = widgetsets; + addonStyles = themes; + } + + public Map<String, URL> getWidgetsets() { + return widgetsets; + } + + public Map<String, URL> getAddonStyles() { + return addonStyles; + } + + } + + /** + * Raw class path entries as given in the java class path string. Only + * entries that could include widgets/widgetsets are listed (primarily + * directories, Vaadin JARs and add-on JARs). + */ + private static List<String> rawClasspathEntries = getRawClasspathEntries(); + + /** + * Map from identifiers (either a package name preceded by the path and a + * slash, or a URL for a JAR file) to the corresponding URLs. This is + * constructed from the class path. + */ + private static Map<String, URL> classpathLocations = getClasspathLocations(rawClasspathEntries); + + /** + * No instantiation from outside, callable methods are static. + */ + private ClassPathExplorer() { + } + + /** + * Finds the names and locations of widgetsets available on the class path. + * + * @return map from widgetset classname to widgetset location URL + * @deprecated Use {@link #getAvailableWidgetSetsAndStylesheets()} instead + */ + @Deprecated + public static Map<String, URL> getAvailableWidgetSets() { + return getAvailableWidgetSetsAndStylesheets().getWidgetsets(); + } + + /** + * Finds the names and locations of widgetsets and themes available on the + * class path. + * + * @return + */ + public static LocationInfo getAvailableWidgetSetsAndStylesheets() { + long start = System.currentTimeMillis(); + Map<String, URL> widgetsets = new HashMap<String, URL>(); + Map<String, URL> themes = new HashMap<String, URL>(); + Set<String> keySet = classpathLocations.keySet(); + for (String location : keySet) { + searchForWidgetSetsAndAddonStyles(location, widgetsets, themes); + } + long end = System.currentTimeMillis(); + + StringBuilder sb = new StringBuilder(); + sb.append("Widgetsets found from classpath:\n"); + for (String ws : widgetsets.keySet()) { + sb.append("\t"); + sb.append(ws); + sb.append(" in "); + sb.append(widgetsets.get(ws)); + sb.append("\n"); + } + + sb.append("Addon styles found from classpath:\n"); + for (String theme : themes.keySet()) { + sb.append("\t"); + sb.append(theme); + sb.append(" in "); + sb.append(themes.get(theme)); + sb.append("\n"); + } + + final Logger logger = getLogger(); + logger.info(sb.toString()); + logger.info("Search took " + (end - start) + "ms"); + return new LocationInfo(widgetsets, themes); + } + + /** + * Finds all GWT modules / Vaadin widgetsets and Addon styles in a valid + * location. + * + * If the location is a directory, all GWT modules (files with the + * ".gwt.xml" extension) are added to widgetsets. + * + * If the location is a JAR file, the comma-separated values of the + * "Vaadin-Widgetsets" attribute in its manifest are added to widgetsets. + * + * @param locationString + * an entry in {@link #classpathLocations} + * @param widgetsets + * a map from widgetset name (including package, with dots as + * separators) to a URL (see {@link #classpathLocations}) - new + * entries are added to this map + */ + private static void searchForWidgetSetsAndAddonStyles( + String locationString, Map<String, URL> widgetsets, + Map<String, URL> addonStyles) { + + URL location = classpathLocations.get(locationString); + File directory = new File(location.getFile()); + + if (directory.exists() && !directory.isHidden()) { + // Get the list of the files contained in the directory + String[] files = directory.list(); + for (int i = 0; i < files.length; i++) { + // we are only interested in .gwt.xml files + if (!files[i].endsWith(".gwt.xml")) { + continue; + } + + // remove the .gwt.xml extension + String classname = files[i].substring(0, files[i].length() - 8); + String packageName = locationString.substring(locationString + .lastIndexOf("/") + 1); + classname = packageName + "." + classname; + + if (!WidgetSetBuilder.isWidgetset(classname)) { + // Only return widgetsets and not GWT modules to avoid + // comparing modules and widgetsets + continue; + } + + if (!widgetsets.containsKey(classname)) { + String packagePath = packageName.replaceAll("\\.", "/"); + String basePath = location.getFile().replaceAll( + "/" + packagePath + "$", ""); + try { + URL url = new URL(location.getProtocol(), + location.getHost(), location.getPort(), + basePath); + widgetsets.put(classname, url); + } catch (MalformedURLException e) { + // should never happen as based on an existing URL, + // only changing end of file name/path part + getLogger().log(Level.SEVERE, + "Error locating the widgetset " + classname, e); + } + } + } + } else { + + try { + // check files in jar file, entries will list all directories + // and files in jar + + URLConnection openConnection = location.openConnection(); + if (openConnection instanceof JarURLConnection) { + JarURLConnection conn = (JarURLConnection) openConnection; + + JarFile jarFile = conn.getJarFile(); + + Manifest manifest = jarFile.getManifest(); + if (manifest == null) { + // No manifest so this is not a Vaadin Add-on + return; + } + + // Check for widgetset attribute + String value = manifest.getMainAttributes().getValue( + "Vaadin-Widgetsets"); + if (value != null) { + String[] widgetsetNames = value.split(","); + for (int i = 0; i < widgetsetNames.length; i++) { + String widgetsetname = widgetsetNames[i].trim(); + if (!widgetsetname.equals("")) { + widgetsets.put(widgetsetname, location); + } + } + } + + // Check for theme attribute + value = manifest.getMainAttributes().getValue( + "Vaadin-Stylesheets"); + if (value != null) { + String[] stylesheets = value.split(","); + for (int i = 0; i < stylesheets.length; i++) { + String stylesheet = stylesheets[i].trim(); + if (!stylesheet.equals("")) { + addonStyles.put(stylesheet, location); + } + } + } + } + } catch (IOException e) { + getLogger().log(Level.WARNING, "Error parsing jar file", e); + } + + } + } + + /** + * Splits the current class path into entries, and filters them accepting + * directories, Vaadin add-on JARs with widgetsets and Vaadin JARs. + * + * Some other non-JAR entries may also be included in the result. + * + * @return filtered list of class path entries + */ + private final static List<String> getRawClasspathEntries() { + // try to keep the order of the classpath + List<String> locations = new ArrayList<String>(); + + String pathSep = System.getProperty("path.separator"); + String classpath = System.getProperty("java.class.path"); + + if (classpath.startsWith("\"")) { + classpath = classpath.substring(1); + } + if (classpath.endsWith("\"")) { + classpath = classpath.substring(0, classpath.length() - 1); + } + + getLogger().log(Level.FINE, "Classpath: {0}", classpath); + + String[] split = classpath.split(pathSep); + for (int i = 0; i < split.length; i++) { + String classpathEntry = split[i]; + if (acceptClassPathEntry(classpathEntry)) { + locations.add(classpathEntry); + } + } + + return locations; + } + + /** + * Determine every URL location defined by the current classpath, and it's + * associated package name. + * + * See {@link #classpathLocations} for information on output format. + * + * @param rawClasspathEntries + * raw class path entries as split from the Java class path + * string + * @return map of classpath locations, see {@link #classpathLocations} + */ + private final static Map<String, URL> getClasspathLocations( + List<String> rawClasspathEntries) { + long start = System.currentTimeMillis(); + // try to keep the order of the classpath + Map<String, URL> locations = new LinkedHashMap<String, URL>(); + for (String classpathEntry : rawClasspathEntries) { + File file = new File(classpathEntry); + include(null, file, locations); + } + long end = System.currentTimeMillis(); + Logger logger = getLogger(); + if (logger.isLoggable(Level.FINE)) { + logger.fine("getClassPathLocations took " + (end - start) + "ms"); + } + return locations; + } + + /** + * Checks a class path entry to see whether it can contain widgets and + * widgetsets. + * + * All directories are automatically accepted. JARs are accepted if they + * have the "Vaadin-Widgetsets" attribute in their manifest or the JAR file + * name contains "vaadin-" or ".vaadin.". + * + * Also other non-JAR entries may be accepted, the caller should be prepared + * to handle them. + * + * @param classpathEntry + * class path entry string as given in the Java class path + * @return true if the entry should be considered when looking for widgets + * or widgetsets + */ + private static boolean acceptClassPathEntry(String classpathEntry) { + if (!classpathEntry.endsWith(".jar")) { + // accept all non jars (practically directories) + return true; + } else { + // accepts jars that comply with vaadin-component packaging + // convention (.vaadin. or vaadin- as distribution packages), + if (classpathEntry.contains("vaadin-") + || classpathEntry.contains(".vaadin.")) { + return true; + } else { + URL url; + try { + url = new URL("file:" + + new File(classpathEntry).getCanonicalPath()); + url = new URL("jar:" + url.toExternalForm() + "!/"); + JarURLConnection conn = (JarURLConnection) url + .openConnection(); + getLogger().fine(url.toString()); + JarFile jarFile = conn.getJarFile(); + Manifest manifest = jarFile.getManifest(); + if (manifest != null) { + Attributes mainAttributes = manifest + .getMainAttributes(); + if (mainAttributes.getValue("Vaadin-Widgetsets") != null) { + return true; + } + if (mainAttributes.getValue("Vaadin-Stylesheets") != null) { + return true; + } + } + } catch (MalformedURLException e) { + getLogger().log(Level.FINEST, "Failed to inspect JAR file", + e); + } catch (IOException e) { + getLogger().log(Level.FINEST, "Failed to inspect JAR file", + e); + } + + return false; + } + } + } + + /** + * Recursively add subdirectories and jar files to locations - see + * {@link #classpathLocations}. + * + * @param name + * @param file + * @param locations + */ + private final static void include(String name, File file, + Map<String, URL> locations) { + if (!file.exists()) { + return; + } + if (!file.isDirectory()) { + // could be a JAR file + includeJar(file, locations); + return; + } + + if (file.isHidden() || file.getPath().contains(File.separator + ".")) { + return; + } + + if (name == null) { + name = ""; + } else { + name += "."; + } + + // add all directories recursively + File[] dirs = file.listFiles(DIRECTORIES_ONLY); + for (int i = 0; i < dirs.length; i++) { + try { + // add the present directory + if (!dirs[i].isHidden() + && !dirs[i].getPath().contains(File.separator + ".")) { + String key = dirs[i].getCanonicalPath() + "/" + name + + dirs[i].getName(); + locations.put(key, + new URL("file://" + dirs[i].getCanonicalPath())); + } + } catch (Exception ioe) { + return; + } + include(name + dirs[i].getName(), dirs[i], locations); + } + } + + /** + * Add a jar file to locations - see {@link #classpathLocations}. + * + * @param name + * @param locations + */ + private static void includeJar(File file, Map<String, URL> locations) { + try { + URL url = new URL("file:" + file.getCanonicalPath()); + url = new URL("jar:" + url.toExternalForm() + "!/"); + JarURLConnection conn = (JarURLConnection) url.openConnection(); + JarFile jarFile = conn.getJarFile(); + if (jarFile != null) { + // the key does not matter here as long as it is unique + locations.put(url.toString(), url); + } + } catch (Exception e) { + // e.printStackTrace(); + return; + } + + } + + /** + * Find and return the default source directory where to create new + * widgetsets. + * + * Return the first directory (not a JAR file etc.) on the classpath by + * default. + * + * TODO this could be done better... + * + * @return URL + */ + public static URL getDefaultSourceDirectory() { + + final Logger logger = getLogger(); + + if (logger.isLoggable(Level.FINE)) { + logger.fine("classpathLocations values:"); + ArrayList<String> locations = new ArrayList<String>( + classpathLocations.keySet()); + for (String location : locations) { + logger.fine(String.valueOf(classpathLocations.get(location))); + } + } + + Iterator<String> it = rawClasspathEntries.iterator(); + while (it.hasNext()) { + String entry = it.next(); + + File directory = new File(entry); + if (directory.exists() && !directory.isHidden() + && directory.isDirectory()) { + try { + return new URL("file://" + directory.getCanonicalPath()); + } catch (MalformedURLException e) { + logger.log(Level.FINEST, "Ignoring exception", e); + // ignore: continue to the next classpath entry + } catch (IOException e) { + logger.log(Level.FINEST, "Ignoring exception", e); + // ignore: continue to the next classpath entry + } + } + } + return null; + } + + /** + * Test method for helper tool + */ + public static void main(String[] args) { + getLogger().info( + "Searching for available widgetsets and stylesheets..."); + + ClassPathExplorer.getAvailableWidgetSetsAndStylesheets(); + } + + private static final Logger getLogger() { + return Logger.getLogger(ClassPathExplorer.class.getName()); + } + +} diff --git a/server/src/com/vaadin/server/widgetsetutils/WidgetSetBuilder.java b/server/src/com/vaadin/server/widgetsetutils/WidgetSetBuilder.java new file mode 100644 index 0000000000..3a0e59df71 --- /dev/null +++ b/server/src/com/vaadin/server/widgetsetutils/WidgetSetBuilder.java @@ -0,0 +1,213 @@ +/* + * 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.widgetsetutils; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.Reader; +import java.net.URL; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class to update widgetsets GWT module configuration file. Can be used + * command line or via IDE tools. + * + * <p> + * If module definition file contains text "WS Compiler: manually edited", tool + * will skip editing file. + * + */ +public class WidgetSetBuilder { + + public static void main(String[] args) throws IOException { + if (args.length == 0) { + printUsage(); + } else { + String widgetsetname = args[0]; + updateWidgetSet(widgetsetname); + + } + } + + public static void updateWidgetSet(final String widgetset) + throws IOException, FileNotFoundException { + boolean changed = false; + + Map<String, URL> availableWidgetSets = ClassPathExplorer + .getAvailableWidgetSets(); + + URL sourceUrl = availableWidgetSets.get(widgetset); + if (sourceUrl == null) { + // find first/default source directory + sourceUrl = ClassPathExplorer.getDefaultSourceDirectory(); + } + + String widgetsetfilename = sourceUrl.getFile() + "/" + + widgetset.replace(".", "/") + ".gwt.xml"; + + File widgetsetFile = new File(widgetsetfilename); + if (!widgetsetFile.exists()) { + // create empty gwt module file + File parent = widgetsetFile.getParentFile(); + if (parent != null && !parent.exists()) { + if (!parent.mkdirs()) { + throw new IOException( + "Could not create directory for the widgetset: " + + parent.getPath()); + } + } + widgetsetFile.createNewFile(); + PrintStream printStream = new PrintStream(new FileOutputStream( + widgetsetFile)); + printStream.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<!DOCTYPE module PUBLIC \"-//Google Inc.//DTD " + + "Google Web Toolkit 1.7.0//EN\" \"http://google" + + "-web-toolkit.googlecode.com/svn/tags/1.7.0/dis" + + "tro-source/core/src/gwt-module.dtd\">\n"); + printStream.print("<module>\n"); + printStream + .print(" <!--\n" + + " Uncomment the following to compile the widgetset for one browser only.\n" + + " This can reduce the GWT compilation time significantly when debugging.\n" + + " The line should be commented out before deployment to production\n" + + " environments.\n\n" + + " Multiple browsers can be specified for GWT 1.7 as a comma separated\n" + + " list. The supported user agents at the moment of writing were:\n" + + " ie6,ie8,gecko,gecko1_8,safari,opera\n\n" + + " The value gecko1_8 is used for Firefox 3 and later and safari is used for\n" + + " webkit based browsers including Google Chrome.\n" + + " -->\n" + + " <!-- <set-property name=\"user.agent\" value=\"gecko1_8\"/> -->\n"); + printStream.print("\n</module>\n"); + printStream.close(); + changed = true; + } + + String content = readFile(widgetsetFile); + if (isEditable(content)) { + String originalContent = content; + + Collection<String> oldInheritedWidgetsets = getCurrentInheritedWidgetsets(content); + + // add widgetsets that do not exist + Iterator<String> i = availableWidgetSets.keySet().iterator(); + while (i.hasNext()) { + String ws = i.next(); + if (ws.equals(widgetset)) { + // do not inherit the module itself + continue; + } + if (!oldInheritedWidgetsets.contains(ws)) { + content = addWidgetSet(ws, content); + } + } + + for (String ws : oldInheritedWidgetsets) { + if (!availableWidgetSets.containsKey(ws)) { + // widgetset not available in classpath + content = removeWidgetSet(ws, content); + } + } + + changed = changed || !content.equals(originalContent); + if (changed) { + commitChanges(widgetsetfilename, content); + } + } else { + System.out + .println("Widgetset is manually edited. Skipping updates."); + } + } + + private static boolean isEditable(String content) { + return !content.contains("WS Compiler: manually edited"); + } + + private static String removeWidgetSet(String ws, String content) { + return content.replaceFirst("<inherits name=\"" + ws + "\"[^/]*/>", ""); + } + + private static void commitChanges(String widgetsetfilename, String content) + throws IOException { + BufferedWriter bufferedWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(widgetsetfilename))); + bufferedWriter.write(content); + bufferedWriter.close(); + } + + private static String addWidgetSet(String ws, String content) { + return content.replace("</module>", "\n <inherits name=\"" + ws + + "\" />" + "\n</module>"); + } + + private static Collection<String> getCurrentInheritedWidgetsets( + String content) { + HashSet<String> hashSet = new HashSet<String>(); + Pattern inheritsPattern = Pattern.compile(" name=\"([^\"]*)\""); + + Matcher matcher = inheritsPattern.matcher(content); + + while (matcher.find()) { + String gwtModule = matcher.group(1); + if (isWidgetset(gwtModule)) { + hashSet.add(gwtModule); + } + } + return hashSet; + } + + static boolean isWidgetset(String gwtModuleName) { + return gwtModuleName.toLowerCase().contains("widgetset"); + } + + private static String readFile(File widgetsetFile) throws IOException { + Reader fi = new FileReader(widgetsetFile); + BufferedReader bufferedReader = new BufferedReader(fi); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = bufferedReader.readLine()) != null) { + sb.append(line); + sb.append("\n"); + } + fi.close(); + return sb.toString(); + } + + private static void printUsage() { + PrintStream o = System.out; + o.println(WidgetSetBuilder.class.getSimpleName() + " usage:"); + o.println(" 1. Set the same classpath as you will " + + "have for the GWT compiler."); + o.println(" 2. Give the widgetsetname (to be created or updated)" + + " as first parameter"); + o.println(); + o.println("All found vaadin widgetsets will be inherited in given widgetset"); + + } + +} diff --git a/server/src/com/vaadin/ui/AbstractColorPicker.java b/server/src/com/vaadin/ui/AbstractColorPicker.java index d7037e366d..c3bdd49155 100644 --- a/server/src/com/vaadin/ui/AbstractColorPicker.java +++ b/server/src/com/vaadin/ui/AbstractColorPicker.java @@ -405,6 +405,7 @@ public abstract class AbstractColorPicker extends AbstractComponent implements window.setImmediate(true); window.addCloseListener(this); window.addColorChangeListener(new ColorChangeListener() { + @Override public void colorChanged(ColorChangeEvent event) { AbstractColorPicker.this.colorChanged(event); } diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java index 623dc5dbc3..3bca63a3b7 100644 --- a/server/src/com/vaadin/ui/AbstractField.java +++ b/server/src/com/vaadin/ui/AbstractField.java @@ -25,13 +25,13 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.logging.Logger; import com.vaadin.data.Buffered; import com.vaadin.data.Property; import com.vaadin.data.Validatable; import com.vaadin.data.Validator; import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.LegacyPropertyHelper; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.Converter.ConversionException; import com.vaadin.data.util.converter.ConverterUtil; @@ -42,6 +42,7 @@ import com.vaadin.server.AbstractErrorMessage; import com.vaadin.server.CompositeErrorMessage; import com.vaadin.server.ErrorMessage; import com.vaadin.shared.AbstractFieldState; +import com.vaadin.shared.util.SharedUtil; /** * <p> @@ -74,9 +75,6 @@ public abstract class AbstractField<T> extends AbstractComponent implements /* Private members */ - private static final Logger logger = Logger.getLogger(AbstractField.class - .getName()); - /** * Value of the abstract field. */ @@ -370,13 +368,24 @@ public abstract class AbstractField<T> extends AbstractComponent implements return buffered; } - /* Property interface implementation */ - /** - * Returns the (field) value converted to a String using toString(). + * Returns a string representation of this object. The returned string + * representation depends on if the legacy Property toString mode is enabled + * or disabled. + * <p> + * If legacy Property toString mode is enabled, returns the value of this + * <code>Field</code> converted to a String. + * </p> + * <p> + * If legacy Property toString mode is disabled, the string representation + * has no special meaning + * </p> + * + * @see LegacyPropertyHelper#isLegacyToStringEnabled() * - * @see java.lang.Object#toString() - * @deprecated As of 7.0, use {@link #getValue()} to get the value of the + * @return A string representation of the value value stored in the Property + * or a string representation of the Property object. + * @deprecated As of 7.0. Use {@link #getValue()} to get the value of the * field, {@link #getConvertedValue()} to get the field value * converted to the data model type or * {@link #getPropertyDataSource()} .getValue() to get the value @@ -385,17 +394,15 @@ public abstract class AbstractField<T> extends AbstractComponent implements @Deprecated @Override public String toString() { - logger.warning("You are using AbstractField.toString() to get the value for a " - + getClass().getSimpleName() - + ". This will not be supported starting from Vaadin 7.1 " - + "(your debugger might call toString() and cause this message to appear)."); - final Object value = getFieldValue(); - if (value == null) { - return null; + if (!LegacyPropertyHelper.isLegacyToStringEnabled()) { + return super.toString(); + } else { + return LegacyPropertyHelper.legacyPropertyToString(this); } - return value.toString(); } + /* Property interface implementation */ + /** * Gets the current value of the field. * @@ -451,7 +458,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements throws Property.ReadOnlyException, Converter.ConversionException, InvalidValueException { - if (!equals(newFieldValue, getInternalValue())) { + if (!SharedUtil.equals(newFieldValue, getInternalValue())) { // Read only fields can not be changed if (isReadOnly()) { @@ -459,7 +466,8 @@ public abstract class AbstractField<T> extends AbstractComponent implements } try { T doubleConvertedFieldValue = convertFromModel(convertToModel(newFieldValue)); - if (!equals(newFieldValue, doubleConvertedFieldValue)) { + if (!SharedUtil + .equals(newFieldValue, doubleConvertedFieldValue)) { newFieldValue = doubleConvertedFieldValue; repaintIsNotNeeded = false; } @@ -536,11 +544,9 @@ public abstract class AbstractField<T> extends AbstractComponent implements } } + @Deprecated static boolean equals(Object value1, Object value2) { - if (value1 == null) { - return value2 == null; - } - return value1.equals(value2); + return SharedUtil.equals(value1, value2); } /* External data source */ @@ -1228,8 +1234,8 @@ public abstract class AbstractField<T> extends AbstractComponent implements public void valueChange(Property.ValueChangeEvent event) { if (!isBuffered()) { if (committingValueToDataSource) { - boolean propertyNotifiesOfTheBufferedValue = equals(event - .getProperty().getValue(), getInternalValue()); + boolean propertyNotifiesOfTheBufferedValue = SharedUtil.equals( + event.getProperty().getValue(), getInternalValue()); if (!propertyNotifiesOfTheBufferedValue) { /* * Property (or chained property like PropertyFormatter) now @@ -1345,15 +1351,33 @@ public abstract class AbstractField<T> extends AbstractComponent implements } private void localeMightHaveChanged() { - if (!equals(valueLocale, getLocale()) && dataSource != null - && !isModified()) { - // When we have a data source and the internal value is directly - // read from that we want to update the value - T newInternalValue = convertFromModel(getPropertyDataSource() - .getValue()); - if (!equals(newInternalValue, getInternalValue())) { - setInternalValue(newInternalValue); - fireValueChange(false); + if (!SharedUtil.equals(valueLocale, getLocale())) { + // The locale HAS actually changed + + if (dataSource != null && !isModified()) { + // When we have a data source and the internal value is directly + // read from that we want to update the value + T newInternalValue = convertFromModel(getPropertyDataSource() + .getValue()); + if (!SharedUtil.equals(newInternalValue, getInternalValue())) { + setInternalValue(newInternalValue); + fireValueChange(false); + } + } else if (dataSource == null && converter != null) { + /* + * No data source but a converter has been set. The same issues + * as above but we cannot use propertyDataSource. Convert the + * current value back to a model value using the old locale and + * then convert back using the new locale. If this does not + * match the field value we need to set the converted value + * again. + */ + Object convertedValue = convertToModel(getInternalValue(), + valueLocale); + T newinternalValue = convertFromModel(convertedValue); + if (!SharedUtil.equals(getInternalValue(), newinternalValue)) { + setConvertedValue(convertedValue); + } } } } @@ -1600,7 +1624,7 @@ public abstract class AbstractField<T> extends AbstractComponent implements setModified(false); // If the new value differs from the previous one - if (!equals(newFieldValue, getInternalValue())) { + if (!SharedUtil.equals(newFieldValue, getInternalValue())) { setInternalValue(newFieldValue); fireValueChange(false); } else if (wasModified) { diff --git a/server/src/com/vaadin/ui/AbstractMedia.java b/server/src/com/vaadin/ui/AbstractMedia.java index 41677467bb..97947b568d 100644 --- a/server/src/com/vaadin/ui/AbstractMedia.java +++ b/server/src/com/vaadin/ui/AbstractMedia.java @@ -25,6 +25,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import com.vaadin.server.ConnectorResource; +import com.vaadin.server.DownloadStream; import com.vaadin.server.Resource; import com.vaadin.server.ResourceReference; import com.vaadin.server.VaadinRequest; @@ -83,7 +84,14 @@ public abstract class AbstractMedia extends AbstractComponent { public boolean handleConnectorRequest(VaadinRequest request, VaadinResponse response, String path) throws IOException { Matcher matcher = Pattern.compile("(\\d+)(/.*)?").matcher(path); - if (matcher.matches()) { + if (!matcher.matches()) { + return super.handleConnectorRequest(request, response, path); + } + + DownloadStream stream; + + getSession().lock(); + try { List<URLReference> sources = getState().sources; int sourceIndex = Integer.parseInt(matcher.group(1)); @@ -98,11 +106,13 @@ public abstract class AbstractMedia extends AbstractComponent { URLReference reference = sources.get(sourceIndex); ConnectorResource resource = (ConnectorResource) ResourceReference .getResource(reference); - resource.getStream().writeResponse(request, response); - return true; - } else { - return super.handleConnectorRequest(request, response, path); + stream = resource.getStream(); + } finally { + getSession().unlock(); } + + stream.writeResponse(request, response); + return true; } private Logger getLogger() { diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java index 8c2f86926d..c9eb756daa 100644 --- a/server/src/com/vaadin/ui/AbstractOrderedLayout.java +++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -53,6 +53,8 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements */ protected LinkedList<Component> components = new LinkedList<Component>(); + private Alignment defaultComponentAlignment = Alignment.TOP_LEFT; + /* Child component alignments */ /** @@ -147,7 +149,9 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements } private void componentAdded(Component c) { - getState().childData.put(c, new ChildComponentData()); + ChildComponentData ccd = new ChildComponentData(); + ccd.alignmentBitmask = getDefaultComponentAlignment().getBitMask(); + getState().childData.put(c, ccd); } /** @@ -417,4 +421,27 @@ public abstract class AbstractOrderedLayout extends AbstractLayout implements public void setMargin(MarginInfo marginInfo) { getState().marginsBitmask = marginInfo.getBitMask(); } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment() + */ + @Override + public Alignment getDefaultComponentAlignment() { + return defaultComponentAlignment; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com + * .vaadin.ui.Alignment) + */ + @Override + public void setDefaultComponentAlignment(Alignment defaultAlignment) { + defaultComponentAlignment = defaultAlignment; + } + } diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java index fcfc55aadc..1bcf802f12 100644 --- a/server/src/com/vaadin/ui/Button.java +++ b/server/src/com/vaadin/ui/Button.java @@ -32,6 +32,7 @@ import com.vaadin.event.ShortcutAction; import com.vaadin.event.ShortcutAction.KeyCode; import com.vaadin.event.ShortcutAction.ModifierKey; import com.vaadin.event.ShortcutListener; +import com.vaadin.server.Resource; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.button.ButtonServerRpc; import com.vaadin.shared.ui.button.ButtonState; @@ -581,6 +582,35 @@ public class Button extends AbstractComponent implements } /** + * Sets the component's icon and alt text. + * + * An alt text is shown when an image could not be loaded, and read by + * assisitve devices. + * + * @param icon + * the icon to be shown with the component's caption. + * @param iconAltText + * String to use as alt text + */ + public void setIcon(Resource icon, String iconAltText) { + super.setIcon(icon); + getState().iconAltText = iconAltText == null ? "" : iconAltText; + } + + /** + * Returns the icon's alt text. + * + * @return String with the alt text + */ + public String getIconAlternateText() { + return getState().iconAltText; + } + + public void setIconAlternateText(String iconAltText) { + getState().iconAltText = iconAltText; + } + + /** * Set whether the caption text is rendered as HTML or not. You might need * to retheme button to allow higher content than the original text style. * diff --git a/server/src/com/vaadin/ui/Calendar.java b/server/src/com/vaadin/ui/Calendar.java new file mode 100644 index 0000000000..38fa355dd8 --- /dev/null +++ b/server/src/com/vaadin/ui/Calendar.java @@ -0,0 +1,1845 @@ +/* + * 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.lang.reflect.Method; +import java.text.DateFormat; +import java.text.DateFormatSymbols; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.EventListener; +import java.util.GregorianCalendar; +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.Map.Entry; +import java.util.Set; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.CalendarState; +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventClickHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.RangeSelectEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.RangeSelectHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClickHandler; +import com.vaadin.ui.components.calendar.CalendarDateRange; +import com.vaadin.ui.components.calendar.CalendarTargetDetails; +import com.vaadin.ui.components.calendar.ContainerEventProvider; +import com.vaadin.ui.components.calendar.event.BasicEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEditableEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeListener; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; +import com.vaadin.ui.components.calendar.handler.BasicBackwardHandler; +import com.vaadin.ui.components.calendar.handler.BasicDateClickHandler; +import com.vaadin.ui.components.calendar.handler.BasicEventMoveHandler; +import com.vaadin.ui.components.calendar.handler.BasicEventResizeHandler; +import com.vaadin.ui.components.calendar.handler.BasicForwardHandler; +import com.vaadin.ui.components.calendar.handler.BasicWeekClickHandler; + +/** + * <p> + * Vaadin Calendar is for visualizing events in a calendar. Calendar events can + * be visualized in the variable length view depending on the start and end + * dates. + * </p> + * + * <li>You can set the viewable date range with the {@link #setStartDate(Date)} + * and {@link #setEndDate(Date)} methods. Calendar has a default date range of + * one week</li> + * + * <li>Calendar has two kind of views: monthly and weekly view</li> + * + * <li>If date range is seven days or shorter, the weekly view is used.</li> + * + * <li>Calendar queries its events by using a + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider}. By default, a + * {@link com.vaadin.addon.calendar.event.BasicEventProvider BasicEventProvider} + * is used.</li> + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class Calendar extends AbstractComponent implements + CalendarComponentEvents.NavigationNotifier, + CalendarComponentEvents.EventMoveNotifier, + CalendarComponentEvents.RangeSelectNotifier, + CalendarComponentEvents.EventResizeNotifier, + CalendarEventProvider.EventSetChangeListener, DropTarget, + CalendarEditableEventProvider, Action.Container { + + /** + * Calendar can use either 12 hours clock or 24 hours clock. + */ + public enum TimeFormat { + + Format12H(), Format24H(); + } + + /** Defines currently active format for time. 12H/24H. */ + protected TimeFormat currentTimeFormat; + + /** Internal calendar data source. */ + protected java.util.Calendar currentCalendar = java.util.Calendar + .getInstance(); + + /** Defines the component's active time zone. */ + protected TimeZone timezone; + + /** Defines the calendar's date range starting point. */ + protected Date startDate = null; + + /** Defines the calendar's date range ending point. */ + protected Date endDate = null; + + /** Event provider. */ + private CalendarEventProvider calendarEventProvider; + + /** + * Internal buffer for the events that are retrieved from the event + * provider. + */ + protected List<CalendarEvent> events; + + /** Date format that will be used in the UIDL for dates. */ + protected DateFormat df_date = new SimpleDateFormat("yyyy-MM-dd"); + + /** Time format that will be used in the UIDL for time. */ + protected DateFormat df_time = new SimpleDateFormat("HH:mm:ss"); + + /** Date format that will be used in the UIDL for both date and time. */ + protected DateFormat df_date_time = new SimpleDateFormat( + DateConstants.CLIENT_DATE_FORMAT + "-" + + DateConstants.CLIENT_TIME_FORMAT); + + /** + * Week view's scroll position. Client sends updates to this value so that + * scroll position wont reset all the time. + */ + private int scrollTop = 0; + + /** Caption format for the weekly view */ + private String weeklyCaptionFormat = null; + + /** Map from event ids to event handlers */ + private final Map<String, EventListener> handlers; + + /** + * Drop Handler for Vaadin DD. By default null. + */ + private DropHandler dropHandler; + + /** + * First day to show for a week + */ + private int firstDay = 1; + + /** + * Last day to show for a week + */ + private int lastDay = 7; + + /** + * First hour to show for a day + */ + private int firstHour = 0; + + /** + * Last hour to show for a day + */ + private int lastHour = 23; + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * + */ + private CalendarServerRpcImpl rpc = new CalendarServerRpcImpl(); + + /** + * Returns the logger for the calendar + */ + protected Logger getLogger() { + return Logger.getLogger(Calendar.class.getName()); + } + + /** + * Construct a Vaadin Calendar with a BasicEventProvider and no caption. + * Default date range is one week. + */ + public Calendar() { + this(null, new BasicEventProvider()); + } + + /** + * Construct a Vaadin Calendar with a BasicEventProvider and the provided + * caption. Default date range is one week. + * + * @param caption + */ + public Calendar(String caption) { + this(caption, new BasicEventProvider()); + } + + /** + * <p> + * Construct a Vaadin Calendar with event provider. Event provider is + * obligatory, because calendar component will query active events through + * it. + * </p> + * + * <p> + * By default, Vaadin Calendar will show dates from the start of the current + * week to the end of the current week. Use {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)} to change this. + * </p> + * + * @param eventProvider + * Event provider, cannot be null. + */ + public Calendar(CalendarEventProvider eventProvider) { + this(null, eventProvider); + } + + /** + * <p> + * Construct a Vaadin Calendar with event provider and a caption. Event + * provider is obligatory, because calendar component will query active + * events through it. + * </p> + * + * <p> + * By default, Vaadin Calendar will show dates from the start of the current + * week to the end of the current week. Use {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)} to change this. + * </p> + * + * @param eventProvider + * Event provider, cannot be null. + */ + // this is the constructor every other constructor calls + public Calendar(String caption, CalendarEventProvider eventProvider) { + registerRpc(rpc); + setCaption(caption); + handlers = new HashMap<String, EventListener>(); + setDefaultHandlers(); + currentCalendar.setTime(new Date()); + setEventProvider(eventProvider); + getState().firstDayOfWeek = firstDay; + getState().lastVisibleDayOfWeek = lastDay; + getState().firstHourOfDay = firstHour; + getState().lastHourOfDay = lastHour; + setTimeFormat(null); + + } + + @Override + public CalendarState getState() { + return (CalendarState) super.getState(); + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + + initCalendarWithLocale(); + + getState().format24H = TimeFormat.Format24H == getTimeFormat(); + setupDaysAndActions(); + setupCalendarEvents(); + rpc.scroll(scrollTop); + } + + /** + * Set all the wanted default handlers here. This is always called after + * constructing this object. All other events have default handlers except + * range and event click. + */ + protected void setDefaultHandlers() { + setHandler(new BasicBackwardHandler()); + setHandler(new BasicForwardHandler()); + setHandler(new BasicWeekClickHandler()); + setHandler(new BasicDateClickHandler()); + setHandler(new BasicEventMoveHandler()); + setHandler(new BasicEventResizeHandler()); + } + + /** + * Gets the calendar's start date. + * + * @return First visible date. + */ + public Date getStartDate() { + if (startDate == null) { + currentCalendar.set(java.util.Calendar.DAY_OF_WEEK, + currentCalendar.getFirstDayOfWeek()); + return currentCalendar.getTime(); + } + return startDate; + } + + /** + * Sets start date for the calendar. This and {@link #setEndDate(Date)} + * control the range of dates visible on the component. The default range is + * one week. + * + * @param date + * First visible date to show. + */ + public void setStartDate(Date date) { + if (!date.equals(startDate)) { + startDate = date; + markAsDirty(); + } + } + + /** + * Gets the calendar's end date. + * + * @return Last visible date. + */ + public Date getEndDate() { + if (endDate == null) { + currentCalendar.set(java.util.Calendar.DAY_OF_WEEK, + currentCalendar.getFirstDayOfWeek() + 6); + return currentCalendar.getTime(); + } + return endDate; + } + + /** + * Sets end date for the calendar. Starting from startDate, only six weeks + * will be shown if duration to endDate is longer than six weeks. + * + * This and {@link #setStartDate(Date)} control the range of dates visible + * on the component. The default range is one week. + * + * @param date + * Last visible date to show. + */ + public void setEndDate(Date date) { + if (startDate != null && startDate.after(date)) { + startDate = (Date) date.clone(); + markAsDirty(); + } else if (!date.equals(endDate)) { + endDate = date; + markAsDirty(); + } + } + + /** + * Sets the locale to be used in the Calendar component. + * + * @see com.vaadin.ui.AbstractComponent#setLocale(java.util.Locale) + */ + @Override + public void setLocale(Locale newLocale) { + super.setLocale(newLocale); + initCalendarWithLocale(); + } + + /** + * Initialize the java calendar instance with the current locale and + * timezone. + */ + private void initCalendarWithLocale() { + if (timezone != null) { + currentCalendar = java.util.Calendar.getInstance(timezone, + getLocale()); + + } else { + currentCalendar = java.util.Calendar.getInstance(getLocale()); + } + } + + private void setupCalendarEvents() { + int durationInDays = (int) (((endDate.getTime()) - startDate.getTime()) / DateConstants.DAYINMILLIS); + durationInDays++; + if (durationInDays > 60) { + throw new RuntimeException("Daterange is too big (max 60) = " + + durationInDays); + } + + Date firstDateToShow = expandStartDate(startDate, durationInDays > 7); + Date lastDateToShow = expandEndDate(endDate, durationInDays > 7); + + currentCalendar.setTime(firstDateToShow); + events = getEventProvider().getEvents(firstDateToShow, lastDateToShow); + + List<CalendarState.Event> calendarStateEvents = new ArrayList<CalendarState.Event>(); + if (events != null) { + for (int i = 0; i < events.size(); i++) { + CalendarEvent e = events.get(i); + CalendarState.Event event = new CalendarState.Event(); + event.index = i; + event.caption = e.getCaption() == null ? "" : e.getCaption(); + event.dateFrom = df_date.format(e.getStart()); + event.dateTo = df_date.format(e.getEnd()); + event.timeFrom = df_time.format(e.getStart()); + event.timeTo = df_time.format(e.getEnd()); + event.description = e.getDescription() == null ? "" : e + .getDescription(); + event.styleName = e.getStyleName() == null ? "" : e + .getStyleName(); + event.allDay = e.isAllDay(); + calendarStateEvents.add(event); + } + } + getState().events = calendarStateEvents; + } + + private void setupDaysAndActions() { + // Make sure we have a up-to-date locale + initCalendarWithLocale(); + + CalendarState state = getState(); + + state.firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + + // If only one is null, throw exception + // If both are null, set defaults + if (startDate == null ^ endDate == null) { + String message = "Schedule cannot be painted without a proper date range.\n"; + if (startDate == null) { + throw new IllegalStateException(message + + "You must set a start date using setStartDate(Date)."); + + } else { + throw new IllegalStateException(message + + "You must set an end date using setEndDate(Date)."); + } + + } else if (startDate == null && endDate == null) { + // set defaults + startDate = getStartDate(); + endDate = getEndDate(); + } + + int durationInDays = (int) (((endDate.getTime()) - startDate.getTime()) / DateConstants.DAYINMILLIS); + durationInDays++; + if (durationInDays > 60) { + throw new RuntimeException("Daterange is too big (max 60) = " + + durationInDays); + } + + state.dayNames = getDayNamesShort(); + state.monthNames = getMonthNamesShort(); + + // Use same timezone in all dates this component handles. + // Show "now"-marker in browser within given timezone. + Date now = new Date(); + currentCalendar.setTime(now); + now = currentCalendar.getTime(); + + // Reset time zones for custom date formats + df_date.setTimeZone(currentCalendar.getTimeZone()); + df_time.setTimeZone(currentCalendar.getTimeZone()); + + state.now = (df_date.format(now) + " " + df_time.format(now)); + + Date firstDateToShow = expandStartDate(startDate, durationInDays > 7); + Date lastDateToShow = expandEndDate(endDate, durationInDays > 7); + + currentCalendar.setTime(firstDateToShow); + + DateFormat weeklyCaptionFormatter = getWeeklyCaptionFormatter(); + weeklyCaptionFormatter.setTimeZone(currentCalendar.getTimeZone()); + + Map<CalendarDateRange, Set<Action>> actionMap = new HashMap<CalendarDateRange, Set<Action>>(); + + List<CalendarState.Day> days = new ArrayList<CalendarState.Day>(); + + // Send all dates to client from server. This + // approach was taken because gwt doesn't + // support date localization properly. + while (currentCalendar.getTime().compareTo(lastDateToShow) < 1) { + final Date date = currentCalendar.getTime(); + final CalendarState.Day day = new CalendarState.Day(); + day.date = df_date.format(date); + day.localizedDateFormat = weeklyCaptionFormatter.format(date); + day.dayOfWeek = getDowByLocale(currentCalendar); + day.week = currentCalendar.get(java.util.Calendar.WEEK_OF_YEAR); + + days.add(day); + + // Get actions for a specific date + if (actionHandlers != null) { + for (Action.Handler actionHandler : actionHandlers) { + + // Create calendar which omits time + GregorianCalendar cal = new GregorianCalendar( + getTimeZone(), getLocale()); + cal.clear(); + cal.set(currentCalendar.get(java.util.Calendar.YEAR), + currentCalendar.get(java.util.Calendar.MONTH), + currentCalendar.get(java.util.Calendar.DATE)); + + // Get day start and end times + Date start = cal.getTime(); + cal.add(java.util.Calendar.DATE, 1); + Date end = cal.getTime(); + + boolean monthView = (durationInDays > 7); + + /** + * If in day or week view add actions for each half-an-hour. + * If in month view add actions for each day + */ + if (monthView) { + setActionsForDay(actionMap, start, end, actionHandler); + } else { + setActionsForEachHalfHour(actionMap, start, end, + actionHandler); + } + + } + } + + currentCalendar.add(java.util.Calendar.DATE, 1); + } + state.days = days; + state.actions = createActionsList(actionMap); + } + + private void setActionsForEachHalfHour( + Map<CalendarDateRange, Set<Action>> actionMap, Date start, + Date end, Action.Handler actionHandler) { + GregorianCalendar cal = new GregorianCalendar(getTimeZone(), + getLocale()); + cal.setTime(start); + while (cal.getTime().before(end)) { + Date s = cal.getTime(); + cal.add(java.util.Calendar.MINUTE, 30); + Date e = cal.getTime(); + CalendarDateRange range = new CalendarDateRange(s, e, getTimeZone()); + Action[] actions = actionHandler.getActions(range, this); + if (actions != null) { + Set<Action> actionSet = new HashSet<Action>( + Arrays.asList(actions)); + actionMap.put(range, actionSet); + } + } + } + + private void setActionsForDay( + Map<CalendarDateRange, Set<Action>> actionMap, Date start, + Date end, Action.Handler actionHandler) { + CalendarDateRange range = new CalendarDateRange(start, end, + getTimeZone()); + Action[] actions = actionHandler.getActions(range, this); + if (actions != null) { + Set<Action> actionSet = new HashSet<Action>(Arrays.asList(actions)); + actionMap.put(range, actionSet); + } + } + + private List<CalendarState.Action> createActionsList( + Map<CalendarDateRange, Set<Action>> actionMap) { + if (actionMap.isEmpty()) { + return null; + } + + List<CalendarState.Action> calendarActions = new ArrayList<CalendarState.Action>(); + + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + + for (Entry<CalendarDateRange, Set<Action>> entry : actionMap.entrySet()) { + CalendarDateRange range = entry.getKey(); + Set<Action> actions = entry.getValue(); + for (Action action : actions) { + String key = actionMapper.key(action); + CalendarState.Action calendarAction = new CalendarState.Action(); + calendarAction.actionKey = key; + calendarAction.caption = action.getCaption(); + setResource(key, action.getIcon()); + calendarAction.iconKey = key; + calendarAction.startDate = formatter.format(range.getStart()); + calendarAction.endDate = formatter.format(range.getEnd()); + calendarActions.add(calendarAction); + } + } + + return calendarActions; + } + + /** + * Gets currently active time format. Value is either TimeFormat.Format12H + * or TimeFormat.Format24H. + * + * @return TimeFormat Format for the time. + */ + public TimeFormat getTimeFormat() { + if (currentTimeFormat == null) { + SimpleDateFormat f = (SimpleDateFormat) SimpleDateFormat + .getTimeInstance(SimpleDateFormat.SHORT, getLocale()); + String p = f.toPattern(); + if (p.indexOf("HH") != -1 || p.indexOf("H") != -1) { + return TimeFormat.Format24H; + } + return TimeFormat.Format12H; + } + return currentTimeFormat; + } + + /** + * Example: <code>setTimeFormat(TimeFormat.Format12H);</code></br> Set to + * null, if you want the format being defined by the locale. + * + * @param format + * Set 12h or 24h format. Default is defined by the locale. + */ + public void setTimeFormat(TimeFormat format) { + currentTimeFormat = format; + markAsDirty(); + } + + /** + * Returns a time zone that is currently used by this component. + * + * @return Component's Time zone + */ + public TimeZone getTimeZone() { + if (timezone == null) { + return currentCalendar.getTimeZone(); + } + return timezone; + } + + /** + * Set time zone that this component will use. Null value sets the default + * time zone. + * + * @param zone + * Time zone to use + */ + public void setTimeZone(TimeZone zone) { + timezone = zone; + if (!currentCalendar.getTimeZone().equals(zone)) { + if (zone == null) { + zone = TimeZone.getDefault(); + } + currentCalendar.setTimeZone(zone); + df_date_time.setTimeZone(zone); + markAsDirty(); + } + } + + /** + * Get the internally used Calendar instance. This is the currently used + * instance of {@link java.util.Calendar} but is bound to change during the + * lifetime of the component. + * + * @return the currently used java calendar + */ + public java.util.Calendar getInternalCalendar() { + return currentCalendar; + } + + /** + * <p> + * This method restricts the weekdays that are shown. This affects both the + * monthly and the weekly view. The general contract is that <b>firstDay < + * lastDay</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param firstDay + * the first day of the week to show, between 1 and 7 + */ + public void setFirstVisibleDayOfWeek(int firstDay) { + if (this.firstDay != firstDay && firstDay >= 1 && firstDay <= 7 + && getLastVisibleDayOfWeek() >= firstDay) { + this.firstDay = firstDay; + getState().firstVisibleDayOfWeek = firstDay; + } + } + + /** + * Get the first visible day of the week. Returns the weekdays as integers + * represented by {@link java.util.Calendar#DAY_OF_WEEK} + * + * @return An integer representing the week day according to + * {@link java.util.Calendar#DAY_OF_WEEK} + */ + public int getFirstVisibleDayOfWeek() { + return firstDay; + } + + /** + * <p> + * This method restricts the weekdays that are shown. This affects both the + * monthly and the weekly view. The general contract is that <b>firstDay < + * lastDay</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param lastDay + * the first day of the week to show, between 1 and 7 + */ + public void setLastVisibleDayOfWeek(int lastDay) { + if (this.lastDay != lastDay && lastDay >= 1 && lastDay <= 7 + && getFirstVisibleDayOfWeek() <= lastDay) { + this.lastDay = lastDay; + getState().lastVisibleDayOfWeek = lastDay; + } + } + + /** + * Get the last visible day of the week. Returns the weekdays as integers + * represented by {@link java.util.Calendar#DAY_OF_WEEK} + * + * @return An integer representing the week day according to + * {@link java.util.Calendar#DAY_OF_WEEK} + */ + public int getLastVisibleDayOfWeek() { + return lastDay; + } + + /** + * <p> + * This method restricts the hours that are shown per day. This affects the + * weekly view. The general contract is that <b>firstHour < lastHour</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param firstHour + * the first hour of the day to show, between 0 and 23 + */ + public void setFirstVisibleHourOfDay(int firstHour) { + if (this.firstHour != firstHour && firstHour >= 0 && firstHour <= 23 + && firstHour <= getLastVisibleHourOfDay()) { + this.firstHour = firstHour; + getState().firstHourOfDay = firstHour; + } + } + + /** + * Returns the first visible hour in the week view. Returns the hour using a + * 24h time format + * + */ + public int getFirstVisibleHourOfDay() { + return firstHour; + } + + /** + * <p> + * This method restricts the hours that are shown per day. This affects the + * weekly view. The general contract is that <b>firstHour < lastHour</b>. + * </p> + * + * <p> + * Note that this only affects the rendering process. Events are still + * requested by the dates set by {@link #setStartDate(Date)} and + * {@link #setEndDate(Date)}. + * </p> + * + * @param lastHour + * the first hour of the day to show, between 0 and 23 + */ + public void setLastVisibleHourOfDay(int lastHour) { + if (this.lastHour != lastHour && lastHour >= 0 && lastHour <= 23 + && lastHour >= getFirstVisibleHourOfDay()) { + this.lastHour = lastHour; + getState().lastHourOfDay = lastHour; + } + } + + /** + * Returns the last visible hour in the week view. Returns the hour using a + * 24h time format + * + */ + public int getLastVisibleHourOfDay() { + return lastHour; + } + + /** + * Gets the date caption format for the weekly view. + * + * @return The pattern used in caption of dates in weekly view. + */ + public String getWeeklyCaptionFormat() { + return weeklyCaptionFormat; + } + + /** + * Sets custom date format for the weekly view. This is the caption of the + * date. Format could be like "mmm MM/dd". + * + * @param dateFormatPattern + * The date caption pattern. + */ + public void setWeeklyCaptionFormat(String dateFormatPattern) { + if ((weeklyCaptionFormat == null && dateFormatPattern != null) + || (weeklyCaptionFormat != null && !weeklyCaptionFormat + .equals(dateFormatPattern))) { + weeklyCaptionFormat = dateFormatPattern; + markAsDirty(); + } + } + + private DateFormat getWeeklyCaptionFormatter() { + if (weeklyCaptionFormat != null) { + return new SimpleDateFormat(weeklyCaptionFormat, getLocale()); + } else { + return SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT, + getLocale()); + } + } + + /** + * Get the day of week by the given calendar and its locale + * + * @param calendar + * The calendar to use + * @return + */ + private static int getDowByLocale(java.util.Calendar calendar) { + int fow = calendar.get(java.util.Calendar.DAY_OF_WEEK); + + // monday first + if (calendar.getFirstDayOfWeek() == java.util.Calendar.MONDAY) { + fow = (fow == java.util.Calendar.SUNDAY) ? 7 : fow - 1; + } + + return fow; + } + + /** + * Is the user allowed to trigger events which alters the events + * + * @return true if the client is allowed to send changes to server + * @see #isEventClickAllowed() + */ + protected boolean isClientChangeAllowed() { + return !isReadOnly() && isEnabled(); + } + + /** + * Is the user allowed to trigger click events + * + * @return true if the client is allowed to click events + * @see #isClientChangeAllowed() + */ + protected boolean isEventClickAllowed() { + return isEnabled(); + } + + /** + * Fires an event when the user selecing moving forward/backward in the + * calendar. + * + * @param forward + * True if the calendar moved forward else backward is assumed. + */ + protected void fireNavigationEvent(boolean forward) { + if (forward) { + fireEvent(new ForwardEvent(this)); + } else { + fireEvent(new BackwardEvent(this)); + } + } + + /** + * Fires an event move event to all server side move listerners + * + * @param index + * The index of the event in the events list + * @param newFromDatetime + * The changed from date time + */ + protected void fireEventMove(int index, Date newFromDatetime) { + MoveEvent event = new MoveEvent(this, events.get(index), + newFromDatetime); + + if (calendarEventProvider instanceof EventMoveHandler) { + // Notify event provider if it is an event move handler + ((EventMoveHandler) calendarEventProvider).eventMove(event); + } + + // Notify event move handler attached by using the + // setHandler(EventMoveHandler) method + fireEvent(event); + } + + /** + * Fires event when a week was clicked in the calendar. + * + * @param week + * The week that was clicked + * @param year + * The year of the week + */ + protected void fireWeekClick(int week, int year) { + fireEvent(new WeekClick(this, week, year)); + } + + /** + * Fires event when a date was clicked in the calendar. Uses an existing + * event from the event cache. + * + * @param index + * The index of the event in the event cache. + */ + protected void fireEventClick(Integer index) { + fireEvent(new EventClick(this, events.get(index))); + } + + /** + * Fires event when a date was clicked in the calendar. Creates a new event + * for the date and passes it to the listener. + * + * @param date + * The date and time that was clicked + */ + protected void fireDateClick(Date date) { + fireEvent(new DateClickEvent(this, date)); + } + + /** + * Fires an event range selected event. The event is fired when a user + * highlights an area in the calendar. The highlighted areas start and end + * dates are returned as arguments. + * + * @param from + * The start date and time of the highlighted area + * @param to + * The end date and time of the highlighted area + * @param monthlyMode + * Is the calendar in monthly mode + */ + protected void fireRangeSelect(Date from, Date to, boolean monthlyMode) { + fireEvent(new RangeSelectEvent(this, from, to, monthlyMode)); + } + + /** + * Fires an event resize event. The event is fired when a user resizes the + * event in the calendar causing the time range of the event to increase or + * decrease. The new start and end times are returned as arguments to this + * method. + * + * @param index + * The index of the event in the event cache + * @param startTime + * The new start date and time of the event + * @param endTime + * The new end date and time of the event + */ + protected void fireEventResize(int index, Date startTime, Date endTime) { + EventResize event = new EventResize(this, events.get(index), startTime, + endTime); + + if (calendarEventProvider instanceof EventResizeHandler) { + // Notify event provider if it is an event resize handler + ((EventResizeHandler) calendarEventProvider).eventResize(event); + } + + // Notify event resize handler attached by using the + // setHandler(EventMoveHandler) method + fireEvent(event); + } + + /** + * Localized display names for week days starting from sunday. Returned + * array's length is always 7. + * + * @return Array of localized weekday names. + */ + protected String[] getDayNamesShort() { + DateFormatSymbols s = new DateFormatSymbols(getLocale()); + return Arrays.copyOfRange(s.getWeekdays(), 1, 8); + } + + /** + * Localized display names for months starting from January. Returned + * array's length is always 12. + * + * @return Array of localized month names. + */ + protected String[] getMonthNamesShort() { + DateFormatSymbols s = new DateFormatSymbols(getLocale()); + return Arrays.copyOf(s.getShortMonths(), 12); + } + + /** + * Gets a date that is first day in the week that target given date belongs + * to. + * + * @param date + * Target date + * @return Date that is first date in same week that given date is. + */ + protected Date getFirstDateForWeek(Date date) { + int firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + currentCalendar.setTime(date); + while (firstDayOfWeek != currentCalendar + .get(java.util.Calendar.DAY_OF_WEEK)) { + currentCalendar.add(java.util.Calendar.DATE, -1); + } + return currentCalendar.getTime(); + } + + /** + * Gets a date that is last day in the week that target given date belongs + * to. + * + * @param date + * Target date + * @return Date that is last date in same week that given date is. + */ + protected Date getLastDateForWeek(Date date) { + currentCalendar.setTime(date); + currentCalendar.add(java.util.Calendar.DATE, 1); + int firstDayOfWeek = currentCalendar.getFirstDayOfWeek(); + // Roll to weeks last day using firstdayofweek. Roll until FDofW is + // found and then roll back one day. + while (firstDayOfWeek != currentCalendar + .get(java.util.Calendar.DAY_OF_WEEK)) { + currentCalendar.add(java.util.Calendar.DATE, 1); + } + currentCalendar.add(java.util.Calendar.DATE, -1); + return currentCalendar.getTime(); + } + + /** + * Calculates the end time of the day using the given calendar and date + * + * @param date + * @param calendar + * the calendar instance to be used in the calculation. The given + * instance is unchanged in this operation. + * @return the given date, with time set to the end of the day + */ + private static Date getEndOfDay(java.util.Calendar calendar, Date date) { + java.util.Calendar calendarClone = (java.util.Calendar) calendar + .clone(); + + calendarClone.setTime(date); + calendarClone.set(java.util.Calendar.MILLISECOND, + calendarClone.getActualMaximum(java.util.Calendar.MILLISECOND)); + calendarClone.set(java.util.Calendar.SECOND, + calendarClone.getActualMaximum(java.util.Calendar.SECOND)); + calendarClone.set(java.util.Calendar.MINUTE, + calendarClone.getActualMaximum(java.util.Calendar.MINUTE)); + calendarClone.set(java.util.Calendar.HOUR, + calendarClone.getActualMaximum(java.util.Calendar.HOUR)); + calendarClone.set(java.util.Calendar.HOUR_OF_DAY, + calendarClone.getActualMaximum(java.util.Calendar.HOUR_OF_DAY)); + + return calendarClone.getTime(); + } + + /** + * Calculates the end time of the day using the given calendar and date + * + * @param date + * @param calendar + * the calendar instance to be used in the calculation. The given + * instance is unchanged in this operation. + * @return the given date, with time set to the end of the day + */ + private static Date getStartOfDay(java.util.Calendar calendar, Date date) { + java.util.Calendar calendarClone = (java.util.Calendar) calendar + .clone(); + + calendarClone.setTime(date); + calendarClone.set(java.util.Calendar.MILLISECOND, 0); + calendarClone.set(java.util.Calendar.SECOND, 0); + calendarClone.set(java.util.Calendar.MINUTE, 0); + calendarClone.set(java.util.Calendar.HOUR, 0); + calendarClone.set(java.util.Calendar.HOUR_OF_DAY, 0); + + return calendarClone.getTime(); + } + + /** + * Finds the first day of the week and returns a day representing the start + * of that day + * + * @param start + * The actual date + * @param expandToFullWeek + * Should the returned date be moved to the start of the week + * @return If expandToFullWeek is set then it returns the first day of the + * week, else it returns a clone of the actual date with the time + * set to the start of the day + */ + protected Date expandStartDate(Date start, boolean expandToFullWeek) { + // If the duration is more than week, use monthly view and get startweek + // and endweek. Example if views daterange is from tuesday to next weeks + // wednesday->expand to monday to nextweeks sunday. If firstdayofweek = + // monday + if (expandToFullWeek) { + start = getFirstDateForWeek(start); + + } else { + start = (Date) start.clone(); + } + + // Always expand to the start of the first day to the end of the last + // day + start = getStartOfDay(currentCalendar, start); + + return start; + } + + /** + * Finds the last day of the week and returns a day representing the end of + * that day + * + * @param end + * The actual date + * @param expandToFullWeek + * Should the returned date be moved to the end of the week + * @return If expandToFullWeek is set then it returns the last day of the + * week, else it returns a clone of the actual date with the time + * set to the end of the day + */ + protected Date expandEndDate(Date end, boolean expandToFullWeek) { + // If the duration is more than week, use monthly view and get startweek + // and endweek. Example if views daterange is from tuesday to next weeks + // wednesday->expand to monday to nextweeks sunday. If firstdayofweek = + // monday + if (expandToFullWeek) { + end = getLastDateForWeek(end); + + } else { + end = (Date) end.clone(); + } + + // Always expand to the start of the first day to the end of the last + // day + end = getEndOfDay(currentCalendar, end); + + return end; + } + + /** + * Set the {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} to be used with this calendar. The EventProvider + * is used to query for events to show, and must be non-null. By default a + * {@link com.vaadin.addon.calendar.event.BasicEventProvider + * BasicEventProvider} is used. + * + * @param calendarEventProvider + * the calendarEventProvider to set. Cannot be null. + */ + public void setEventProvider(CalendarEventProvider calendarEventProvider) { + if (calendarEventProvider == null) { + throw new IllegalArgumentException( + "Calendar event provider cannot be null"); + } + + // remove old listener + if (getEventProvider() instanceof EventSetChangeNotifier) { + ((EventSetChangeNotifier) getEventProvider()) + .removeEventSetChangeListener(this); + } + + this.calendarEventProvider = calendarEventProvider; + + // add new listener + if (calendarEventProvider instanceof EventSetChangeNotifier) { + ((EventSetChangeNotifier) calendarEventProvider) + .addEventSetChangeListener(this); + } + } + + /** + * @return the {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} currently used + */ + public CalendarEventProvider getEventProvider() { + return calendarEventProvider; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarEvents.EventChangeListener#eventChange + * (com.vaadin.addon.calendar.ui.CalendarEvents.EventChange) + */ + @Override + public void eventSetChange(EventSetChangeEvent changeEvent) { + // sanity check + if (calendarEventProvider == changeEvent.getProvider()) { + markAsDirty(); + } + } + + /** + * Set the handler for the given type information. Mirrors + * {@link #addListener(String, Class, Object, Method) addListener} from + * AbstractComponent + * + * @param eventId + * A unique id for the event. Usually one of + * {@link CalendarEventId} + * @param eventType + * The class of the event, most likely a subclass of + * {@link CalendarComponentEvent} + * @param listener + * A listener that listens to the given event + * @param listenerMethod + * The method on the lister to call when the event is triggered + */ + protected void setHandler(String eventId, Class<?> eventType, + EventListener listener, Method listenerMethod) { + if (handlers.get(eventId) != null) { + removeListener(eventId, eventType, handlers.get(eventId)); + handlers.remove(eventId); + } + + if (listener != null) { + addListener(eventId, eventType, listener, listenerMethod); + handlers.put(eventId, listener); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler) + */ + @Override + public void setHandler(ForwardHandler listener) { + setHandler(ForwardEvent.EVENT_ID, ForwardEvent.class, listener, + ForwardHandler.forwardMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler) + */ + @Override + public void setHandler(BackwardHandler listener) { + setHandler(BackwardEvent.EVENT_ID, BackwardEvent.class, listener, + BackwardHandler.backwardMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler) + */ + @Override + public void setHandler(DateClickHandler listener) { + setHandler(DateClickEvent.EVENT_ID, DateClickEvent.class, listener, + DateClickHandler.dateClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventClickHandler) + */ + @Override + public void setHandler(EventClickHandler listener) { + setHandler(EventClick.EVENT_ID, EventClick.class, listener, + EventClickHandler.eventClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler) + */ + @Override + public void setHandler(WeekClickHandler listener) { + setHandler(WeekClick.EVENT_ID, WeekClick.class, listener, + WeekClickHandler.weekClickMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * ) + */ + @Override + public void setHandler(EventResizeHandler listener) { + setHandler(EventResize.EVENT_ID, EventResize.class, listener, + EventResizeHandler.eventResizeMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectHandler + * ) + */ + @Override + public void setHandler(RangeSelectHandler listener) { + setHandler(RangeSelectEvent.EVENT_ID, RangeSelectEvent.class, listener, + RangeSelectHandler.rangeSelectMethod); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler) + */ + @Override + public void setHandler(EventMoveHandler listener) { + setHandler(MoveEvent.EVENT_ID, MoveEvent.class, listener, + EventMoveHandler.eventMoveMethod); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.CalendarEventNotifier + * #getHandler(java.lang.String) + */ + @Override + public EventListener getHandler(String eventId) { + return handlers.get(eventId); + } + + /** + * Get the currently active drop handler + */ + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + /** + * Set the drop handler for the calendar See {@link DropHandler} for + * implementation details. + * + * @param dropHandler + * The drop handler to set + */ + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map) + */ + @Override + public TargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + Map<String, Object> serverVariables = new HashMap<String, Object>(1); + + if (clientVariables.containsKey("dropSlotIndex")) { + int slotIndex = (Integer) clientVariables.get("dropSlotIndex"); + int dayIndex = (Integer) clientVariables.get("dropDayIndex"); + + currentCalendar.setTime(getStartOfDay(currentCalendar, startDate)); + currentCalendar.add(java.util.Calendar.DATE, dayIndex); + + // change this if slot length is modified + currentCalendar.add(java.util.Calendar.MINUTE, slotIndex * 30); + + serverVariables.put("dropTime", currentCalendar.getTime()); + + } else { + int dayIndex = (Integer) clientVariables.get("dropDayIndex"); + currentCalendar.setTime(expandStartDate(startDate, true)); + currentCalendar.add(java.util.Calendar.DATE, dayIndex); + serverVariables.put("dropDay", currentCalendar.getTime()); + } + + CalendarTargetDetails td = new CalendarTargetDetails(serverVariables, + this); + td.setHasDropTime(clientVariables.containsKey("dropSlotIndex")); + + return td; + } + + /** + * Sets a container as a data source for the events in the calendar. + * Equivalent for doing + * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code> + * + * Use this method if you are adding a container which uses the default + * property ids like {@link BeanItemContainer} for instance. If you are + * using custom properties instead use + * {@link Calendar#setContainerDataSource(com.vaadin.data.Container.Indexed, Object, Object, Object, Object, Object)} + * + * Please note that the container must be sorted by date! + * + * @param container + * The container to use as a datasource + */ + public void setContainerDataSource(Container.Indexed container) { + ContainerEventProvider provider = new ContainerEventProvider(container); + provider.addEventSetChangeListener(new CalendarEventProvider.EventSetChangeListener() { + @Override + public void eventSetChange(EventSetChangeEvent changeEvent) { + // Repaint if events change + markAsDirty(); + } + }); + provider.addEventChangeListener(new EventChangeListener() { + @Override + public void eventChange(EventChangeEvent changeEvent) { + // Repaint if event changes + markAsDirty(); + } + }); + setEventProvider(provider); + } + + /** + * Sets a container as a data source for the events in the calendar. + * Equivalent for doing + * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code> + * + * Please note that the container must be sorted by date! + * + * @param container + * The container to use as a data source + * @param captionProperty + * The property that has the caption, null if no caption property + * is present + * @param descriptionProperty + * The property that has the description, null if no description + * property is present + * @param startDateProperty + * The property that has the starting date + * @param endDateProperty + * The property that has the ending date + * @param styleNameProperty + * The property that has the stylename, null if no stylname + * property is present + */ + public void setContainerDataSource(Container.Indexed container, + Object captionProperty, Object descriptionProperty, + Object startDateProperty, Object endDateProperty, + Object styleNameProperty) { + ContainerEventProvider provider = new ContainerEventProvider(container); + provider.setCaptionProperty(captionProperty); + provider.setDescriptionProperty(descriptionProperty); + provider.setStartDateProperty(startDateProperty); + provider.setEndDateProperty(endDateProperty); + provider.setStyleNameProperty(styleNameProperty); + provider.addEventSetChangeListener(new CalendarEventProvider.EventSetChangeListener() { + @Override + public void eventSetChange(EventSetChangeEvent changeEvent) { + // Repaint if events change + markAsDirty(); + } + }); + provider.addEventChangeListener(new EventChangeListener() { + @Override + public void eventChange(EventChangeEvent changeEvent) { + // Repaint if event changes + markAsDirty(); + } + }); + setEventProvider(provider); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + return getEventProvider().getEvents(startDate, endDate); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + if (getEventProvider() instanceof CalendarEditableEventProvider) { + CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider(); + provider.addEvent(event); + markAsDirty(); + } else { + throw new UnsupportedOperationException( + "Event provider does not support adding events"); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + if (getEventProvider() instanceof CalendarEditableEventProvider) { + CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider(); + provider.removeEvent(event); + markAsDirty(); + } else { + throw new UnsupportedOperationException( + "Event provider does not support removing events"); + } + } + + /** + * Adds an action handler to the calender that handles event produced by the + * context menu. + * + * <p> + * The {@link Handler#getActions(Object, Object)} parameters depend on what + * view the Calendar is in: + * <ul> + * <li>If the Calendar is in <i>Day or Week View</i> then the target + * parameter will be a {@link CalendarDateRange} with a range of + * half-an-hour. The {@link Handler#getActions(Object, Object)} method will + * be called once per half-hour slot.</li> + * <li>If the Calendar is in <i>Month View</i> then the target parameter + * will be a {@link CalendarDateRange} with a range of one day. The + * {@link Handler#getActions(Object, Object)} will be called once for each + * day. + * </ul> + * The Dates passed into the {@link CalendarDateRange} are in the same + * timezone as the calendar is. + * </p> + * + * <p> + * The {@link Handler#handleAction(Action, Object, Object)} parameters + * depend on what the context menu is called upon: + * <ul> + * <li>If the context menu is called upon an event then the target parameter + * is the event, i.e. instanceof {@link CalendarEvent}</li> + * <li>If the context menu is called upon an empty slot then the target is a + * {@link Date} representing that slot + * </ul> + * </p> + */ + @Override + public void addActionHandler(Handler actionHandler) { + if (actionHandler != null) { + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper<Action>(); + } + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + markAsDirty(); + } + } + } + + /** + * Is the calendar in a mode where all days of the month is shown + * + * @return Returns true if calendar is in monthly mode and false if it is in + * weekly mode + */ + public boolean isMonthlyMode() { + CalendarState state = (CalendarState) getState(false); + if (state.days != null) { + return state.days.size() > 7; + } else { + // Default mode + return true; + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.Action.Container#removeActionHandler(com.vaadin.event + * .Action.Handler) + */ + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + actionHandlers.remove(actionHandler); + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + markAsDirty(); + } + } + + private class CalendarServerRpcImpl implements CalendarServerRpc { + + @Override + public void eventMove(int eventIndex, String newDate) { + if (!isClientChangeAllowed()) { + return; + } + if (newDate != null) { + try { + Date d = df_date_time.parse(newDate); + if (eventIndex >= 0 && eventIndex < events.size() + && events.get(eventIndex) != null) { + fireEventMove(eventIndex, d); + } + } catch (ParseException e) { + getLogger().log(Level.WARNING, e.getMessage()); + } + } + } + + @Override + public void rangeSelect(String range) { + if (!isClientChangeAllowed()) { + return; + } + + if (range != null && range.length() > 14 && range.contains("TO")) { + String[] dates = range.split("TO"); + try { + Date d1 = df_date.parse(dates[0]); + Date d2 = df_date.parse(dates[1]); + + fireRangeSelect(d1, d2, true); + + } catch (ParseException e) { + // NOP + } + } else if (range != null && range.length() > 12 + && range.contains(":")) { + String[] dates = range.split(":"); + if (dates.length == 3) { + try { + Date d = df_date.parse(dates[0]); + currentCalendar.setTime(d); + int startMinutes = Integer.parseInt(dates[1]); + int endMinutes = Integer.parseInt(dates[2]); + currentCalendar.add(java.util.Calendar.MINUTE, + startMinutes); + Date start = currentCalendar.getTime(); + currentCalendar.add(java.util.Calendar.MINUTE, + endMinutes - startMinutes); + Date end = currentCalendar.getTime(); + fireRangeSelect(start, end, false); + } catch (ParseException e) { + // NOP + } catch (NumberFormatException e) { + // NOP + } + } + } + } + + @Override + public void forward() { + fireEvent(new ForwardEvent(Calendar.this)); + } + + @Override + public void backward() { + fireEvent(new BackwardEvent(Calendar.this)); + } + + @Override + public void dateClick(String date) { + if (!isClientChangeAllowed()) { + return; + } + if (date != null && date.length() > 6) { + try { + Date d = df_date.parse(date); + fireDateClick(d); + } catch (ParseException e) { + } + } + } + + @Override + public void weekClick(String event) { + if (!isClientChangeAllowed()) { + return; + } + if (event.length() > 0 && event.contains("w")) { + String[] splitted = event.split("w"); + if (splitted.length == 2) { + try { + int yr = 1900 + Integer.parseInt(splitted[0]); + int week = Integer.parseInt(splitted[1]); + fireWeekClick(week, yr); + } catch (NumberFormatException e) { + // NOP + } + } + } + } + + @Override + public void eventClick(int eventIndex) { + if (!isEventClickAllowed()) { + return; + } + if (eventIndex >= 0 && eventIndex < events.size() + && events.get(eventIndex) != null) { + fireEventClick(eventIndex); + } + } + + @Override + public void eventResize(int eventIndex, String newStartDate, + String newEndDate) { + if (!isClientChangeAllowed()) { + return; + } + if (newStartDate != null && !"".equals(newStartDate) + && newEndDate != null && !"".equals(newEndDate)) { + try { + Date newStartTime = df_date_time.parse(newStartDate); + Date newEndTime = df_date_time.parse(newEndDate); + + fireEventResize(eventIndex, newStartTime, newEndTime); + } catch (ParseException e) { + // NOOP + } + } + } + + @Override + public void scroll(int scrollPosition) { + scrollTop = scrollPosition; + markAsDirty(); + } + + @Override + public void actionOnEmptyCell(String actionKey, String startDate, + String endDate) { + Action action = actionMapper.get(actionKey); + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + try { + Date start = formatter.parse(startDate); + for (Action.Handler ah : actionHandlers) { + ah.handleAction(action, this, start); + } + + } catch (ParseException e) { + getLogger().log(Level.WARNING, + "Could not parse action date string"); + } + + } + + @Override + public void actionOnEvent(String actionKey, String startDate, + String endDate, int eventIndex) { + Action action = actionMapper.get(actionKey); + SimpleDateFormat formatter = new SimpleDateFormat( + DateConstants.ACTION_DATE_FORMAT_PATTERN); + formatter.setTimeZone(getTimeZone()); + for (Action.Handler ah : actionHandlers) { + ah.handleAction(action, this, events.get(eventIndex)); + } + } + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java index a229003224..85cdcdf65c 100644 --- a/server/src/com/vaadin/ui/ConnectorTracker.java +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -17,6 +17,7 @@ package com.vaadin.ui; import java.io.IOException; import java.io.Serializable; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -31,9 +32,9 @@ import org.json.JSONException; import org.json.JSONObject; import com.vaadin.server.AbstractClientConnector; -import com.vaadin.server.AbstractCommunicationManager; import com.vaadin.server.ClientConnector; import com.vaadin.server.GlobalResourceHandler; +import com.vaadin.server.LegacyCommunicationManager; import com.vaadin.server.StreamVariable; /** @@ -295,7 +296,7 @@ public class ConnectorTracker implements Serializable { uninitializedConnectors.remove(connector); diffStates.remove(connector); iterator.remove(); - } else if (!AbstractCommunicationManager + } else if (!LegacyCommunicationManager .isConnectorVisibleToClient(connector) && !uninitializedConnectors.contains(connector)) { uninitializedConnectors.add(connector); @@ -463,6 +464,31 @@ public class ConnectorTracker implements Serializable { return dirtyConnectors; } + /** + * Checks if there a dirty connectors. + * + * @return true if there are dirty connectors, false otherwise + */ + public boolean hasDirtyConnectors() { + return !getDirtyConnectors().isEmpty(); + } + + /** + * Returns a collection of those {@link #getDirtyConnectors() dirty + * connectors} that are actually visible to the client. + * + * @return A list of dirty and visible connectors. + */ + public ArrayList<ClientConnector> getDirtyVisibleConnectors() { + ArrayList<ClientConnector> dirtyConnectors = new ArrayList<ClientConnector>(); + for (ClientConnector c : getDirtyConnectors()) { + if (LegacyCommunicationManager.isConnectorVisibleToClient(c)) { + dirtyConnectors.add(c); + } + } + return dirtyConnectors; + } + public JSONObject getDiffState(ClientConnector connector) { assert getConnector(connector.getConnectorId()) == connector; return diffStates.get(connector); diff --git a/server/src/com/vaadin/ui/DateField.java b/server/src/com/vaadin/ui/DateField.java index 1a8955801b..5017fac993 100644 --- a/server/src/com/vaadin/ui/DateField.java +++ b/server/src/com/vaadin/ui/DateField.java @@ -19,10 +19,8 @@ package com.vaadin.ui; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; @@ -31,6 +29,7 @@ import com.vaadin.data.Property; import com.vaadin.data.Validator; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.validator.DateRangeValidator; import com.vaadin.event.FieldEvents; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; @@ -40,6 +39,7 @@ import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.shared.ui.datefield.DateFieldConstants; import com.vaadin.shared.ui.datefield.Resolution; +import com.vaadin.shared.ui.datefield.TextualDateFieldState; /** * <p> @@ -148,6 +148,10 @@ public class DateField extends AbstractField<Date> implements private TimeZone timeZone = null; private static Map<Resolution, String> variableNameForResolution = new HashMap<Resolution, String>(); + + private String dateOutOfRangeMessage = "Date is out of allowed range"; + + private DateRangeValidator currentRangeValidator; { variableNameForResolution.put(Resolution.SECOND, "sec"); variableNameForResolution.put(Resolution.MINUTE, "min"); @@ -279,6 +283,174 @@ public class DateField extends AbstractField<Date> implements return super.shouldHideErrors() && uiHasValidDateString; } + @Override + protected TextualDateFieldState getState() { + return (TextualDateFieldState) super.getState(); + } + + @Override + protected TextualDateFieldState getState(boolean markAsDirty) { + return (TextualDateFieldState) super.getState(markAsDirty); + } + + /** + * Sets the start range for this component. If the value is set before this + * date (taking the resolution into account), the component will not + * validate. If <code>startDate</code> is set to <code>null</code>, any + * value before <code>endDate</code> will be accepted by the range + * + * @param startDate + * - the allowed range's start date + */ + public void setRangeStart(Date startDate) { + if (startDate != null && getState().rangeEnd != null + && startDate.after(getState().rangeEnd)) { + throw new IllegalStateException( + "startDate cannot be later than endDate"); + } + getState().rangeStart = startDate; + // rangeStart = startDate; + // This has to be done to correct for the resolution + // updateRangeState(); + updateRangeValidator(); + } + + /** + * Sets the current error message if the range validation fails. + * + * @param dateOutOfRangeMessage + * - Localizable message which is shown when value (the date) is + * set outside allowed range + */ + public void setDateOutOfRangeMessage(String dateOutOfRangeMessage) { + this.dateOutOfRangeMessage = dateOutOfRangeMessage; + updateRangeValidator(); + } + + /** + * Gets the end range for a certain resolution. The range is inclusive, so + * if rangeEnd is set to zero milliseconds past year n and resolution is set + * to YEAR, any date in year n will be accepted. Resolutions lower than DAY + * will be interpreted on a DAY level. That is, everything below DATE is + * cleared + * + * @param forResolution + * - the range conforms to the resolution + * @return + */ + private Date getRangeEnd(Resolution forResolution) { + // We need to set the correct resolution for the dates, + // otherwise the range validator will complain + + Date rangeEnd = getState(false).rangeEnd; + if (rangeEnd == null) { + return null; + } + + Calendar endCal = Calendar.getInstance(); + endCal.setTime(rangeEnd); + + if (forResolution == Resolution.YEAR) { + // Adding one year (minresolution) and clearing the rest. + endCal.set(endCal.get(Calendar.YEAR) + 1, 0, 1, 0, 0, 0); + } else if (forResolution == Resolution.MONTH) { + // Adding one month (minresolution) and clearing the rest. + endCal.set(endCal.get(Calendar.YEAR), + endCal.get(Calendar.MONTH) + 1, 1, 0, 0, 0); + } else { + endCal.set(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH), + endCal.get(Calendar.DATE) + 1, 0, 0, 0); + } + // removing one millisecond will now get the endDate to return to + // current resolution's set time span (year or month) + endCal.set(Calendar.MILLISECOND, -1); + return endCal.getTime(); + } + + /** + * Gets the start range for a certain resolution. The range is inclusive, so + * if <code>rangeStart</code> is set to one millisecond before year n and + * resolution is set to YEAR, any date in year n - 1 will be accepted. + * Lowest supported resolution is DAY. + * + * @param forResolution + * - the range conforms to the resolution + * @return + */ + private Date getRangeStart(Resolution forResolution) { + if (getState(false).rangeStart == null) { + return null; + } + Calendar startCal = Calendar.getInstance(); + startCal.setTime(getState(false).rangeStart); + + if (forResolution == Resolution.YEAR) { + startCal.set(startCal.get(Calendar.YEAR), 0, 1, 0, 0, 0); + } else if (forResolution == Resolution.MONTH) { + startCal.set(startCal.get(Calendar.YEAR), + startCal.get(Calendar.MONTH), 1, 0, 0, 0); + } else { + startCal.set(startCal.get(Calendar.YEAR), + startCal.get(Calendar.MONTH), startCal.get(Calendar.DATE), + 0, 0, 0); + } + + startCal.set(Calendar.MILLISECOND, 0); + return startCal.getTime(); + } + + private void updateRangeValidator() { + if (currentRangeValidator != null) { + removeValidator(currentRangeValidator); + } + + currentRangeValidator = new DateRangeValidator(dateOutOfRangeMessage, + getRangeStart(resolution), getRangeEnd(resolution), null); + + addValidator(currentRangeValidator); + + } + + /** + * Sets the end range for this component. If the value is set after this + * date (taking the resolution into account), the component will not + * validate. If <code>endDate</code> is set to <code>null</code>, any value + * after <code>startDate</code> will be accepted by the range. + * + * @param endDate + * - the allowed range's end date (inclusive, based on the + * current resolution) + */ + public void setRangeEnd(Date endDate) { + if (endDate != null && getState().rangeStart != null + && getState().rangeStart.after(endDate)) { + throw new IllegalStateException( + "endDate cannot be earlier than startDate"); + } + // rangeEnd = endDate; + getState().rangeEnd = endDate; + updateRangeValidator(); + } + + /** + * Returns the precise rangeStart used. + * + * @param startDate + * + */ + public Date getRangeStart() { + return getState(false).rangeStart; + } + + /** + * Returns the precise rangeEnd used. + * + * @param startDate + */ + public Date getRangeEnd() { + return getState(false).rangeEnd; + } + /* * Invoked when a variable of the component changes. Don't add a JavaDoc * comment here, we use the default documentation from implemented @@ -574,6 +746,7 @@ public class DateField extends AbstractField<Date> implements */ public void setResolution(Resolution resolution) { this.resolution = resolution; + updateRangeValidator(); markAsDirty(); } diff --git a/server/src/com/vaadin/ui/GridLayout.java b/server/src/com/vaadin/ui/GridLayout.java index e60d9c676a..53a25c1c83 100644 --- a/server/src/com/vaadin/ui/GridLayout.java +++ b/server/src/com/vaadin/ui/GridLayout.java @@ -92,6 +92,7 @@ public class GridLayout extends AbstractLayout implements private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>(); private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>(); + private Alignment defaultComponentAlignment = Alignment.TOP_LEFT; /** * Constructor for a grid of given size (number of columns and rows). @@ -573,6 +574,7 @@ public class GridLayout extends AbstractLayout implements int row2) { this.component = component; childData = new ChildComponentData(); + childData.alignment = getDefaultComponentAlignment().getBitMask(); childData.column1 = column1; childData.row1 = row1; childData.column2 = column2; @@ -1226,4 +1228,26 @@ public class GridLayout extends AbstractLayout implements return new MarginInfo(getState().marginsBitmask); } + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment() + */ + @Override + public Alignment getDefaultComponentAlignment() { + return defaultComponentAlignment; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com + * .vaadin.ui.Alignment) + */ + @Override + public void setDefaultComponentAlignment(Alignment defaultAlignment) { + defaultComponentAlignment = defaultAlignment; + } + } diff --git a/server/src/com/vaadin/ui/Label.java b/server/src/com/vaadin/ui/Label.java index f49a1403cf..d037652a09 100644 --- a/server/src/com/vaadin/ui/Label.java +++ b/server/src/com/vaadin/ui/Label.java @@ -21,10 +21,13 @@ import java.util.Locale; import java.util.logging.Logger; import com.vaadin.data.Property; +import com.vaadin.data.util.AbstractProperty; +import com.vaadin.data.util.LegacyPropertyHelper; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterUtil; import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.shared.ui.label.LabelState; +import com.vaadin.shared.util.SharedUtil; /** * Label component for showing non-editable short texts. @@ -203,23 +206,6 @@ public class Label extends AbstractComponent implements Property<String>, } /** - * Returns the value displayed by this label. - * - * @see java.lang.Object#toString() - * @deprecated As of 7.0, use {@link #getValue()} to get the value of the - * label or {@link #getPropertyDataSource()} .getValue() to get - * the value of the data source. - */ - @Deprecated - @Override - public String toString() { - logger.warning("You are using Label.toString() to get the value for a " - + getClass().getSimpleName() - + ". This will not be supported starting from Vaadin 7.1 (your debugger might call toString() and cause this message to appear)."); - return getValue(); - } - - /** * Gets the type of the Property. * * @see com.vaadin.data.Property#getType() @@ -419,7 +405,7 @@ public class Label extends AbstractComponent implements Property<String>, private void updateValueFromDataSource() { // Update the internal value from the data source String newConvertedValue = getDataSourceValue(); - if (!AbstractField.equals(newConvertedValue, getState().text)) { + if (!SharedUtil.equals(newConvertedValue, getState().text)) { getState().text = newConvertedValue; fireValueChange(); } @@ -541,4 +527,35 @@ public class Label extends AbstractComponent implements Property<String>, markAsDirty(); } + /** + * Returns a string representation of this object. The returned string + * representation depends on if the legacy Property toString mode is enabled + * or disabled. + * <p> + * If legacy Property toString mode is enabled, returns the value displayed + * by this label. + * </p> + * <p> + * If legacy Property toString mode is disabled, the string representation + * has no special meaning + * </p> + * + * @see AbstractProperty#isLegacyToStringEnabled() + * + * @return The value displayed by this label or a string representation of + * this Label object. + * + * @deprecated As of 7.0, use {@link #getValue()} to get the value of the + * label or {@link #getPropertyDataSource()}.getValue() to get + * the value of the data source. + */ + @Deprecated + @Override + public String toString() { + if (!LegacyPropertyHelper.isLegacyToStringEnabled()) { + return super.toString(); + } else { + return LegacyPropertyHelper.legacyPropertyToString(this); + } + } } diff --git a/server/src/com/vaadin/ui/Layout.java b/server/src/com/vaadin/ui/Layout.java index cd6ffc42d2..dc16b186f2 100644 --- a/server/src/com/vaadin/ui/Layout.java +++ b/server/src/com/vaadin/ui/Layout.java @@ -61,6 +61,23 @@ public interface Layout extends ComponentContainer, Serializable { */ public Alignment getComponentAlignment(Component childComponent); + /** + * Sets the alignment used for new components added to this layout. The + * default is {@link Alignment#TOP_LEFT}. + * + * @param defaultComponentAlignment + * The new default alignment + */ + public void setDefaultComponentAlignment( + Alignment defaultComponentAlignment); + + /** + * Returns the alignment used for new components added to this layout + * + * @return The default alignment + */ + public Alignment getDefaultComponentAlignment(); + } /** diff --git a/server/src/com/vaadin/ui/LoadingIndicatorConfiguration.java b/server/src/com/vaadin/ui/LoadingIndicatorConfiguration.java new file mode 100644 index 0000000000..57ccdc1b64 --- /dev/null +++ b/server/src/com/vaadin/ui/LoadingIndicatorConfiguration.java @@ -0,0 +1,160 @@ +/* + * 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.UIState.LoadingIndicatorConfigurationState; + +/** + * Provides method for configuring the loading indicator. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public interface LoadingIndicatorConfiguration extends Serializable { + /** + * Sets the delay before the loading indicator is shown. The default is + * 300ms. + * + * @param firstDelay + * The first delay (in ms) + */ + public void setFirstDelay(int firstDelay); + + /** + * Returns the delay before the loading indicator is shown. + * + * @return The first delay (in ms) + */ + public int getFirstDelay(); + + /** + * Sets the delay before the loading indicator goes into the "second" state. + * The delay is calculated from the time when the loading indicator was + * triggered. The default is 1500ms. + * + * @param secondDelay + * The delay before going into the "second" state (in ms) + */ + public void setSecondDelay(int secondDelay); + + /** + * Returns the delay before the loading indicator goes into the "second" + * state. The delay is calculated from the time when the loading indicator + * was triggered. + * + * @return The delay before going into the "second" state (in ms) + */ + public int getSecondDelay(); + + /** + * Sets the delay before the loading indicator goes into the "third" state. + * The delay is calculated from the time when the loading indicator was + * triggered. The default is 5000ms. + * + * @param thirdDelay + * The delay before going into the "third" state (in ms) + */ + public void setThirdDelay(int thirdDelay); + + /** + * Returns the delay before the loading indicator goes into the "third" + * state. The delay is calculated from the time when the loading indicator + * was triggered. + * + * @return The delay before going into the "third" state (in ms) + */ + public int getThirdDelay(); +} + +class LoadingIndicatorConfigurationImpl implements + LoadingIndicatorConfiguration { + private UI ui; + + public LoadingIndicatorConfigurationImpl(UI ui) { + this.ui = ui; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#setFirstDelay(int) + */ + @Override + public void setFirstDelay(int firstDelay) { + getState().firstDelay = firstDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#getFirstDelay() + */ + @Override + public int getFirstDelay() { + return getState(false).firstDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#setSecondDelay(int) + */ + @Override + public void setSecondDelay(int secondDelay) { + getState().secondDelay = secondDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#getSecondDelay() + */ + @Override + public int getSecondDelay() { + return getState(false).secondDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#setThirdDelay(int) + */ + @Override + public void setThirdDelay(int thirdDelay) { + getState().thirdDelay = thirdDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.LoadingIndicator#getThirdDelay() + */ + @Override + public int getThirdDelay() { + return getState(false).thirdDelay; + } + + private LoadingIndicatorConfigurationState getState() { + return ui.getState().loadingIndicatorConfiguration; + } + + private LoadingIndicatorConfigurationState getState(boolean markAsDirty) { + return ui.getState(markAsDirty).loadingIndicatorConfiguration; + } + +} diff --git a/server/src/com/vaadin/ui/LoginForm.java b/server/src/com/vaadin/ui/LoginForm.java index 11ae1b379b..d06882927e 100644 --- a/server/src/com/vaadin/ui/LoginForm.java +++ b/server/src/com/vaadin/ui/LoginForm.java @@ -61,24 +61,30 @@ public class LoginForm extends CustomComponent { private Embedded iframe = new Embedded(); @Override - public boolean handleConnectorRequest(VaadinRequest request, - VaadinResponse response, String path) throws IOException { - String method = VaadinServletService.getCurrentServletRequest() - .getMethod(); + public boolean handleConnectorRequest(final VaadinRequest request, + final VaadinResponse response, String path) throws IOException { if (!path.equals("login")) { return super.handleConnectorRequest(request, response, path); } - String responseString = null; - if (method.equalsIgnoreCase("post")) { - responseString = handleLogin(request); - } else { - responseString = getLoginHTML(); - } + final StringBuilder responseBuilder = new StringBuilder(); + + getUI().access(new Runnable() { + @Override + public void run() { + String method = VaadinServletService.getCurrentServletRequest() + .getMethod(); + if (method.equalsIgnoreCase("post")) { + responseBuilder.append(handleLogin(request)); + } else { + responseBuilder.append(getLoginHTML()); + } + } + }); - if (responseString != null) { + if (responseBuilder.length() > 0) { response.setContentType("text/html; charset=utf-8"); response.setCacheTime(-1); - response.getWriter().write(responseString); + response.getWriter().write(responseBuilder.toString()); return true; } else { return false; diff --git a/server/src/com/vaadin/ui/PopupDateField.java b/server/src/com/vaadin/ui/PopupDateField.java index ae33493c89..f0bb0d74fe 100644 --- a/server/src/com/vaadin/ui/PopupDateField.java +++ b/server/src/com/vaadin/ui/PopupDateField.java @@ -118,4 +118,24 @@ public class PopupDateField extends DateField { getState().textFieldEnabled = state; } + /** + * Set a description that explains the usage of the Widget for users of + * assistive devices. + * + * @param description + * String with the description + */ + public void setAssistiveText(String description) { + getState().descriptionForAssistiveDevices = description; + } + + /** + * Get the description that explains the usage of the Widget for users of + * assistive devices. + * + * @return String with the description + */ + public String getAssistiveText() { + return getState().descriptionForAssistiveDevices; + } } diff --git a/server/src/com/vaadin/ui/TooltipConfiguration.java b/server/src/com/vaadin/ui/TooltipConfiguration.java new file mode 100644 index 0000000000..f9120aa18d --- /dev/null +++ b/server/src/com/vaadin/ui/TooltipConfiguration.java @@ -0,0 +1,240 @@ +/* + * 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.UIState.TooltipConfigurationState; + +/** + * Provides method for configuring the tooltip. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public interface TooltipConfiguration extends Serializable { + /** + * Returns the time (in ms) the tooltip should be displayed after an event + * that will cause it to be closed (e.g. mouse click outside the component, + * key down). + * + * @return The close timeout + */ + public int getCloseTimeout(); + + /** + * Sets the time (in ms) the tooltip should be displayed after an event that + * will cause it to be closed (e.g. mouse click outside the component, key + * down). + * + * @param closeTimeout + * The close timeout + */ + public void setCloseTimeout(int closeTimeout); + + /** + * Returns the time (in ms) during which {@link #getQuickOpenDelay()} should + * be used instead of {@link #getOpenDelay()}. The quick open delay is used + * when the tooltip has very recently been shown, is currently hidden but + * about to be shown again. + * + * @return The quick open timeout + */ + public int getQuickOpenTimeout(); + + /** + * Sets the time (in ms) that determines when {@link #getQuickOpenDelay()} + * should be used instead of {@link #getOpenDelay()}. The quick open delay + * is used when the tooltip has very recently been shown, is currently + * hidden but about to be shown again. + * + * @param quickOpenTimeout + * The quick open timeout + */ + public void setQuickOpenTimeout(int quickOpenTimeout); + + /** + * Returns the time (in ms) that should elapse before a tooltip will be + * shown, in the situation when a tooltip has very recently been shown + * (within {@link #getQuickOpenDelay()} ms). + * + * @return The quick open delay + */ + public int getQuickOpenDelay(); + + /** + * Sets the time (in ms) that should elapse before a tooltip will be shown, + * in the situation when a tooltip has very recently been shown (within + * {@link #getQuickOpenDelay()} ms). + * + * @param quickOpenDelay + * The quick open delay + */ + public void setQuickOpenDelay(int quickOpenDelay); + + /** + * Returns the time (in ms) that should elapse after an event triggering + * tooltip showing has occurred (e.g. mouse over) before the tooltip is + * shown. If a tooltip has recently been shown, then + * {@link #getQuickOpenDelay()} is used instead of this. + * + * @return The open delay + */ + public int getOpenDelay(); + + /** + * Sets the time (in ms) that should elapse after an event triggering + * tooltip showing has occurred (e.g. mouse over) before the tooltip is + * shown. If a tooltip has recently been shown, then + * {@link #getQuickOpenDelay()} is used instead of this. + * + * @param openDelay + * The open delay + */ + public void setOpenDelay(int openDelay); + + /** + * Returns the maximum width of the tooltip popup. + * + * @return The maximum width the tooltip popup + */ + public int getMaxWidth(); + + /** + * Sets the maximum width of the tooltip popup. + * + * @param maxWidth + * The maximum width the tooltip popup + */ + public void setMaxWidth(int maxWidth); +} + +class TooltipConfigurationImpl implements TooltipConfiguration { + private UI ui; + + public TooltipConfigurationImpl(UI ui) { + this.ui = ui; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.UI.Tooltip#getCloseTimeout() + */ + @Override + public int getCloseTimeout() { + return getState(false).closeTimeout; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#setCloseTimeout(int) + */ + @Override + public void setCloseTimeout(int closeTimeout) { + getState().closeTimeout = closeTimeout; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#getQuickOpenTimeout() + */ + @Override + public int getQuickOpenTimeout() { + return getState(false).quickOpenTimeout; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#setQuickOpenTimeout(int) + */ + @Override + public void setQuickOpenTimeout(int quickOpenTimeout) { + getState().quickOpenTimeout = quickOpenTimeout; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#getQuickOpenDelay() + */ + @Override + public int getQuickOpenDelay() { + return getState(false).quickOpenDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#setQuickOpenDelay(int) + */ + @Override + public void setQuickOpenDelay(int quickOpenDelay) { + getState().quickOpenDelay = quickOpenDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#getOpenDelay() + */ + @Override + public int getOpenDelay() { + return getState(false).openDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#setOpenDelay(int) + */ + @Override + public void setOpenDelay(int openDelay) { + getState().openDelay = openDelay; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#getMaxWidth() + */ + @Override + public int getMaxWidth() { + return getState(false).maxWidth; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Tooltip#setMaxWidth(int) + */ + @Override + public void setMaxWidth(int maxWidth) { + getState().maxWidth = maxWidth; + } + + private TooltipConfigurationState getState() { + return ui.getState().tooltipConfiguration; + } + + private TooltipConfigurationState getState(boolean markAsDirty) { + return ui.getState(markAsDirty).tooltipConfiguration; + } + +} diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java index a6dbea51ba..15175b5a8b 100644 --- a/server/src/com/vaadin/ui/Tree.java +++ b/server/src/com/vaadin/ui/Tree.java @@ -72,6 +72,13 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, /* Private members */ + private static final String NULL_ALT_EXCEPTION_MESSAGE = "Parameter 'altText' needs to be non null"; + + /** + * Item icons alt texts. + */ + private final HashMap<Object, String> itemIconAlts = new HashMap<Object, String>(); + /** * Set of expanded nodes. */ @@ -163,6 +170,70 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, super(caption, dataSource); } + @Override + public void setItemIcon(Object itemId, Resource icon) { + setItemIcon(itemId, icon, ""); + } + + /** + * Sets the icon for an item. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param icon + * the icon to use or null. + * + * @param altText + * the alternative text for the icon + */ + public void setItemIcon(Object itemId, Resource icon, String altText) { + if (itemId != null) { + super.setItemIcon(itemId, icon); + + if (icon == null) { + itemIconAlts.remove(itemId); + } else if (altText == null) { + throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE); + } else { + itemIconAlts.put(itemId, altText); + } + markAsDirty(); + } + } + + /** + * Set the alternate text for an item. + * + * Used when the item has an icon. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param altText + * the alternative text for the icon + */ + public void setItemIconAlternateText(Object itemId, String altText) { + if (itemId != null) { + if (altText == null) { + throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE); + } else { + itemIconAlts.put(itemId, altText); + } + } + } + + /** + * Return the alternate text of an icon in a tree item. + * + * @param itemId + * Object with the ID of the item + * @return String with the alternate text of the icon, or null when no icon + * was set + */ + public String getItemIconAlternateText(Object itemId) { + String storedAlt = itemIconAlts.get(itemId); + return storedAlt == null ? "" : storedAlt; + } + /* Expanding and collapsing */ /** @@ -638,6 +709,8 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, if (icon != null) { target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON, getItemIcon(itemId)); + target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT, + getItemIconAlternateText(itemId)); } final String key = itemIdMapper.key(itemId); target.addAttribute("key", key); @@ -861,6 +934,37 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } + @Override + public void containerItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + super.containerItemSetChange(event); + if (getContainerDataSource() instanceof Filterable) { + boolean hasFilters = !((Filterable) getContainerDataSource()) + .getContainerFilters().isEmpty(); + if (!hasFilters) { + /* + * If Container is not filtered then the itemsetchange is caused + * by either adding or removing items to the container. To + * prevent a memory leak we should cleanup the expanded list + * from items which was removed. + * + * However, there will still be a leak if the container is + * filtered to show only a subset of the items in the tree and + * later unfiltered items are removed from the container. In + * that case references to the unfiltered item ids will remain + * in the expanded list until the Tree instance is removed and + * the list is destroyed, or the container data source is + * replaced/updated. To force the removal of the removed items + * the application developer needs to a) remove the container + * filters temporarly or b) re-apply the container datasource + * using setContainerDataSource(getContainerDataSource()) + */ + cleanupExpandedItems(); + } + } + + } + /* Expand event and listener */ /** diff --git a/server/src/com/vaadin/ui/UI.java b/server/src/com/vaadin/ui/UI.java index 796d1f08ea..e077b003b8 100644 --- a/server/src/com/vaadin/ui/UI.java +++ b/server/src/com/vaadin/ui/UI.java @@ -37,9 +37,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.communication.PushConnection; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.communication.PushMode; import com.vaadin.shared.ui.ui.ScrollClientRpc; +import com.vaadin.shared.ui.ui.UIClientRpc; import com.vaadin.shared.ui.ui.UIConstants; import com.vaadin.shared.ui.ui.UIServerRpc; import com.vaadin.shared.ui.ui.UIState; @@ -114,7 +117,10 @@ public abstract class UI extends AbstractSingleComponentContainer implements /** Identifies the click event */ private ConnectorTracker connectorTracker = new ConnectorTracker(this); - private Page page = new Page(this); + private Page page = new Page(this, getState(false).pageState); + + private LoadingIndicatorConfiguration loadingIndicatorConfiguration = new LoadingIndicatorConfigurationImpl( + this); /** * Scroll Y position. @@ -144,6 +150,14 @@ public abstract class UI extends AbstractSingleComponentContainer implements UI.this.scrollTop = scrollTop; UI.this.scrollLeft = scrollLeft; } + + @Override + public void poll() { + /* + * No-op. This is only called to cause a server visit to check for + * changes. + */ + } }; /** @@ -155,6 +169,9 @@ public abstract class UI extends AbstractSingleComponentContainer implements private boolean closing = false; + private TooltipConfiguration tooltipConfiguration = new TooltipConfigurationImpl( + 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. @@ -347,6 +364,12 @@ public abstract class UI extends AbstractSingleComponentContainer implements } else { if (session == null) { detach(); + if (pushConnection != null && pushConnection.isConnected()) { + // Close the push connection when UI is detached. Otherwise + // the push connection and possibly VaadinSession will live + // on. + pushConnection.disconnect(); + } } this.session = session; } @@ -466,6 +489,10 @@ public abstract class UI extends AbstractSingleComponentContainer implements private Navigator navigator; + private PushConnection pushConnection = null; + + private boolean hasPendingPush = false; + /** * This method is used by Component.Focusable objects to request focus to * themselves. Focus renders must be handled at window level (instead of @@ -659,10 +686,16 @@ public abstract class UI extends AbstractSingleComponentContainer implements /** * Should resize operations be lazy, i.e. should there be a delay before - * layout sizes are recalculated. Speeds up resize operations in slow UIs - * with the penalty of slightly decreased usability. + * layout sizes are recalculated and resize events are sent to the server. + * Speeds up resize operations in slow UIs with the penalty of slightly + * decreased usability. * <p> * Default value: <code>false</code> + * </p> + * <p> + * When there are active window resize listeners, lazy resize mode should be + * used to avoid a large number of events during resize. + * </p> * * @param resizeLazy * true to use a delay before recalculating sizes, false to @@ -986,6 +1019,15 @@ public abstract class UI extends AbstractSingleComponentContainer implements */ public void close() { closing = true; + + boolean sessionExpired = (session == null || session.isClosing()); + getRpcProxy(UIClientRpc.class).uiClosed(sessionExpired); + if (getPushConnection() != null && getPushConnection().isConnected()) { + // Push the Rpc to the client. The connection will be closed when + // the UI is detached and cleaned up. + getPushConnection().push(); + } + } /** @@ -1054,4 +1096,265 @@ public abstract class UI extends AbstractSingleComponentContainer implements public int getTabIndex() { return getState(false).tabIndex; } + + /** + * Provides exclusive access to this UI from outside a request handling + * thread. + * <p> + * The given runnable is executed while holding the session lock to ensure + * exclusive access to this UI and its session. The UI and related thread + * locals are set properly before executing the runnable. + * </p> + * <p> + * RPC handlers for components inside this UI do not need this method as the + * session is automatically locked by the framework during request handling. + * </p> + * <p> + * Note that calling this method while another session is locked by the + * current thread will cause an exception. This is to prevent deadlock + * situations when two threads have locked one session each and are both + * waiting for the lock for the other session. + * </p> + * + * @param runnable + * the runnable which accesses the UI + * @throws UIDetachedException + * if the UI is not attached to a session (and locking can + * therefore not be done) + * @throws IllegalStateException + * if the current thread holds the lock for another session + * + * @see #getCurrent() + * @see VaadinSession#access(Runnable) + * @see VaadinSession#lock() + */ + public void access(Runnable runnable) throws UIDetachedException { + Map<Class<?>, CurrentInstance> old = null; + + VaadinSession session = getSession(); + + if (session == null) { + throw new UIDetachedException(); + } + + VaadinService.verifyNoOtherSessionLocked(session); + + session.lock(); + try { + if (getSession() == null) { + // UI was detached after fetching the session but before we + // acquired the lock. + throw new UIDetachedException(); + } + old = CurrentInstance.setThreadLocals(this); + runnable.run(); + } finally { + session.unlock(); + if (old != null) { + CurrentInstance.restoreThreadLocals(old); + } + } + + } + + /** + * @deprecated As of 7.1.0.beta1, use {@link #access(Runnable)} instead. + * This method will be removed before the final 7.1.0 release. + */ + @Deprecated + public void runSafely(Runnable runnable) throws UIDetachedException { + access(runnable); + } + + /** + * Retrieves the object used for configuring tooltips. + * + * @return The instance used for tooltip configuration + */ + public TooltipConfiguration getTooltipConfiguration() { + return tooltipConfiguration; + } + + /** + * Retrieves the object used for configuring the loading indicator. + * + * @return The instance used for configuring the loading indicator + */ + public LoadingIndicatorConfiguration getLoadingIndicatorConfiguration() { + return loadingIndicatorConfiguration; + } + + /** + * Pushes the pending changes and client RPC invocations of this UI to the + * client-side. + * <p> + * As with all UI methods, it is not safe to call push() without holding the + * {@link VaadinSession#lock() session lock}. + * + * @throws IllegalStateException + * if push is disabled. + * @throws UIDetachedException + * if this UI is not attached to a session. + * + * @see #getPushMode() + * + * @since 7.1 + */ + public void push() { + VaadinSession session = getSession(); + if (session != null) { + assert session.hasLock(); + if (!getConnectorTracker().hasDirtyConnectors()) { + // Do not push if there is nothing to push + return; + } + + if (!getPushMode().isEnabled()) { + throw new IllegalStateException("Push not enabled"); + } + + if (pushConnection == null) { + hasPendingPush = true; + } else { + pushConnection.push(); + } + } else { + throw new UIDetachedException("Trying to push a detached UI"); + } + } + + /** + * Returns the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public PushConnection getPushConnection() { + return pushConnection; + } + + /** + * Sets the internal push connection object used by this UI. This method + * should only be called by the framework. + */ + public void setPushConnection(PushConnection pushConnection) { + // If pushMode is disabled then there should never be a pushConnection + assert (getPushMode().isEnabled() || pushConnection == null); + + if (pushConnection == this.pushConnection) { + return; + } + + if (this.pushConnection != null) { + this.pushConnection.disconnect(); + } + + this.pushConnection = pushConnection; + if (pushConnection != null && hasPendingPush) { + hasPendingPush = false; + pushConnection.push(); + } + } + + /** + * Sets the interval with which the UI should poll the server to see if + * there are any changes. Polling is disabled by default. + * <p> + * Note that it is possible to enable push and polling at the same time but + * it should not be done to avoid excessive server traffic. + * </p> + * <p> + * Add-on developers should note that this method is only meant for the + * application developer. An add-on should not set the poll interval + * directly, rather instruct the user to set it. + * </p> + * + * @param intervalInMillis + * The interval (in ms) with which the UI should poll the server + * or -1 to disable polling + */ + public void setPollInterval(int intervalInMillis) { + getState().pollInterval = intervalInMillis; + } + + /** + * Returns the interval with which the UI polls the server. + * + * @return The interval (in ms) with which the UI polls the server or -1 if + * polling is disabled + */ + public int getPollInterval() { + return getState(false).pollInterval; + } + + /** + * Returns the mode of bidirectional ("push") communication that is used in + * this UI. + * + * @return The push mode. + */ + public PushMode getPushMode() { + return getState(false).pushMode; + } + + /** + * Sets the mode of bidirectional ("push") communication that should be used + * in this UI. + * <p> + * Add-on developers should note that this method is only meant for the + * application developer. An add-on should not set the push mode directly, + * rather instruct the user to set it. + * </p> + * + * @param pushMode + * The push mode to use. + * + * @throws IllegalArgumentException + * if the argument is null. + * @throws IllegalStateException + * if push support is not available. + */ + public void setPushMode(PushMode pushMode) { + if (pushMode == null) { + throw new IllegalArgumentException("Push mode cannot be null"); + } + + if (pushMode.isEnabled()) { + VaadinSession session = getSession(); + if (session != null && !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().pushMode = pushMode; + } + + /** + * Get the label that is added to the container element, where tooltip, + * notification and dialogs are added to. + * + * @return the label of the container + */ + public String getOverlayContainerLabel() { + return getState().overlayContainerLabel; + } + + /** + * Sets the label that is added to the container element, where tooltip, + * notifications and dialogs are added to. + * <p> + * This is helpful for users of assistive devices, as this element is + * reachable for them. + * </p> + * + * @param overlayContainerLabel + * label to use for the container + */ + public void setOverlayContainerLabel(String overlayContainerLabel) { + getState().overlayContainerLabel = overlayContainerLabel; + } } diff --git a/server/src/com/vaadin/ui/UIDetachedException.java b/server/src/com/vaadin/ui/UIDetachedException.java new file mode 100644 index 0000000000..07207b0bf3 --- /dev/null +++ b/server/src/com/vaadin/ui/UIDetachedException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui; + +/** + * Exception thrown if the UI has been detached when it should not be. + * + * @author Vaadin Ltd + * @since 7.1 + */ +public class UIDetachedException extends RuntimeException { + + public UIDetachedException() { + super(); + } + + public UIDetachedException(String message, Throwable cause) { + super(message, cause); + } + + public UIDetachedException(String message) { + super(message); + } + + public UIDetachedException(Throwable cause) { + super(cause); + } + +} diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java index d8b33e6b25..9f64c9118e 100644 --- a/server/src/com/vaadin/ui/Window.java +++ b/server/src/com/vaadin/ui/Window.java @@ -35,8 +35,10 @@ import com.vaadin.server.ClientConnector; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; 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.util.ReflectTools; /** * A component that represents a floating popup window that can be added to a @@ -71,6 +73,11 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, public void click(MouseEventDetails mouseDetails) { fireEvent(new ClickEvent(Window.this, mouseDetails)); } + + @Override + public void windowModeChanged(WindowMode newState) { + setWindowMode(newState); + } }; /** @@ -234,10 +241,10 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, /** * Gets the distance of Window left border in pixels from left border of the - * containing (main window). + * containing (main window) when the window is in {@link WindowMode#NORMAL}. * * @return the Distance of Window left border in pixels from left border of - * the containing (main window). or -1 if unspecified. + * the containing (main window).or -1 if unspecified * @since 4.0.0 */ public int getPositionX() { @@ -246,7 +253,8 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, /** * Sets the distance of Window left border in pixels from left border of the - * containing (main window). + * containing (main window). Has effect only if in {@link WindowMode#NORMAL} + * mode. * * @param positionX * the Distance of Window left border in pixels from left border @@ -260,10 +268,11 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, /** * Gets the distance of Window top border in pixels from top border of the - * containing (main window). + * containing (main window) when the window is in {@link WindowMode#NORMAL} + * state, or when next set to that state. * * @return Distance of Window top border in pixels from top border of the - * containing (main window). or -1 if unspecified . + * containing (main window). or -1 if unspecified * * @since 4.0.0 */ @@ -273,7 +282,8 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, /** * Sets the distance of Window top border in pixels from top border of the - * containing (main window). + * containing (main window). Has effect only if in {@link WindowMode#NORMAL} + * mode. * * @param positionY * the Distance of Window top border in pixels from top border of @@ -402,6 +412,101 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, } /** + * Event which is fired when the mode of the Window changes. + * + * @author Vaadin Ltd + * @since 7.1 + * + */ + public static class WindowModeChangeEvent extends Component.Event { + + private final WindowMode windowMode; + + /** + * + * @param source + */ + public WindowModeChangeEvent(Component source, WindowMode windowMode) { + super(source); + this.windowMode = windowMode; + } + + /** + * Gets the Window. + * + * @return the window + */ + public Window getWindow() { + return (Window) getSource(); + } + + /** + * Gets the new window mode. + * + * @return the new mode + */ + public WindowMode getWindowMode() { + return windowMode; + } + } + + /** + * An interface used for listening to Window maximize / restore events. Add + * the WindowModeChangeListener to a window and + * {@link WindowModeChangeListener#windowModeChanged(WindowModeChangeEvent)} + * will be called whenever the window is maximized ( + * {@link WindowMode#MAXIMIZED}) or restored ({@link WindowMode#NORMAL} ). + */ + public interface WindowModeChangeListener extends Serializable { + + public static final Method windowModeChangeMethod = ReflectTools + .findMethod(WindowModeChangeListener.class, + "windowModeChanged", WindowModeChangeEvent.class); + + /** + * Called when the user maximizes / restores a window. Use + * {@link WindowModeChangeEvent#getWindow()} to get a reference to the + * {@link Window} that was maximized / restored. Use + * {@link WindowModeChangeEvent#getWindowMode()} to get a reference to + * the new state. + * + * @param event + */ + public void windowModeChanged(WindowModeChangeEvent event); + } + + /** + * Adds a WindowModeChangeListener to the window. + * + * The WindowModeChangeEvent is fired when the user changed the display + * state by clicking the maximize/restore button or by double clicking on + * the window header. The event is also fired if the state is changed using + * {@link #setWindowMode(WindowMode)}. + * + * @param listener + * the WindowModeChangeListener to add. + */ + public void addWindowModeChangeListener(WindowModeChangeListener listener) { + addListener(WindowModeChangeEvent.class, listener, + WindowModeChangeListener.windowModeChangeMethod); + } + + /** + * Removes the WindowModeChangeListener from the window. + * + * @param listener + * the WindowModeChangeListener to remove. + */ + public void removeWindowModeChangeListener(WindowModeChangeListener listener) { + removeListener(WindowModeChangeEvent.class, listener, + WindowModeChangeListener.windowModeChangeMethod); + } + + protected void fireWindowWindowModeChange() { + fireEvent(new Window.WindowModeChangeEvent(this, getState().windowMode)); + } + + /** * Method for the resize event. */ private static final Method WINDOW_RESIZE_METHOD; @@ -670,6 +775,30 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, getState().draggable = draggable; } + /** + * Gets the current mode of the window. + * + * @see WindowMode + * @return the mode of the window. + */ + public WindowMode getWindowMode() { + return getState(false).windowMode; + } + + /** + * Sets the mode for the window + * + * @see WindowMode + * @param windowMode + * The new mode + */ + public void setWindowMode(WindowMode windowMode) { + if (windowMode != getWindowMode()) { + getState().windowMode = windowMode; + fireWindowWindowModeChange(); + } + } + /* * Actions */ @@ -873,4 +1002,9 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, protected WindowState getState() { return (WindowState) super.getState(); } + + @Override + protected WindowState getState(boolean markAsDirty) { + return (WindowState) super.getState(markAsDirty); + } } diff --git a/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvent.java b/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvent.java new file mode 100644 index 0000000000..1f012157b5 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvent.java @@ -0,0 +1,51 @@ +/* + * 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.components.calendar; + +import com.vaadin.ui.Calendar; +import com.vaadin.ui.Component; + +/** + * All Calendar events extends this class. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +@SuppressWarnings("serial") +public class CalendarComponentEvent extends Component.Event { + + /** + * Set the source of the event + * + * @param source + * The source calendar + * + */ + public CalendarComponentEvent(Calendar source) { + super(source); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Event#getComponent() + */ + @Override + public Calendar getComponent() { + return (Calendar) super.getComponent(); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvents.java b/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvents.java new file mode 100644 index 0000000000..1904d69898 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/CalendarComponentEvents.java @@ -0,0 +1,603 @@ +/* + * 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.components.calendar; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.EventListener; + +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.ui.Calendar; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.util.ReflectTools; + +/** + * Interface for all Vaadin Calendar events. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarComponentEvents extends Serializable { + + /** + * Notifier interface for notifying listener of calendar events + */ + public interface CalendarEventNotifier extends Serializable { + /** + * Get the assigned event handler for the given eventId. + * + * @param eventId + * @return the assigned eventHandler, or null if no handler is assigned + */ + public EventListener getHandler(String eventId); + } + + /** + * Notifier interface for event drag & drops. + */ + public interface EventMoveNotifier extends CalendarEventNotifier { + + /** + * Set the EventMoveHandler. + * + * @param listener + * EventMoveHandler to be added + */ + public void setHandler(EventMoveHandler listener); + + } + + /** + * MoveEvent is sent when existing event is dragged to a new position. + */ + @SuppressWarnings("serial") + public class MoveEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTMOVE; + + /** Index for the moved Schedule.Event. */ + private CalendarEvent calendarEvent; + + /** New starting date for the moved Calendar.Event. */ + private Date newStart; + + /** + * MoveEvent needs the target event and new start date. + * + * @param source + * Calendar component. + * @param calendarEvent + * Target event. + * @param newStart + * Target event's new start date. + */ + public MoveEvent(Calendar source, CalendarEvent calendarEvent, + Date newStart) { + super(source); + + this.calendarEvent = calendarEvent; + this.newStart = newStart; + } + + /** + * Get target event. + * + * @return Target event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + /** + * Get new start date. + * + * @return New start date. + */ + public Date getNewStart() { + return newStart; + } + } + + /** + * Handler interface for when events are being dragged on the calendar + * + */ + public interface EventMoveHandler extends EventListener, Serializable { + + /** Trigger method for the MoveEvent. */ + public static final Method eventMoveMethod = ReflectTools.findMethod( + EventMoveHandler.class, "eventMove", MoveEvent.class); + + /** + * This method will be called when event has been moved to a new + * position. + * + * @param event + * MoveEvent containing specific information of the new + * position and target event. + */ + public void eventMove(MoveEvent event); + } + + /** + * Handler interface for day or time cell drag-marking with mouse. + */ + public interface RangeSelectNotifier extends Serializable, + CalendarEventNotifier { + + /** + * Set the RangeSelectHandler that listens for drag-marking. + * + * @param listener + * RangeSelectHandler to be added. + */ + public void setHandler(RangeSelectHandler listener); + } + + /** + * RangeSelectEvent is sent when day or time cells are drag-marked with + * mouse. + */ + @SuppressWarnings("serial") + public class RangeSelectEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.RANGESELECT; + + /** Calendar event's start date. */ + private Date start; + + /** Calendar event's end date. */ + private Date end; + + /** + * Defines the event's view mode. + */ + private boolean monthlyMode; + + /** + * RangeSelectEvent needs a start and end date. + * + * @param source + * Calendar component. + * @param start + * Start date. + * @param end + * End date. + * @param monthlyMode + * Calendar view mode. + */ + public RangeSelectEvent(Calendar source, Date start, Date end, + boolean monthlyMode) { + super(source); + this.start = start; + this.end = end; + this.monthlyMode = monthlyMode; + } + + /** + * Get start date. + * + * @return Start date. + */ + public Date getStart() { + return start; + } + + /** + * Get end date. + * + * @return End date. + */ + public Date getEnd() { + return end; + } + + /** + * Gets the event's view mode. Calendar can be be either in monthly or + * weekly mode, depending on the active date range. + * + * @deprecated User {@link Calendar#isMonthlyMode()} instead + * + * @return Returns true when monthly view is active. + */ + @Deprecated + public boolean isMonthlyMode() { + return monthlyMode; + } + } + + /** RangeSelectHandler handles RangeSelectEvent. */ + public interface RangeSelectHandler extends EventListener, Serializable { + + /** Trigger method for the RangeSelectEvent. */ + public static final Method rangeSelectMethod = ReflectTools + .findMethod(RangeSelectHandler.class, "rangeSelect", + RangeSelectEvent.class); + + /** + * This method will be called when day or time cells are drag-marked + * with mouse. + * + * @param event + * RangeSelectEvent that contains range start and end date. + */ + public void rangeSelect(RangeSelectEvent event); + } + + /** Notifier interface for navigation listening. */ + public interface NavigationNotifier extends Serializable { + /** + * Add a forward navigation listener. + * + * @param handler + * ForwardHandler to be added. + */ + public void setHandler(ForwardHandler handler); + + /** + * Add a backward navigation listener. + * + * @param handler + * BackwardHandler to be added. + */ + public void setHandler(BackwardHandler handler); + + /** + * Add a date click listener. + * + * @param handler + * DateClickHandler to be added. + */ + public void setHandler(DateClickHandler handler); + + /** + * Add a event click listener. + * + * @param handler + * EventClickHandler to be added. + */ + public void setHandler(EventClickHandler handler); + + /** + * Add a week click listener. + * + * @param handler + * WeekClickHandler to be added. + */ + public void setHandler(WeekClickHandler handler); + } + + /** + * ForwardEvent is sent when forward navigation button is clicked. + */ + @SuppressWarnings("serial") + public class ForwardEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.FORWARD; + + /** + * ForwardEvent needs only the source component. + * + * @param source + * Calendar component. + */ + public ForwardEvent(Calendar source) { + super(source); + } + } + + /** ForwardHandler handles ForwardEvent. */ + public interface ForwardHandler extends EventListener, Serializable { + + /** Trigger method for the ForwardEvent. */ + public static final Method forwardMethod = ReflectTools.findMethod( + ForwardHandler.class, "forward", ForwardEvent.class); + + /** + * This method will be called when date range is moved forward. + * + * @param event + * ForwardEvent + */ + public void forward(ForwardEvent event); + } + + /** + * BackwardEvent is sent when backward navigation button is clicked. + */ + @SuppressWarnings("serial") + public class BackwardEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.BACKWARD; + + /** + * BackwardEvent needs only the source source component. + * + * @param source + * Calendar component. + */ + public BackwardEvent(Calendar source) { + super(source); + } + } + + /** BackwardHandler handles BackwardEvent. */ + public interface BackwardHandler extends EventListener, Serializable { + + /** Trigger method for the BackwardEvent. */ + public static final Method backwardMethod = ReflectTools.findMethod( + BackwardHandler.class, "backward", BackwardEvent.class); + + /** + * This method will be called when date range is moved backwards. + * + * @param event + * BackwardEvent + */ + public void backward(BackwardEvent event); + } + + /** + * DateClickEvent is sent when a date is clicked. + */ + @SuppressWarnings("serial") + public class DateClickEvent extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.DATECLICK; + + /** Date that was clicked. */ + private Date date; + + /** DateClickEvent needs the target date that was clicked. */ + public DateClickEvent(Calendar source, Date date) { + super(source); + this.date = date; + } + + /** + * Get clicked date. + * + * @return Clicked date. + */ + public Date getDate() { + return date; + } + } + + /** DateClickHandler handles DateClickEvent. */ + public interface DateClickHandler extends EventListener, Serializable { + + /** Trigger method for the DateClickEvent. */ + public static final Method dateClickMethod = ReflectTools.findMethod( + DateClickHandler.class, "dateClick", DateClickEvent.class); + + /** + * This method will be called when a date is clicked. + * + * @param event + * DateClickEvent containing the target date. + */ + public void dateClick(DateClickEvent event); + } + + /** + * EventClick is sent when an event is clicked. + */ + @SuppressWarnings("serial") + public class EventClick extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTCLICK; + + /** Clicked source event. */ + private CalendarEvent calendarEvent; + + /** Target source event is needed for the EventClick. */ + public EventClick(Calendar source, CalendarEvent calendarEvent) { + super(source); + this.calendarEvent = calendarEvent; + } + + /** + * Get the clicked event. + * + * @return Clicked event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + } + + /** EventClickHandler handles EventClick. */ + public interface EventClickHandler extends EventListener, Serializable { + + /** Trigger method for the EventClick. */ + public static final Method eventClickMethod = ReflectTools.findMethod( + EventClickHandler.class, "eventClick", EventClick.class); + + /** + * This method will be called when an event is clicked. + * + * @param event + * EventClick containing the target event. + */ + public void eventClick(EventClick event); + } + + /** + * WeekClick is sent when week is clicked. + */ + @SuppressWarnings("serial") + public class WeekClick extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.WEEKCLICK; + + /** Target week. */ + private int week; + + /** Target year. */ + private int year; + + /** + * WeekClick needs a target year and week. + * + * @param source + * Target source. + * @param week + * Target week. + * @param year + * Target year. + */ + public WeekClick(Calendar source, int week, int year) { + super(source); + this.week = week; + this.year = year; + } + + /** + * Get week as a integer. See {@link java.util.Calendar} for the allowed + * values. + * + * @return Week as a integer. + */ + public int getWeek() { + return week; + } + + /** + * Get year as a integer. See {@link java.util.Calendar} for the allowed + * values. + * + * @return Year as a integer + */ + public int getYear() { + return year; + } + } + + /** WeekClickHandler handles WeekClicks. */ + public interface WeekClickHandler extends EventListener, Serializable { + + /** Trigger method for the WeekClick. */ + public static final Method weekClickMethod = ReflectTools.findMethod( + WeekClickHandler.class, "weekClick", WeekClick.class); + + /** + * This method will be called when a week is clicked. + * + * @param event + * WeekClick containing the target week and year. + */ + public void weekClick(WeekClick event); + } + + /** + * EventResize is sent when an event is resized + */ + @SuppressWarnings("serial") + public class EventResize extends CalendarComponentEvent { + + public static final String EVENT_ID = CalendarEventId.EVENTRESIZE; + + private CalendarEvent calendarEvent; + + private Date startTime; + + private Date endTime; + + public EventResize(Calendar source, CalendarEvent calendarEvent, + Date startTime, Date endTime) { + super(source); + this.calendarEvent = calendarEvent; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Get target event. + * + * @return Target event. + */ + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + /** + * @deprecated Use {@link #getNewStart()} instead + * + * @return the new start time + */ + @Deprecated + public Date getNewStartTime() { + return startTime; + } + + /** + * Returns the updated start date/time of the event + * + * @return The new date for the event + */ + public Date getNewStart() { + return startTime; + } + + /** + * @deprecated Use {@link #getNewEnd()} instead + * + * @return the new end time + */ + @Deprecated + public Date getNewEndTime() { + return endTime; + } + + /** + * Returns the updates end date/time of the event + * + * @return The new date for the event + */ + public Date getNewEnd() { + return endTime; + } + } + + /** + * Notifier interface for event resizing. + */ + public interface EventResizeNotifier extends Serializable { + + /** + * Set a EventResizeHandler. + * + * @param handler + * EventResizeHandler to be set + */ + public void setHandler(EventResizeHandler handler); + } + + /** + * Handler for EventResize event. + */ + public interface EventResizeHandler extends EventListener, Serializable { + + /** Trigger method for the EventResize. */ + public static final Method eventResizeMethod = ReflectTools.findMethod( + EventResizeHandler.class, "eventResize", EventResize.class); + + void eventResize(EventResize event); + } + +} diff --git a/server/src/com/vaadin/ui/components/calendar/CalendarDateRange.java b/server/src/com/vaadin/ui/components/calendar/CalendarDateRange.java new file mode 100644 index 0000000000..01b766a6db --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/CalendarDateRange.java @@ -0,0 +1,86 @@ +/* + * 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.components.calendar; + +import java.io.Serializable; +import java.util.Date; +import java.util.TimeZone; + +/** + * Class for representing a date range. + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +@SuppressWarnings("serial") +public class CalendarDateRange implements Serializable { + + private Date start; + + private Date end; + + private final transient TimeZone tz; + + /** + * Constructor + * + * @param start + * The start date and time of the date range + * @param end + * The end date and time of the date range + */ + public CalendarDateRange(Date start, Date end, TimeZone tz) { + super(); + this.start = start; + this.end = end; + this.tz = tz; + } + + /** + * Get the start date of the date range + * + * @return the start Date of the range + */ + public Date getStart() { + return start; + } + + /** + * Get the end date of the date range + * + * @return the end Date of the range + */ + public Date getEnd() { + return end; + } + + /** + * Is a date in the date range + * + * @param date + * The date to check + * @return true if the date range contains a date start and end of range + * inclusive; false otherwise + */ + public boolean inRange(Date date) { + if (date == null) { + return false; + } + + return date.compareTo(start) >= 0 && date.compareTo(end) <= 0; + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/CalendarTargetDetails.java b/server/src/com/vaadin/ui/components/calendar/CalendarTargetDetails.java new file mode 100644 index 0000000000..1a3ef67377 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/CalendarTargetDetails.java @@ -0,0 +1,80 @@ +/* + * 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.components.calendar; + +import java.util.Date; +import java.util.Map; + +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetailsImpl; +import com.vaadin.ui.Calendar; + +/** + * Drop details for {@link com.vaadin.ui.addon.calendar.ui.Calendar Calendar}. + * When something is dropped on the Calendar, this class contains the specific + * details of the drop point. Specifically, this class gives access to the date + * where the drop happened. If the Calendar was in weekly mode, the date also + * includes the start time of the slot. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class CalendarTargetDetails extends TargetDetailsImpl { + + private boolean hasDropTime; + + public CalendarTargetDetails(Map<String, Object> rawDropData, + DropTarget dropTarget) { + super(rawDropData, dropTarget); + } + + /** + * @return true if {@link #getDropTime()} will return a date object with the + * time set to the start of the time slot where the drop happened + */ + public boolean hasDropTime() { + return hasDropTime; + } + + /** + * Does the dropped item have a time associated with it + * + * @param hasDropTime + */ + public void setHasDropTime(boolean hasDropTime) { + this.hasDropTime = hasDropTime; + } + + /** + * @return the date where the drop happened + */ + public Date getDropTime() { + if (hasDropTime) { + return (Date) getData("dropTime"); + } else { + return (Date) getData("dropDay"); + } + } + + /** + * @return the {@link com.vaadin.ui.addon.calendar.ui.Calendar Calendar} + * instance which was the target of the drop + */ + public Calendar getTargetCalendar() { + return (Calendar) getTarget(); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/ContainerEventProvider.java b/server/src/com/vaadin/ui/components/calendar/ContainerEventProvider.java new file mode 100644 index 0000000000..37ea255d27 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/ContainerEventProvider.java @@ -0,0 +1,577 @@ +/* + * 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.components.calendar; + +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.event.BasicEvent; +import com.vaadin.ui.components.calendar.event.CalendarEditableEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeListener; +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeNotifier; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier; + +/** + * A event provider which uses a {@link Container} as a datasource. Container + * used as data source. + * + * NOTE: The data source must be sorted by date! + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class ContainerEventProvider implements CalendarEditableEventProvider, + EventSetChangeNotifier, EventChangeNotifier, EventMoveHandler, + EventResizeHandler, Container.ItemSetChangeListener, + Property.ValueChangeListener { + + // Default property ids + public static final String CAPTION_PROPERTY = "caption"; + public static final String DESCRIPTION_PROPERTY = "description"; + public static final String STARTDATE_PROPERTY = "start"; + public static final String ENDDATE_PROPERTY = "end"; + public static final String STYLENAME_PROPERTY = "styleName"; + + /** + * Internal class to keep the container index which item this event + * represents + * + */ + private class ContainerCalendarEvent extends BasicEvent { + private final int index; + + public ContainerCalendarEvent(int containerIndex) { + super(); + index = containerIndex; + } + + public int getContainerIndex() { + return index; + } + } + + /** + * Listeners attached to the container + */ + private final List<EventSetChangeListener> eventSetChangeListeners = new LinkedList<CalendarEventProvider.EventSetChangeListener>(); + private final List<EventChangeListener> eventChangeListeners = new LinkedList<CalendarEvent.EventChangeListener>(); + + /** + * The event cache contains the events previously created by + * {@link #getEvents(Date, Date)} + */ + private final List<CalendarEvent> eventCache = new LinkedList<CalendarEvent>(); + + /** + * The container used as datasource + */ + private Indexed container; + + /** + * Container properties. Defaults based on using the {@link BasicEvent} + * helper class. + */ + private Object captionProperty = CAPTION_PROPERTY; + private Object descriptionProperty = DESCRIPTION_PROPERTY; + private Object startDateProperty = STARTDATE_PROPERTY; + private Object endDateProperty = ENDDATE_PROPERTY; + private Object styleNameProperty = STYLENAME_PROPERTY; + + /** + * Constructor + * + * @param container + * Container to use as a data source. + */ + public ContainerEventProvider(Container.Indexed container) { + this.container = container; + listenToContainerEvents(); + } + + /** + * Set the container data source + * + * @param container + * The container to use as datasource + * + */ + public void setContainerDataSource(Container.Indexed container) { + // Detach the previous container + detachContainerDataSource(); + + this.container = container; + listenToContainerEvents(); + } + + /** + * Returns the container used as data source + * + */ + public Container.Indexed getContainerDataSource() { + return container; + } + + /** + * Attaches listeners to the container so container events can be processed + */ + private void listenToContainerEvents() { + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container).addItemSetChangeListener(this); + } + if (container instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) container).addValueChangeListener(this); + } + } + + /** + * Removes listeners from the container so no events are processed + */ + private void ignoreContainerEvents() { + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(this); + } + if (container instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) container).removeValueChangeListener(this); + } + } + + /** + * Converts an event in the container to an {@link CalendarEvent} + * + * @param index + * The index of the item in the container to get the event for + * @return + */ + private CalendarEvent getEvent(int index) { + + // Check the event cache first + for (CalendarEvent e : eventCache) { + if (e instanceof ContainerCalendarEvent + && ((ContainerCalendarEvent) e).getContainerIndex() == index) { + return e; + } else if (container.getIdByIndex(index) == e) { + return e; + } + } + + final Object id = container.getIdByIndex(index); + Item item = container.getItem(id); + CalendarEvent event; + if (id instanceof CalendarEvent) { + /* + * If we are using the BeanItemContainer or another container which + * stores the objects as ids then just return the instances + */ + event = (CalendarEvent) id; + + } else { + /* + * Else we use the properties to create the event + */ + BasicEvent basicEvent = new ContainerCalendarEvent(index); + + // Set values from property values + if (captionProperty != null + && item.getItemPropertyIds().contains(captionProperty)) { + basicEvent.setCaption(String.valueOf(item.getItemProperty( + captionProperty).getValue())); + } + if (descriptionProperty != null + && item.getItemPropertyIds().contains(descriptionProperty)) { + basicEvent.setDescription(String.valueOf(item.getItemProperty( + descriptionProperty).getValue())); + } + if (startDateProperty != null + && item.getItemPropertyIds().contains(startDateProperty)) { + basicEvent.setStart((Date) item.getItemProperty( + startDateProperty).getValue()); + } + if (endDateProperty != null + && item.getItemPropertyIds().contains(endDateProperty)) { + basicEvent.setEnd((Date) item.getItemProperty(endDateProperty) + .getValue()); + } + if (styleNameProperty != null + && item.getItemPropertyIds().contains(styleNameProperty)) { + basicEvent.setDescription(String.valueOf(item.getItemProperty( + descriptionProperty).getValue())); + } + event = basicEvent; + } + return event; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + eventCache.clear(); + + int[] rangeIndexes = getFirstAndLastEventIndex(startDate, endDate); + for (int i = rangeIndexes[0]; i <= rangeIndexes[1] + && i < container.size(); i++) { + eventCache.add(getEvent(i)); + } + return Collections.unmodifiableList(eventCache); + } + + /** + * Get the first event for a date + * + * @param date + * The date to search for, NUll returns first event in container + * @return Returns an array where the first item is the start index and the + * second item is the end item + */ + private int[] getFirstAndLastEventIndex(Date start, Date end) { + int startIndex = 0; + int size = container.size(); + int endIndex = size - 1; + + if (start != null) { + /* + * Iterating from the start of the container, if range is in the end + * of the container then this will be slow TODO This could be + * improved by using some sort of divide and conquer algorithm + */ + while (startIndex < size) { + Object id = container.getIdByIndex(startIndex); + Item item = container.getItem(id); + Date d = (Date) item.getItemProperty(startDateProperty) + .getValue(); + if (d.compareTo(start) >= 0) { + break; + } + startIndex++; + } + } + + if (end != null) { + /* + * Iterate from the start index until range ends + */ + endIndex = startIndex; + while (endIndex < size - 1) { + Object id = container.getIdByIndex(endIndex); + Item item = container.getItem(id); + Date d = (Date) item.getItemProperty(endDateProperty) + .getValue(); + if (d == null) { + // No end date present, use start date + d = (Date) item.getItemProperty(startDateProperty) + .getValue(); + } + if (d.compareTo(end) >= 0) { + endIndex--; + break; + } + endIndex++; + } + } + + return new int[] { startIndex, endIndex }; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider.EventSetChangeNotifier + * #addListener(com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeListener) + */ + @Override + public void addEventSetChangeListener(EventSetChangeListener listener) { + if (!eventSetChangeListeners.contains(listener)) { + eventSetChangeListeners.add(listener); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider.EventSetChangeNotifier + * #removeListener(com.vaadin.addon.calendar.event.CalendarEventProvider. + * EventSetChangeListener) + */ + @Override + public void removeEventSetChangeListener(EventSetChangeListener listener) { + eventSetChangeListeners.remove(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier#addListener + * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener) + */ + @Override + public void addEventChangeListener(EventChangeListener listener) { + if (eventChangeListeners.contains(listener)) { + eventChangeListeners.add(listener); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier# + * removeListener + * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener) + */ + @Override + public void removeEventChangeListener(EventChangeListener listener) { + eventChangeListeners.remove(listener); + } + + /** + * Get the property which provides the caption of the event + */ + public Object getCaptionProperty() { + return captionProperty; + } + + /** + * Set the property which provides the caption of the event + */ + public void setCaptionProperty(Object captionProperty) { + this.captionProperty = captionProperty; + } + + /** + * Get the property which provides the description of the event + */ + public Object getDescriptionProperty() { + return descriptionProperty; + } + + /** + * Set the property which provides the description of the event + */ + public void setDescriptionProperty(Object descriptionProperty) { + this.descriptionProperty = descriptionProperty; + } + + /** + * Get the property which provides the starting date and time of the event + */ + public Object getStartDateProperty() { + return startDateProperty; + } + + /** + * Set the property which provides the starting date and time of the event + */ + public void setStartDateProperty(Object startDateProperty) { + this.startDateProperty = startDateProperty; + } + + /** + * Get the property which provides the ending date and time of the event + */ + public Object getEndDateProperty() { + return endDateProperty; + } + + /** + * Set the property which provides the ending date and time of the event + */ + public void setEndDateProperty(Object endDateProperty) { + this.endDateProperty = endDateProperty; + } + + /** + * Get the property which provides the style name for the event + */ + public Object getStyleNameProperty() { + return styleNameProperty; + } + + /** + * Set the property which provides the style name for the event + */ + public void setStyleNameProperty(Object styleNameProperty) { + this.styleNameProperty = styleNameProperty; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange + * (com.vaadin.data.Container.ItemSetChangeEvent) + */ + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + if (event.getContainer() == container) { + // Trigger an eventset change event when the itemset changes + for (EventSetChangeListener listener : eventSetChangeListeners) { + listener.eventSetChange(new EventSetChangeEvent(this)); + } + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Property.ValueChangeListener#valueChange(com.vaadin.data + * .Property.ValueChangeEvent) + */ + @Override + public void valueChange(ValueChangeEvent event) { + /* + * TODO Need to figure out how to get the item which triggered the the + * valuechange event and then trigger a EventChange event to the + * listeners + */ + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler + * #eventMove + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent) + */ + @Override + public void eventMove(MoveEvent event) { + CalendarEvent ce = event.getCalendarEvent(); + if (eventCache.contains(ce)) { + int index; + if (ce instanceof ContainerCalendarEvent) { + index = ((ContainerCalendarEvent) ce).getContainerIndex(); + } else { + index = container.indexOfId(ce); + } + + long eventLength = ce.getEnd().getTime() - ce.getStart().getTime(); + Date newEnd = new Date(event.getNewStart().getTime() + eventLength); + + ignoreContainerEvents(); + Item item = container.getItem(container.getIdByIndex(index)); + item.getItemProperty(startDateProperty).setValue( + event.getNewStart()); + item.getItemProperty(endDateProperty).setValue(newEnd); + listenToContainerEvents(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * #eventResize + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize) + */ + @Override + public void eventResize(EventResize event) { + CalendarEvent ce = event.getCalendarEvent(); + if (eventCache.contains(ce)) { + int index; + if (ce instanceof ContainerCalendarEvent) { + index = ((ContainerCalendarEvent) ce).getContainerIndex(); + } else { + index = container.indexOfId(ce); + } + ignoreContainerEvents(); + Item item = container.getItem(container.getIdByIndex(index)); + item.getItemProperty(startDateProperty).setValue( + event.getNewStart()); + item.getItemProperty(endDateProperty).setValue(event.getNewEnd()); + listenToContainerEvents(); + } + } + + /** + * If you are reusing the container which previously have been attached to + * this ContainerEventProvider call this method to remove this event + * providers container listeners before attaching it to an other + * ContainerEventProvider + */ + public void detachContainerDataSource() { + ignoreContainerEvents(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + Item item; + try { + item = container.addItem(event); + } catch (UnsupportedOperationException uop) { + // Thrown if container does not support adding items with custom + // ids. JPAContainer for example. + item = container.getItem(container.addItem()); + } + if (item != null) { + item.getItemProperty(getCaptionProperty()).setValue( + event.getCaption()); + item.getItemProperty(getStartDateProperty()).setValue( + event.getStart()); + item.getItemProperty(getEndDateProperty()).setValue(event.getEnd()); + item.getItemProperty(getStyleNameProperty()).setValue( + event.getStyleName()); + item.getItemProperty(getDescriptionProperty()).setValue( + event.getDescription()); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + container.removeItem(event); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/BasicEvent.java b/server/src/com/vaadin/ui/components/calendar/event/BasicEvent.java new file mode 100644 index 0000000000..3f14145f0c --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/BasicEvent.java @@ -0,0 +1,265 @@ +/* + * 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.components.calendar.event; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeNotifier; + +/** + * Simple implementation of + * {@link com.vaadin.addon.calendar.event.CalendarEvent CalendarEvent}. Has + * setters for all required fields and fires events when this event is changed. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEvent implements EditableCalendarEvent, EventChangeNotifier { + + private String caption; + private String description; + private Date end; + private Date start; + private String styleName; + private transient List<EventChangeListener> listeners = new ArrayList<EventChangeListener>(); + + private boolean isAllDay; + + /** + * Default constructor + */ + public BasicEvent() { + + } + + /** + * Constructor for creating an event with the same start and end date + * + * @param caption + * The caption for the event + * @param description + * The description for the event + * @param date + * The date the event occurred + */ + public BasicEvent(String caption, String description, Date date) { + this.caption = caption; + this.description = description; + start = date; + end = date; + } + + /** + * Constructor for creating an event with a start date and an end date. + * Start date should be before the end date + * + * @param caption + * The caption for the event + * @param description + * The description for the event + * @param startDate + * The start date of the event + * @param endDate + * The end date of the event + */ + public BasicEvent(String caption, String description, Date startDate, + Date endDate) { + this.caption = caption; + this.description = description; + start = startDate; + end = endDate; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getCaption() + */ + @Override + public String getCaption() { + return caption; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getDescription() + */ + @Override + public String getDescription() { + return description; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + */ + @Override + public Date getEnd() { + return end; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + */ + @Override + public Date getStart() { + return start; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + */ + @Override + public String getStyleName() { + return styleName; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.addon.calendar.event.CalendarEvent#isAllDay() + */ + @Override + public boolean isAllDay() { + return isAllDay; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setCaption(java.lang + * .String) + */ + @Override + public void setCaption(String caption) { + this.caption = caption; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setDescription(java + * .lang.String) + */ + @Override + public void setDescription(String description) { + this.description = description; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setEnd(java.util. + * Date) + */ + @Override + public void setEnd(Date end) { + this.end = end; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setStart(java.util + * .Date) + */ + @Override + public void setStart(Date start) { + this.start = start; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setStyleName(java + * .lang.String) + */ + @Override + public void setStyleName(String styleName) { + this.styleName = styleName; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventEditor#setAllDay(boolean) + */ + @Override + public void setAllDay(boolean isAllDay) { + this.isAllDay = isAllDay; + fireEventChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * ) + */ + @Override + public void addEventChangeListener(EventChangeListener listener) { + listeners.add(listener); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier + * #removeListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * ) + */ + @Override + public void removeEventChangeListener(EventChangeListener listener) { + listeners.remove(listener); + } + + /** + * Fires an event change event to the listeners. Should be triggered when + * some property of the event changes. + */ + protected void fireEventChange() { + EventChangeEvent event = new EventChangeEvent(this); + + for (EventChangeListener listener : listeners) { + listener.eventChange(event); + } + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/BasicEventProvider.java b/server/src/com/vaadin/ui/components/calendar/event/BasicEventProvider.java new file mode 100644 index 0000000000..b2b74a5e52 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/BasicEventProvider.java @@ -0,0 +1,179 @@ +/* + * 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.components.calendar.event; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.vaadin.ui.components.calendar.event.CalendarEvent.EventChangeEvent; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier; + +/** + * <p> + * Simple implementation of + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider}. Use {@link #addEvent(CalendarEvent)} and + * {@link #removeEvent(CalendarEvent)} to add / remove events. + * </p> + * + * <p> + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider.EventSetChangeNotifier + * EventSetChangeNotifier} and + * {@link com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener + * EventChangeListener} are also implemented, so the Calendar is notified when + * an event is added, changed or removed. + * </p> + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventProvider implements CalendarEditableEventProvider, + EventSetChangeNotifier, CalendarEvent.EventChangeListener { + + protected List<CalendarEvent> eventList = new ArrayList<CalendarEvent>(); + + private List<EventSetChangeListener> listeners = new ArrayList<EventSetChangeListener>(); + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java. + * util.Date, java.util.Date) + */ + @Override + public List<CalendarEvent> getEvents(Date startDate, Date endDate) { + ArrayList<CalendarEvent> activeEvents = new ArrayList<CalendarEvent>(); + + for (CalendarEvent ev : eventList) { + long from = startDate.getTime(); + long to = endDate.getTime(); + + if (ev.getStart() != null && ev.getEnd() != null) { + long f = ev.getStart().getTime(); + long t = ev.getEnd().getTime(); + // Select only events that overlaps with startDate and + // endDate. + if ((f <= to && f >= from) || (t >= from && t <= to) + || (f <= from && t >= to)) { + activeEvents.add(ev); + } + } + } + + return activeEvents; + } + + /** + * Does this event provider container this event + * + * @param event + * The event to check for + * @return If this provider has the event then true is returned, else false + */ + public boolean containsEvent(BasicEvent event) { + return eventList.contains(event); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChangeNotifier + * #addListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChangeListener + * ) + */ + @Override + public void addEventSetChangeListener(EventSetChangeListener listener) { + listeners.add(listener); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChangeNotifier + * #removeListener + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChangeListener + * ) + */ + @Override + public void removeEventSetChangeListener(EventSetChangeListener listener) { + listeners.remove(listener); + } + + /** + * Fires a eventsetchange event. The event is fired when either an event is + * added or removed to the event provider + */ + protected void fireEventSetChange() { + EventSetChangeEvent event = new EventSetChangeEvent(this); + + for (EventSetChangeListener listener : listeners) { + listener.eventSetChange(event); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener + * #eventChange + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChange) + */ + @Override + public void eventChange(EventChangeEvent changeEvent) { + // naive implementation + fireEventSetChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void addEvent(CalendarEvent event) { + eventList.add(event); + if (event instanceof BasicEvent) { + ((BasicEvent) event).addEventChangeListener(this); + } + fireEventSetChange(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent + * (com.vaadin.addon.calendar.event.CalendarEvent) + */ + @Override + public void removeEvent(CalendarEvent event) { + eventList.remove(event); + if (event instanceof BasicEvent) { + ((BasicEvent) event).removeEventChangeListener(this); + } + fireEventSetChange(); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java b/server/src/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java new file mode 100644 index 0000000000..13e84df666 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/CalendarEditableEventProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.calendar.event; + +/** + * An event provider which allows adding and removing events + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarEditableEventProvider extends CalendarEventProvider { + + /** + * Adds an event to the event provider + * + * @param event + * The event to add + */ + void addEvent(CalendarEvent event); + + /** + * Removes an event from the event provider + * + * @param event + * The event + */ + void removeEvent(CalendarEvent event); +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/CalendarEvent.java b/server/src/com/vaadin/ui/components/calendar/event/CalendarEvent.java new file mode 100644 index 0000000000..531ee72c7f --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/CalendarEvent.java @@ -0,0 +1,146 @@ +/* + * 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.components.calendar.event; + +import java.io.Serializable; +import java.util.Date; + +/** + * <p> + * Event in the calendar. Customize your own event by implementing this + * interface. + * </p> + * + * <li>Start and end fields are mandatory.</li> + * + * <li>In "allDay" events longer than one day, starting and ending clock times + * are omitted in UI and only dates are shown.</li> + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +public interface CalendarEvent extends Serializable { + + /** + * Gets start date of event. + * + * @return Start date. + */ + public Date getStart(); + + /** + * Get end date of event. + * + * @return End date; + */ + public Date getEnd(); + + /** + * Gets caption of event. + * + * @return Caption text + */ + public String getCaption(); + + /** + * Gets description of event. Shown as a tooltip over the event. + * + * @return Description text. + */ + public String getDescription(); + + /** + * <p> + * Gets style name of event. In the client, style name will be set to the + * event's element class name and can be styled by CSS + * </p> + * Styling example:</br> <code>Java code: </br> + * event.setStyleName("color1"); + * </br></br> + * CSS:</br> + * .v-calendar-event-color1 {</br> + * background-color: #9effae;</br>}</code> + * + * @return Style name. + */ + public String getStyleName(); + + /** + * An all-day event typically does not occur at a specific time but targets + * a whole day or days. The rendering of all-day events differs from normal + * events. + * + * @return true if this event is an all-day event, false otherwise + */ + public boolean isAllDay(); + + /** + * Event to signal that an event has changed. + */ + @SuppressWarnings("serial") + public class EventChangeEvent implements Serializable { + + private CalendarEvent source; + + public EventChangeEvent(CalendarEvent source) { + this.source = source; + } + + /** + * @return the {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent} that has changed + */ + public CalendarEvent getCalendarEvent() { + return source; + } + } + + /** + * Listener for EventSetChange events. + */ + public interface EventChangeListener extends Serializable { + + /** + * Called when an Event has changed. + */ + public void eventChange(EventChangeEvent eventChangeEvent); + } + + /** + * Notifier interface for EventChange events. + */ + public interface EventChangeNotifier extends Serializable { + + /** + * Add a listener to listen for EventChangeEvents. These events are + * fired when a events properties are changed. + * + * @param listener + * The listener to add + */ + void addEventChangeListener(EventChangeListener listener); + + /** + * Remove a listener from the event provider. + * + * @param listener + * The listener to remove + */ + void removeEventChangeListener(EventChangeListener listener); + } + +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java b/server/src/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java new file mode 100644 index 0000000000..fefb2ca9b6 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/CalendarEventProvider.java @@ -0,0 +1,112 @@ +/* + * 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.components.calendar.event; + +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +/** + * Interface for querying events. The Vaadin Calendar always has a + * CalendarEventProvider set. + * + * @since 7.1.0 + * @author Vaadin Ltd. + */ +public interface CalendarEventProvider extends Serializable { + /** + * <p> + * Gets all available events in the target date range between startDate and + * endDate. The Vaadin Calendar queries the events from the range that is + * shown, which is not guaranteed to be the same as the date range that is + * set. + * </p> + * + * <p> + * For example, if you set the date range to be monday 22.2.2010 - wednesday + * 24.2.2000, the used Event Provider will be queried for events between + * monday 22.2.2010 00:00 and sunday 28.2.2010 23:59. Generally you can + * expect the date range to be expanded to whole days and whole weeks. + * </p> + * + * @param startDate + * Start date + * @param endDate + * End date + * @return List of events + */ + public List<CalendarEvent> getEvents(Date startDate, Date endDate); + + /** + * Event to signal that the set of events has changed and the calendar + * should refresh its view from the + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} . + * + */ + @SuppressWarnings("serial") + public class EventSetChangeEvent implements Serializable { + + private CalendarEventProvider source; + + public EventSetChangeEvent(CalendarEventProvider source) { + this.source = source; + } + + /** + * @return the + * {@link com.vaadin.addon.calendar.event.CalendarEventProvider + * CalendarEventProvider} that has changed + */ + public CalendarEventProvider getProvider() { + return source; + } + } + + /** + * Listener for EventSetChange events. + */ + public interface EventSetChangeListener extends Serializable { + + /** + * Called when the set of Events has changed. + */ + public void eventSetChange(EventSetChangeEvent changeEvent); + } + + /** + * Notifier interface for EventSetChange events. + */ + public interface EventSetChangeNotifier extends Serializable { + + /** + * Add a listener for listening to when new events are adding or removed + * from the event provider. + * + * @param listener + * The listener to add + */ + void addEventSetChangeListener(EventSetChangeListener listener); + + /** + * Remove a listener which listens to {@link EventSetChangeEvent}-events + * + * @param listener + * The listener to remove + */ + void removeEventSetChangeListener(EventSetChangeListener listener); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java b/server/src/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java new file mode 100644 index 0000000000..e8a27ad50f --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/event/EditableCalendarEvent.java @@ -0,0 +1,91 @@ +/* + * 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.components.calendar.event; + +import java.util.Date; + +/** + * <p> + * Extension to the basic {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent}. This interface provides setters (and thus editing + * capabilities) for all {@link com.vaadin.addon.calendar.event.CalendarEvent + * CalendarEvent} fields. For descriptions on the fields, refer to the extended + * interface. + * </p> + * + * <p> + * This interface is used by some of the basic Calendar event handlers in the + * <code>com.vaadin.addon.calendar.ui.handler</code> package to determine + * whether an event can be edited. + * </p> + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public interface EditableCalendarEvent extends CalendarEvent { + + /** + * Set the visible text in the calendar for the event. + * + * @param caption + * The text to show in the calendar + */ + void setCaption(String caption); + + /** + * Set the description of the event. This is shown in the calendar when + * hoovering over the event. + * + * @param description + * The text which describes the event + */ + void setDescription(String description); + + /** + * Set the end date of the event. Must be after the start date. + * + * @param end + * The end date to set + */ + void setEnd(Date end); + + /** + * Set the start date for the event. Must be before the end date + * + * @param start + * The start date of the event + */ + void setStart(Date start); + + /** + * Set the style name for the event used for styling the event cells + * + * @param styleName + * The stylename to use + * + */ + void setStyleName(String styleName); + + /** + * Does the event span the whole day. If so then set this to true + * + * @param isAllDay + * True if the event spans the whole day. In this case the start + * and end times are ignored. + */ + void setAllDay(boolean isAllDay); + +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java new file mode 100644 index 0000000000..65e9c94dec --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicBackwardHandler.java @@ -0,0 +1,79 @@ +/* + * 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.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.BackwardHandler; + +/** + * Implements basic functionality needed to enable backwards navigation. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicBackwardHandler implements BackwardHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler# + * backward + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardEvent) + */ + @Override + public void backward(BackwardEvent event) { + Date start = event.getComponent().getStartDate(); + Date end = event.getComponent().getEndDate(); + + // calculate amount to move back + int durationInDays = (int) (((end.getTime()) - start.getTime()) / DateConstants.DAYINMILLIS); + durationInDays++; + durationInDays = -durationInDays; + + // set new start and end times + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.setTime(start); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newStart = javaCalendar.getTime(); + + javaCalendar.setTime(end); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newEnd = javaCalendar.getTime(); + + setDates(event, newStart, newEnd); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(BackwardEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java new file mode 100644 index 0000000000..ac2470e008 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicDateClickHandler.java @@ -0,0 +1,70 @@ +/* + * 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.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.DateClickHandler; + +/** + * Implements basic functionality needed to switch to day view when a single day + * is clicked. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicDateClickHandler implements DateClickHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler + * #dateClick + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickEvent) + */ + @Override + public void dateClick(DateClickEvent event) { + Date clickedDate = event.getDate(); + + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.setTime(clickedDate); + + // as times are expanded, this is all that is needed to show one day + Date start = javaCalendar.getTime(); + Date end = javaCalendar.getTime(); + + setDates(event, start, end); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(DateClickEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java new file mode 100644 index 0000000000..ae4c5fcc12 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicEventMoveHandler.java @@ -0,0 +1,74 @@ +/* + * 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.components.calendar.handler; + +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventMoveHandler; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.MoveEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.EditableCalendarEvent; + +/** + * Implements basic functionality needed to enable moving events. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventMoveHandler implements EventMoveHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler + * #eventMove + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent) + */ + @Override + public void eventMove(MoveEvent event) { + CalendarEvent calendarEvent = event.getCalendarEvent(); + + if (calendarEvent instanceof EditableCalendarEvent) { + + EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent; + + Date newFromTime = event.getNewStart(); + + // Update event dates + long length = editableEvent.getEnd().getTime() + - editableEvent.getStart().getTime(); + setDates(editableEvent, newFromTime, new Date(newFromTime.getTime() + + length)); + } + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(EditableCalendarEvent event, Date start, Date end) { + event.setStart(start); + event.setEnd(end); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java new file mode 100644 index 0000000000..ee7fc27360 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicEventResizeHandler.java @@ -0,0 +1,70 @@ +/* + * 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.components.calendar.handler; + +import java.util.Date; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResize; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.EventResizeHandler; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.EditableCalendarEvent; + +/** + * Implements basic functionality needed to enable event resizing. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicEventResizeHandler implements EventResizeHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler + * #eventResize + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize) + */ + @Override + public void eventResize(EventResize event) { + CalendarEvent calendarEvent = event.getCalendarEvent(); + + if (calendarEvent instanceof EditableCalendarEvent) { + Date newStartTime = event.getNewStart(); + Date newEndTime = event.getNewEnd(); + + EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent; + + setDates(editableEvent, newStartTime, newEndTime); + } + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(EditableCalendarEvent event, Date start, Date end) { + event.setStart(start); + event.setEnd(end); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java new file mode 100644 index 0000000000..e36c9e5756 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicForwardHandler.java @@ -0,0 +1,77 @@ +/* + * 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.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; + +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardEvent; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.ForwardHandler; + +/** + * Implements basic functionality needed to enable forward navigation. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicForwardHandler implements ForwardHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler#forward + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardEvent) + */ + @Override + public void forward(ForwardEvent event) { + Date start = event.getComponent().getStartDate(); + Date end = event.getComponent().getEndDate(); + + // calculate amount to move forward + int durationInDays = (int) (((end.getTime()) - start.getTime()) / DateConstants.DAYINMILLIS); + durationInDays++; + + // set new start and end times + Calendar javaCalendar = Calendar.getInstance(); + javaCalendar.setTime(start); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newStart = javaCalendar.getTime(); + + javaCalendar.setTime(end); + javaCalendar.add(java.util.Calendar.DATE, durationInDays); + Date newEnd = javaCalendar.getTime(); + + setDates(event, newStart, newEnd); + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(ForwardEvent event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } +} diff --git a/server/src/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java b/server/src/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java new file mode 100644 index 0000000000..846fd7dd53 --- /dev/null +++ b/server/src/com/vaadin/ui/components/calendar/handler/BasicWeekClickHandler.java @@ -0,0 +1,82 @@ +/* + * 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.components.calendar.handler; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClick; +import com.vaadin.ui.components.calendar.CalendarComponentEvents.WeekClickHandler; + +/** + * Implements basic functionality needed to change to week view when a week + * number is clicked. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class BasicWeekClickHandler implements WeekClickHandler { + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler + * #weekClick + * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClick) + */ + @Override + public void weekClick(WeekClick event) { + int week = event.getWeek(); + int year = event.getYear(); + + // set correct year and month + Calendar javaCalendar = event.getComponent().getInternalCalendar(); + javaCalendar.set(GregorianCalendar.YEAR, year); + javaCalendar.set(GregorianCalendar.WEEK_OF_YEAR, week); + + // starting at the beginning of the week + javaCalendar.set(GregorianCalendar.DAY_OF_WEEK, + javaCalendar.getFirstDayOfWeek()); + Date start = javaCalendar.getTime(); + + // ending at the end of the week + javaCalendar.add(GregorianCalendar.DATE, 6); + Date end = javaCalendar.getTime(); + + setDates(event, start, end); + + // times are automatically expanded, no need to worry about them + } + + /** + * Set the start and end dates for the event + * + * @param event + * The event that the start and end dates should be set + * @param start + * The start date + * @param end + * The end date + */ + protected void setDates(WeekClick event, Date start, Date end) { + event.getComponent().setStartDate(start); + event.getComponent().setEndDate(end); + } + +} diff --git a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java index 9e9855afdd..9123245033 100644 --- a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java +++ b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerGrid.java @@ -187,6 +187,7 @@ public class ColorPickerGrid extends AbstractComponent implements ColorSelector * @param listener * The color change listener */ + @Override public void addColorChangeListener(ColorChangeListener listener) { addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); } @@ -202,6 +203,7 @@ public class ColorPickerGrid extends AbstractComponent implements ColorSelector * @param listener * The listener */ + @Override public void removeColorChangeListener(ColorChangeListener listener) { removeListener(ColorChangeEvent.class, listener); } diff --git a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java index e6edbcf40e..2902585f56 100644 --- a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java +++ b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerHistory.java @@ -194,6 +194,7 @@ public class ColorPickerHistory extends CustomComponent implements * @param listener * The listener */ + @Override public void addColorChangeListener(ColorChangeListener listener) { addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD); } @@ -204,6 +205,7 @@ public class ColorPickerHistory extends CustomComponent implements * @param listener * The listener */ + @Override public void removeColorChangeListener(ColorChangeListener listener) { removeListener(ColorChangeEvent.class, listener); } diff --git a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java index c06ae9f6ff..fee52d1a24 100644 --- a/server/src/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java +++ b/server/src/com/vaadin/ui/components/colorpicker/ColorPickerPopup.java @@ -283,6 +283,7 @@ public class ColorPickerPopup extends Window implements ClickListener, } redSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { double red = (Double) event.getProperty().getValue(); if (!updatingColors) { @@ -303,6 +304,7 @@ public class ColorPickerPopup extends Window implements ClickListener, } greenSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { double green = (Double) event.getProperty().getValue(); if (!updatingColors) { @@ -322,6 +324,7 @@ public class ColorPickerPopup extends Window implements ClickListener, } blueSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { double blue = (Double) event.getProperty().getValue(); if (!updatingColors) { @@ -380,6 +383,7 @@ public class ColorPickerPopup extends Window implements ClickListener, hueSlider.setWidth("220px"); hueSlider.setImmediate(true); hueSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { if (!updatingColors) { float hue = (Float.parseFloat(event.getProperty() @@ -417,6 +421,7 @@ public class ColorPickerPopup extends Window implements ClickListener, saturationSlider.setWidth("220px"); saturationSlider.setImmediate(true); saturationSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { if (!updatingColors) { float hue = (Float.parseFloat(hueSlider.getValue() @@ -444,6 +449,7 @@ public class ColorPickerPopup extends Window implements ClickListener, valueSlider.setWidth("220px"); valueSlider.setImmediate(true); valueSlider.addValueChangeListener(new ValueChangeListener() { + @Override public void valueChange(ValueChangeEvent event) { if (!updatingColors) { float hue = (Float.parseFloat(hueSlider.getValue() @@ -754,6 +760,7 @@ public class ColorPickerPopup extends Window implements ClickListener, /** HSV color converter */ Coordinates2Color HSVConverter = new Coordinates2Color() { + @Override public int[] calculate(Color color) { float[] hsv = color.getHSV(); @@ -769,6 +776,7 @@ public class ColorPickerPopup extends Window implements ClickListener, return new int[] { x, y }; } + @Override public Color calculate(int x, int y) { float saturation = 1f - (y / 220.0f); float value = (x / 220.0f); diff --git a/server/src/com/vaadin/util/CurrentInstance.java b/server/src/com/vaadin/util/CurrentInstance.java index 595c162e7e..60489d596e 100644 --- a/server/src/com/vaadin/util/CurrentInstance.java +++ b/server/src/com/vaadin/util/CurrentInstance.java @@ -21,8 +21,28 @@ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; +import com.vaadin.server.VaadinPortlet; +import com.vaadin.server.VaadinPortletService; +import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinServlet; +import com.vaadin.server.VaadinServletService; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.UI; + /** * Keeps track of various thread local instances used by the framework. + * <p> + * Currently the framework uses the following instances: + * </p> + * <p> + * Inheritable: {@link UI}, {@link VaadinPortlet}, {@link VaadinService}, + * {@link VaadinServlet}, {@link VaadinSession}. + * </p> + * <p> + * Non-inheritable: {@link VaadinRequest}, {@link VaadinResponse}. + * </p> * * @author Vaadin Ltd * @version @VERSION@ @@ -32,6 +52,18 @@ public class CurrentInstance implements Serializable { private final Object instance; private final boolean inheritable; + private static boolean portletAvailable = false; + { + try { + /* + * VaadinPortlet depends on portlet API which is available only if + * running in a portal. + */ + portletAvailable = (VaadinPortlet.class.getName() != null); + } catch (Throwable t) { + } + } + private static InheritableThreadLocal<Map<Class<?>, CurrentInstance>> instances = new InheritableThreadLocal<Map<Class<?>, CurrentInstance>>() { @Override protected Map<Class<?>, CurrentInstance> childValue( @@ -135,7 +167,11 @@ public class CurrentInstance implements Serializable { new CurrentInstance(instance, inheritable)); if (previousInstance != null) { assert previousInstance.inheritable == inheritable : "Inheritable status mismatch for " - + type; + + type + + " (previous was " + + previousInstance.inheritable + + ", new is " + + inheritable + ")"; } } } @@ -146,4 +182,72 @@ public class CurrentInstance implements Serializable { public static void clearAll() { instances.remove(); } + + /** + * Restores the given thread locals to the given values. Note that this + * should only be used internally to restore Vaadin classes. + * + * @param old + * A Class -> Object map to set as thread locals + */ + public static void restoreThreadLocals(Map<Class<?>, CurrentInstance> old) { + for (Class c : old.keySet()) { + CurrentInstance ci = old.get(c); + set(c, ci.instance, ci.inheritable); + } + } + + /** + * Sets thread locals for the UI and all related classes + * + * @param ui + * The UI + * @return A map containing the old values of the thread locals this method + * updated. + */ + public static Map<Class<?>, CurrentInstance> setThreadLocals(UI ui) { + Map<Class<?>, CurrentInstance> old = new HashMap<Class<?>, CurrentInstance>(); + old.put(UI.class, new CurrentInstance(UI.getCurrent(), true)); + UI.setCurrent(ui); + old.putAll(setThreadLocals(ui.getSession())); + return old; + } + + /** + * Sets thread locals for the {@link VaadinSession} and all related classes + * + * @param session + * The VaadinSession + * @return A map containing the old values of the thread locals this method + * updated. + */ + public static Map<Class<?>, CurrentInstance> setThreadLocals( + VaadinSession session) { + Map<Class<?>, CurrentInstance> old = new HashMap<Class<?>, CurrentInstance>(); + old.put(VaadinSession.class, + new CurrentInstance(VaadinSession.getCurrent(), true)); + old.put(VaadinService.class, + new CurrentInstance(VaadinService.getCurrent(), true)); + VaadinService service = null; + if (session != null) { + service = session.getService(); + } + + VaadinSession.setCurrent(session); + VaadinService.setCurrent(service); + + if (service instanceof VaadinServletService) { + old.put(VaadinServlet.class, + new CurrentInstance(VaadinServlet.getCurrent(), true)); + VaadinServlet.setCurrent(((VaadinServletService) service) + .getServlet()); + } else if (portletAvailable && service instanceof VaadinPortletService) { + old.put(VaadinPortlet.class, + new CurrentInstance(VaadinPortlet.getCurrent(), true)); + VaadinPortlet.setCurrent(((VaadinPortletService) service) + .getPortlet()); + } + + return old; + } } |