summaryrefslogtreecommitdiffstats
path: root/server/src
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2012-08-13 18:34:33 +0300
committerArtur Signell <artur@vaadin.com>2012-08-13 19:18:33 +0300
commite85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch)
tree9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src
parent14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff)
downloadvaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz
vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src')
-rw-r--r--server/src/com/vaadin/Application.java2426
-rw-r--r--server/src/com/vaadin/RootRequiresMoreInformationException.java25
-rw-r--r--server/src/com/vaadin/Vaadin.gwt.xml85
-rw-r--r--server/src/com/vaadin/Version.java74
-rw-r--r--server/src/com/vaadin/annotations/AutoGenerated.java18
-rw-r--r--server/src/com/vaadin/annotations/EagerInit.java30
-rw-r--r--server/src/com/vaadin/annotations/JavaScript.java41
-rw-r--r--server/src/com/vaadin/annotations/StyleSheet.java38
-rw-r--r--server/src/com/vaadin/annotations/Theme.java24
-rw-r--r--server/src/com/vaadin/annotations/Widgetset.java25
-rw-r--r--server/src/com/vaadin/annotations/package.html12
-rw-r--r--server/src/com/vaadin/data/Buffered.java280
-rw-r--r--server/src/com/vaadin/data/BufferedValidatable.java35
-rw-r--r--server/src/com/vaadin/data/Collapsible.java68
-rw-r--r--server/src/com/vaadin/data/Container.java1105
-rw-r--r--server/src/com/vaadin/data/Item.java180
-rw-r--r--server/src/com/vaadin/data/Property.java402
-rw-r--r--server/src/com/vaadin/data/Validatable.java110
-rw-r--r--server/src/com/vaadin/data/Validator.java175
-rw-r--r--server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java157
-rw-r--r--server/src/com/vaadin/data/fieldgroup/Caption.java15
-rw-r--r--server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java157
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroup.java978
-rw-r--r--server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java31
-rw-r--r--server/src/com/vaadin/data/fieldgroup/PropertyId.java15
-rw-r--r--server/src/com/vaadin/data/package.html49
-rw-r--r--server/src/com/vaadin/data/util/AbstractBeanContainer.java856
-rw-r--r--server/src/com/vaadin/data/util/AbstractContainer.java251
-rw-r--r--server/src/com/vaadin/data/util/AbstractInMemoryContainer.java941
-rw-r--r--server/src/com/vaadin/data/util/AbstractProperty.java226
-rw-r--r--server/src/com/vaadin/data/util/BeanContainer.java168
-rw-r--r--server/src/com/vaadin/data/util/BeanItem.java269
-rw-r--r--server/src/com/vaadin/data/util/BeanItemContainer.java241
-rw-r--r--server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java792
-rw-r--r--server/src/com/vaadin/data/util/ContainerOrderedWrapper.java644
-rw-r--r--server/src/com/vaadin/data/util/DefaultItemSorter.java210
-rw-r--r--server/src/com/vaadin/data/util/FilesystemContainer.java918
-rw-r--r--server/src/com/vaadin/data/util/HierarchicalContainer.java814
-rw-r--r--server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java70
-rw-r--r--server/src/com/vaadin/data/util/IndexedContainer.java1109
-rw-r--r--server/src/com/vaadin/data/util/ItemSorter.java57
-rw-r--r--server/src/com/vaadin/data/util/ListSet.java264
-rw-r--r--server/src/com/vaadin/data/util/MethodProperty.java784
-rw-r--r--server/src/com/vaadin/data/util/MethodPropertyDescriptor.java134
-rw-r--r--server/src/com/vaadin/data/util/NestedMethodProperty.java257
-rw-r--r--server/src/com/vaadin/data/util/NestedPropertyDescriptor.java60
-rw-r--r--server/src/com/vaadin/data/util/ObjectProperty.java141
-rw-r--r--server/src/com/vaadin/data/util/PropertyFormatter.java245
-rw-r--r--server/src/com/vaadin/data/util/PropertysetItem.java340
-rw-r--r--server/src/com/vaadin/data/util/QueryContainer.java675
-rw-r--r--server/src/com/vaadin/data/util/TextFileProperty.java144
-rw-r--r--server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java114
-rw-r--r--server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java43
-rw-r--r--server/src/com/vaadin/data/util/converter/Converter.java159
-rw-r--r--server/src/com/vaadin/data/util/converter/ConverterFactory.java23
-rw-r--r--server/src/com/vaadin/data/util/converter/ConverterUtil.java168
-rw-r--r--server/src/com/vaadin/data/util/converter/DateToLongConverter.java72
-rw-r--r--server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java101
-rw-r--r--server/src/com/vaadin/data/util/converter/ReverseConverter.java84
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java108
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToDateConverter.java112
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java107
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java88
-rw-r--r--server/src/com/vaadin/data/util/converter/StringToNumberConverter.java111
-rw-r--r--server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java76
-rw-r--r--server/src/com/vaadin/data/util/filter/And.java44
-rw-r--r--server/src/com/vaadin/data/util/filter/Between.java74
-rw-r--r--server/src/com/vaadin/data/util/filter/Compare.java327
-rw-r--r--server/src/com/vaadin/data/util/filter/IsNull.java79
-rw-r--r--server/src/com/vaadin/data/util/filter/Like.java83
-rw-r--r--server/src/com/vaadin/data/util/filter/Not.java70
-rw-r--r--server/src/com/vaadin/data/util/filter/Or.java63
-rw-r--r--server/src/com/vaadin/data/util/filter/SimpleStringFilter.java152
-rw-r--r--server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java35
-rw-r--r--server/src/com/vaadin/data/util/package.html18
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java92
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java31
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java248
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java38
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java31
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/Reference.java56
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/RowId.java81
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/RowItem.java133
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java1716
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java36
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java32
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java72
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java41
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java168
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java507
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java118
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java57
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java46
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java211
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java715
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java367
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java101
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java112
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java88
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java163
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java23
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java25
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java38
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java16
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java22
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java30
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java29
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java23
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java98
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java30
-rw-r--r--server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java58
-rw-r--r--server/src/com/vaadin/data/validator/AbstractStringValidator.java42
-rw-r--r--server/src/com/vaadin/data/validator/AbstractValidator.java139
-rw-r--r--server/src/com/vaadin/data/validator/BeanValidator.java176
-rw-r--r--server/src/com/vaadin/data/validator/CompositeValidator.java259
-rw-r--r--server/src/com/vaadin/data/validator/DateRangeValidator.java51
-rw-r--r--server/src/com/vaadin/data/validator/DoubleRangeValidator.java37
-rw-r--r--server/src/com/vaadin/data/validator/DoubleValidator.java58
-rw-r--r--server/src/com/vaadin/data/validator/EmailValidator.java35
-rw-r--r--server/src/com/vaadin/data/validator/IntegerRangeValidator.java37
-rw-r--r--server/src/com/vaadin/data/validator/IntegerValidator.java58
-rw-r--r--server/src/com/vaadin/data/validator/NullValidator.java92
-rw-r--r--server/src/com/vaadin/data/validator/RangeValidator.java186
-rw-r--r--server/src/com/vaadin/data/validator/RegexpValidator.java97
-rw-r--r--server/src/com/vaadin/data/validator/StringLengthValidator.java139
-rw-r--r--server/src/com/vaadin/data/validator/package.html23
-rw-r--r--server/src/com/vaadin/event/Action.java195
-rw-r--r--server/src/com/vaadin/event/ActionManager.java249
-rw-r--r--server/src/com/vaadin/event/ComponentEventListener.java11
-rw-r--r--server/src/com/vaadin/event/DataBoundTransferable.java66
-rw-r--r--server/src/com/vaadin/event/EventRouter.java201
-rw-r--r--server/src/com/vaadin/event/FieldEvents.java275
-rw-r--r--server/src/com/vaadin/event/ItemClickEvent.java121
-rw-r--r--server/src/com/vaadin/event/LayoutEvents.java138
-rw-r--r--server/src/com/vaadin/event/ListenerMethod.java663
-rw-r--r--server/src/com/vaadin/event/MethodEventSource.java157
-rw-r--r--server/src/com/vaadin/event/MouseEvents.java234
-rw-r--r--server/src/com/vaadin/event/ShortcutAction.java373
-rw-r--r--server/src/com/vaadin/event/ShortcutListener.java33
-rw-r--r--server/src/com/vaadin/event/Transferable.java57
-rw-r--r--server/src/com/vaadin/event/TransferableImpl.java47
-rw-r--r--server/src/com/vaadin/event/dd/DragAndDropEvent.java50
-rw-r--r--server/src/com/vaadin/event/dd/DragSource.java52
-rw-r--r--server/src/com/vaadin/event/dd/DropHandler.java61
-rw-r--r--server/src/com/vaadin/event/dd/DropTarget.java42
-rw-r--r--server/src/com/vaadin/event/dd/TargetDetails.java37
-rw-r--r--server/src/com/vaadin/event/dd/TargetDetailsImpl.java46
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java36
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java75
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/And.java54
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java61
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java53
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/Not.java39
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/Or.java52
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java57
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java67
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java51
-rw-r--r--server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java72
-rw-r--r--server/src/com/vaadin/event/package.html58
-rw-r--r--server/src/com/vaadin/external/json/JSONArray.java963
-rw-r--r--server/src/com/vaadin/external/json/JSONException.java32
-rw-r--r--server/src/com/vaadin/external/json/JSONObject.java1693
-rw-r--r--server/src/com/vaadin/external/json/JSONString.java21
-rw-r--r--server/src/com/vaadin/external/json/JSONStringer.java84
-rw-r--r--server/src/com/vaadin/external/json/JSONTokener.java451
-rw-r--r--server/src/com/vaadin/external/json/JSONWriter.java355
-rw-r--r--server/src/com/vaadin/external/json/README68
-rw-r--r--server/src/com/vaadin/navigator/FragmentManager.java38
-rw-r--r--server/src/com/vaadin/navigator/Navigator.java656
-rw-r--r--server/src/com/vaadin/navigator/View.java36
-rw-r--r--server/src/com/vaadin/navigator/ViewChangeListener.java118
-rw-r--r--server/src/com/vaadin/navigator/ViewDisplay.java29
-rw-r--r--server/src/com/vaadin/navigator/ViewProvider.java44
-rw-r--r--server/src/com/vaadin/package.html27
-rw-r--r--server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml6
-rw-r--r--server/src/com/vaadin/service/ApplicationContext.java165
-rw-r--r--server/src/com/vaadin/service/FileTypeResolver.java385
-rw-r--r--server/src/com/vaadin/service/package.html20
-rw-r--r--server/src/com/vaadin/terminal/AbstractClientConnector.java510
-rw-r--r--server/src/com/vaadin/terminal/AbstractErrorMessage.java176
-rw-r--r--server/src/com/vaadin/terminal/AbstractExtension.java76
-rw-r--r--server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java162
-rw-r--r--server/src/com/vaadin/terminal/ApplicationResource.java75
-rw-r--r--server/src/com/vaadin/terminal/ClassResource.java178
-rw-r--r--server/src/com/vaadin/terminal/CombinedRequest.java187
-rw-r--r--server/src/com/vaadin/terminal/CompositeErrorMessage.java112
-rw-r--r--server/src/com/vaadin/terminal/DeploymentConfiguration.java123
-rw-r--r--server/src/com/vaadin/terminal/DownloadStream.java335
-rw-r--r--server/src/com/vaadin/terminal/ErrorMessage.java126
-rw-r--r--server/src/com/vaadin/terminal/Extension.java27
-rw-r--r--server/src/com/vaadin/terminal/ExternalResource.java118
-rw-r--r--server/src/com/vaadin/terminal/FileResource.java174
-rw-r--r--server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java116
-rw-r--r--server/src/com/vaadin/terminal/KeyMapper.java86
-rw-r--r--server/src/com/vaadin/terminal/LegacyPaint.java85
-rw-r--r--server/src/com/vaadin/terminal/Page.java646
-rw-r--r--server/src/com/vaadin/terminal/PaintException.java54
-rw-r--r--server/src/com/vaadin/terminal/PaintTarget.java509
-rw-r--r--server/src/com/vaadin/terminal/RequestHandler.java36
-rw-r--r--server/src/com/vaadin/terminal/Resource.java26
-rw-r--r--server/src/com/vaadin/terminal/Scrollable.java80
-rw-r--r--server/src/com/vaadin/terminal/Sizeable.java242
-rw-r--r--server/src/com/vaadin/terminal/StreamResource.java222
-rw-r--r--server/src/com/vaadin/terminal/StreamVariable.java157
-rw-r--r--server/src/com/vaadin/terminal/SystemError.java82
-rw-r--r--server/src/com/vaadin/terminal/Terminal.java80
-rw-r--r--server/src/com/vaadin/terminal/ThemeResource.java96
-rw-r--r--server/src/com/vaadin/terminal/UserError.java70
-rw-r--r--server/src/com/vaadin/terminal/Vaadin6Component.java44
-rw-r--r--server/src/com/vaadin/terminal/VariableOwner.java85
-rw-r--r--server/src/com/vaadin/terminal/WrappedRequest.java277
-rw-r--r--server/src/com/vaadin/terminal/WrappedResponse.java147
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java1079
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java1623
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java2790
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java143
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java46
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java268
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContext.java80
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java19
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java13
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java38
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java55
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java78
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java11
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java570
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java13
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java39
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java45
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java39
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ClientConnector.java149
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java71
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java122
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java664
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/Constants.java80
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java313
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java417
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java54
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/JsonCodec.java792
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java1022
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java38
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java398
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java170
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java56
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RequestTimer.java43
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ResourceReference.java67
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java172
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RpcManager.java48
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/RpcTarget.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java142
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java113
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java120
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java9
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java16
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java25
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java17
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java28
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java57
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java89
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/UploadException.java15
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java180
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WebBrowser.java462
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java118
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java75
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java217
-rw-r--r--server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java111
-rw-r--r--server/src/com/vaadin/terminal/package.html21
-rw-r--r--server/src/com/vaadin/tools/ReflectTools.java126
-rw-r--r--server/src/com/vaadin/tools/WidgetsetCompiler.java94
-rw-r--r--server/src/com/vaadin/ui/AbsoluteLayout.java632
-rw-r--r--server/src/com/vaadin/ui/AbstractComponent.java1382
-rw-r--r--server/src/com/vaadin/ui/AbstractComponentContainer.java351
-rw-r--r--server/src/com/vaadin/ui/AbstractField.java1657
-rw-r--r--server/src/com/vaadin/ui/AbstractJavaScriptComponent.java165
-rw-r--r--server/src/com/vaadin/ui/AbstractLayout.java77
-rw-r--r--server/src/com/vaadin/ui/AbstractMedia.java196
-rw-r--r--server/src/com/vaadin/ui/AbstractOrderedLayout.java383
-rw-r--r--server/src/com/vaadin/ui/AbstractSelect.java2029
-rw-r--r--server/src/com/vaadin/ui/AbstractSplitPanel.java521
-rw-r--r--server/src/com/vaadin/ui/AbstractTextField.java674
-rw-r--r--server/src/com/vaadin/ui/Accordion.java19
-rw-r--r--server/src/com/vaadin/ui/Alignment.java158
-rw-r--r--server/src/com/vaadin/ui/Audio.java55
-rw-r--r--server/src/com/vaadin/ui/Button.java539
-rw-r--r--server/src/com/vaadin/ui/CheckBox.java141
-rw-r--r--server/src/com/vaadin/ui/ComboBox.java116
-rw-r--r--server/src/com/vaadin/ui/Component.java1047
-rw-r--r--server/src/com/vaadin/ui/ComponentContainer.java222
-rw-r--r--server/src/com/vaadin/ui/ConnectorTracker.java320
-rw-r--r--server/src/com/vaadin/ui/CssLayout.java308
-rw-r--r--server/src/com/vaadin/ui/CustomComponent.java189
-rw-r--r--server/src/com/vaadin/ui/CustomField.java237
-rw-r--r--server/src/com/vaadin/ui/CustomLayout.java329
-rw-r--r--server/src/com/vaadin/ui/DateField.java869
-rw-r--r--server/src/com/vaadin/ui/DefaultFieldFactory.java146
-rw-r--r--server/src/com/vaadin/ui/DragAndDropWrapper.java407
-rw-r--r--server/src/com/vaadin/ui/Embedded.java531
-rw-r--r--server/src/com/vaadin/ui/Field.java97
-rw-r--r--server/src/com/vaadin/ui/Form.java1420
-rw-r--r--server/src/com/vaadin/ui/FormFieldFactory.java41
-rw-r--r--server/src/com/vaadin/ui/FormLayout.java31
-rw-r--r--server/src/com/vaadin/ui/GridLayout.java1415
-rw-r--r--server/src/com/vaadin/ui/HasComponents.java49
-rw-r--r--server/src/com/vaadin/ui/HorizontalLayout.java24
-rw-r--r--server/src/com/vaadin/ui/HorizontalSplitPanel.java34
-rw-r--r--server/src/com/vaadin/ui/Html5File.java65
-rw-r--r--server/src/com/vaadin/ui/InlineDateField.java46
-rw-r--r--server/src/com/vaadin/ui/JavaScript.java157
-rw-r--r--server/src/com/vaadin/ui/JavaScriptFunction.java41
-rw-r--r--server/src/com/vaadin/ui/Label.java483
-rw-r--r--server/src/com/vaadin/ui/Layout.java229
-rw-r--r--server/src/com/vaadin/ui/Link.java242
-rw-r--r--server/src/com/vaadin/ui/ListSelect.java96
-rw-r--r--server/src/com/vaadin/ui/LoginForm.java353
-rw-r--r--server/src/com/vaadin/ui/MenuBar.java890
-rw-r--r--server/src/com/vaadin/ui/NativeButton.java21
-rw-r--r--server/src/com/vaadin/ui/NativeSelect.java91
-rw-r--r--server/src/com/vaadin/ui/Notification.java367
-rw-r--r--server/src/com/vaadin/ui/OptionGroup.java203
-rw-r--r--server/src/com/vaadin/ui/Panel.java486
-rw-r--r--server/src/com/vaadin/ui/PasswordField.java67
-rw-r--r--server/src/com/vaadin/ui/PopupDateField.java80
-rw-r--r--server/src/com/vaadin/ui/PopupView.java453
-rw-r--r--server/src/com/vaadin/ui/ProgressIndicator.java257
-rw-r--r--server/src/com/vaadin/ui/RichTextArea.java344
-rw-r--r--server/src/com/vaadin/ui/Root.java1227
-rw-r--r--server/src/com/vaadin/ui/Select.java803
-rw-r--r--server/src/com/vaadin/ui/Slider.java372
-rw-r--r--server/src/com/vaadin/ui/TabSheet.java1328
-rw-r--r--server/src/com/vaadin/ui/Table.java5449
-rw-r--r--server/src/com/vaadin/ui/TableFieldFactory.java45
-rw-r--r--server/src/com/vaadin/ui/TextArea.java121
-rw-r--r--server/src/com/vaadin/ui/TextField.java92
-rw-r--r--server/src/com/vaadin/ui/Tree.java1615
-rw-r--r--server/src/com/vaadin/ui/TreeTable.java824
-rw-r--r--server/src/com/vaadin/ui/TwinColSelect.java180
-rw-r--r--server/src/com/vaadin/ui/UniqueSerializable.java30
-rw-r--r--server/src/com/vaadin/ui/Upload.java1055
-rw-r--r--server/src/com/vaadin/ui/VerticalLayout.java25
-rw-r--r--server/src/com/vaadin/ui/VerticalSplitPanel.java30
-rw-r--r--server/src/com/vaadin/ui/Video.java81
-rw-r--r--server/src/com/vaadin/ui/Window.java853
-rw-r--r--server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gifbin0 -> 11077 bytes
-rw-r--r--server/src/com/vaadin/ui/doc-files/component_interfaces.gifbin0 -> 2272 bytes
-rw-r--r--server/src/com/vaadin/ui/package.html76
-rw-r--r--server/src/com/vaadin/ui/themes/BaseTheme.java59
-rw-r--r--server/src/com/vaadin/ui/themes/ChameleonTheme.java365
-rw-r--r--server/src/com/vaadin/ui/themes/LiferayTheme.java31
-rw-r--r--server/src/com/vaadin/ui/themes/Reindeer.java217
-rw-r--r--server/src/com/vaadin/ui/themes/Runo.java183
-rw-r--r--server/src/com/vaadin/util/SerializerHelper.java145
-rw-r--r--server/src/org/jsoup/Connection.java481
-rw-r--r--server/src/org/jsoup/Jsoup.java229
-rw-r--r--server/src/org/jsoup/examples/HtmlToPlainText.java109
-rw-r--r--server/src/org/jsoup/examples/ListLinks.java56
-rw-r--r--server/src/org/jsoup/examples/package-info.java4
-rw-r--r--server/src/org/jsoup/helper/DataUtil.java135
-rw-r--r--server/src/org/jsoup/helper/DescendableLinkedList.java82
-rw-r--r--server/src/org/jsoup/helper/HttpConnection.java658
-rw-r--r--server/src/org/jsoup/helper/StringUtil.java140
-rw-r--r--server/src/org/jsoup/helper/Validate.java112
-rw-r--r--server/src/org/jsoup/nodes/Attribute.java131
-rw-r--r--server/src/org/jsoup/nodes/Attributes.java249
-rw-r--r--server/src/org/jsoup/nodes/Comment.java46
-rw-r--r--server/src/org/jsoup/nodes/DataNode.java62
-rw-r--r--server/src/org/jsoup/nodes/Document.java350
-rw-r--r--server/src/org/jsoup/nodes/DocumentType.java46
-rw-r--r--server/src/org/jsoup/nodes/Element.java1119
-rw-r--r--server/src/org/jsoup/nodes/Entities.java184
-rw-r--r--server/src/org/jsoup/nodes/Node.java615
-rw-r--r--server/src/org/jsoup/nodes/TextNode.java175
-rw-r--r--server/src/org/jsoup/nodes/XmlDeclaration.java48
-rw-r--r--server/src/org/jsoup/nodes/entities-base.properties106
-rw-r--r--server/src/org/jsoup/nodes/entities-full.properties2032
-rw-r--r--server/src/org/jsoup/nodes/package-info.java4
-rw-r--r--server/src/org/jsoup/package-info.java4
-rw-r--r--server/src/org/jsoup/parser/CharacterReader.java230
-rw-r--r--server/src/org/jsoup/parser/HtmlTreeBuilder.java672
-rw-r--r--server/src/org/jsoup/parser/HtmlTreeBuilderState.java1482
-rw-r--r--server/src/org/jsoup/parser/ParseError.java40
-rw-r--r--server/src/org/jsoup/parser/ParseErrorList.java34
-rw-r--r--server/src/org/jsoup/parser/Parser.java157
-rw-r--r--server/src/org/jsoup/parser/Tag.java262
-rw-r--r--server/src/org/jsoup/parser/Token.java252
-rw-r--r--server/src/org/jsoup/parser/TokenQueue.java393
-rw-r--r--server/src/org/jsoup/parser/Tokeniser.java230
-rw-r--r--server/src/org/jsoup/parser/TokeniserState.java1778
-rw-r--r--server/src/org/jsoup/parser/TreeBuilder.java60
-rw-r--r--server/src/org/jsoup/parser/XmlTreeBuilder.java111
-rw-r--r--server/src/org/jsoup/parser/package-info.java4
-rw-r--r--server/src/org/jsoup/safety/Cleaner.java129
-rw-r--r--server/src/org/jsoup/safety/Whitelist.java451
-rw-r--r--server/src/org/jsoup/safety/package-info.java4
-rw-r--r--server/src/org/jsoup/select/Collector.java51
-rw-r--r--server/src/org/jsoup/select/CombiningEvaluator.java94
-rw-r--r--server/src/org/jsoup/select/Elements.java536
-rw-r--r--server/src/org/jsoup/select/Evaluator.java454
-rw-r--r--server/src/org/jsoup/select/NodeTraversor.java47
-rw-r--r--server/src/org/jsoup/select/NodeVisitor.java30
-rw-r--r--server/src/org/jsoup/select/QueryParser.java293
-rw-r--r--server/src/org/jsoup/select/Selector.java126
-rw-r--r--server/src/org/jsoup/select/StructuralEvaluator.java132
-rw-r--r--server/src/org/jsoup/select/package-info.java4
408 files changed, 107042 insertions, 0 deletions
diff --git a/server/src/com/vaadin/Application.java b/server/src/com/vaadin/Application.java
new file mode 100644
index 0000000000..1d31410185
--- /dev/null
+++ b/server/src/com/vaadin/Application.java
@@ -0,0 +1,2426 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.net.SocketException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.EventListener;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.vaadin.annotations.EagerInit;
+import com.vaadin.annotations.Theme;
+import com.vaadin.annotations.Widgetset;
+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.service.ApplicationContext;
+import com.vaadin.terminal.AbstractErrorMessage;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.CombinedRequest;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedRequest.BrowserDetails;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
+import com.vaadin.terminal.gwt.server.BootstrapFragmentResponse;
+import com.vaadin.terminal.gwt.server.BootstrapListener;
+import com.vaadin.terminal.gwt.server.BootstrapPageResponse;
+import com.vaadin.terminal.gwt.server.BootstrapResponse;
+import com.vaadin.terminal.gwt.server.ChangeVariablesErrorEvent;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+import com.vaadin.terminal.gwt.server.WebApplicationContext;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.AbstractField;
+import com.vaadin.ui.Root;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.Window;
+
+/**
+ * <p>
+ * Base class required for all Vaadin applications. This class provides all the
+ * basic services required by Vaadin. These services allow external discovery
+ * and manipulation of the user, {@link com.vaadin.ui.Window windows} and
+ * themes, and starting and stopping the application.
+ * </p>
+ *
+ * <p>
+ * As mentioned, all Vaadin applications must inherit this class. However, this
+ * is almost all of what one needs to do to create a fully functional
+ * application. The only thing a class inheriting the <code>Application</code>
+ * needs to do is implement the <code>init</code> method where it creates the
+ * windows it needs to perform its function. Note that all applications must
+ * have at least one window: the main window. The first unnamed window
+ * constructed by an application automatically becomes the main window which
+ * behaves just like other windows with one exception: when accessing windows
+ * using URLs the main window corresponds to the application URL whereas other
+ * windows correspond to a URL gotten by catenating the window's name to the
+ * application URL.
+ * </p>
+ *
+ * <p>
+ * See the class <code>com.vaadin.demo.HelloWorld</code> for a simple example of
+ * a fully working application.
+ * </p>
+ *
+ * <p>
+ * <strong>Window access.</strong> <code>Application</code> provides methods to
+ * list, add and remove the windows it contains.
+ * </p>
+ *
+ * <p>
+ * <strong>Execution control.</strong> This class includes method to start and
+ * finish the execution of the application. Being finished means basically that
+ * no windows will be available from the application anymore.
+ * </p>
+ *
+ * <p>
+ * <strong>Theme selection.</strong> The theme selection process allows a theme
+ * to be specified at three different levels. When a window's theme needs to be
+ * found out, the window itself is queried for a preferred theme. If the window
+ * does not prefer a specific theme, the application containing the window is
+ * queried. If neither the application prefers a theme, the default theme for
+ * the {@link com.vaadin.terminal.Terminal terminal} is used. The terminal
+ * always defines a default theme.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Application implements Terminal.ErrorListener, Serializable {
+
+ /**
+ * The name of the parameter that is by default used in e.g. web.xml to
+ * define the name of the default {@link Root} class.
+ */
+ public static final String ROOT_PARAMETER = "root";
+
+ private static final Method BOOTSTRAP_FRAGMENT_METHOD = ReflectTools
+ .findMethod(BootstrapListener.class, "modifyBootstrapFragment",
+ BootstrapFragmentResponse.class);
+ private static final Method BOOTSTRAP_PAGE_METHOD = ReflectTools
+ .findMethod(BootstrapListener.class, "modifyBootstrapPage",
+ BootstrapPageResponse.class);
+
+ /**
+ * A special application designed to help migrating applications from Vaadin
+ * 6 to Vaadin 7. The legacy application supports setting a main window,
+ * adding additional browser level windows and defining the theme for the
+ * entire application.
+ *
+ * @deprecated This class is only intended to ease migration and should not
+ * be used for new projects.
+ *
+ * @since 7.0
+ */
+ @Deprecated
+ public static class LegacyApplication extends Application {
+ /**
+ * Ignore initial / and then get everything up to the next /
+ */
+ private static final Pattern WINDOW_NAME_PATTERN = Pattern
+ .compile("^/?([^/]+).*");
+
+ private Root.LegacyWindow mainWindow;
+ private String theme;
+
+ private Map<String, Root.LegacyWindow> legacyRootNames = new HashMap<String, Root.LegacyWindow>();
+
+ /**
+ * Sets the main window of this application. Setting window as a main
+ * window of this application also adds the window to this application.
+ *
+ * @param mainWindow
+ * the root to set as the default window
+ */
+ public void setMainWindow(Root.LegacyWindow mainWindow) {
+ if (this.mainWindow != null) {
+ throw new IllegalStateException(
+ "mainWindow has already been set");
+ }
+ if (mainWindow.getApplication() == null) {
+ mainWindow.setApplication(this);
+ } else if (mainWindow.getApplication() != this) {
+ throw new IllegalStateException(
+ "mainWindow is attached to another application");
+ }
+ this.mainWindow = mainWindow;
+ }
+
+ /**
+ * Gets the mainWindow of the application.
+ *
+ * <p>
+ * The main window is the window attached to the application URL (
+ * {@link #getURL()}) and thus which is show by default to the user.
+ * </p>
+ * <p>
+ * Note that each application must have at least one main window.
+ * </p>
+ *
+ * @return the root used as the default window
+ */
+ public Root.LegacyWindow getMainWindow() {
+ return mainWindow;
+ }
+
+ /**
+ * This implementation simulates the way of finding a window for a
+ * request by extracting a window name from the requested path and
+ * passes that name to {@link #getWindow(String)}.
+ *
+ * {@inheritDoc}
+ *
+ * @see #getWindow(String)
+ * @see Application#getRoot(WrappedRequest)
+ */
+
+ @Override
+ public Root.LegacyWindow getRoot(WrappedRequest request) {
+ String pathInfo = request.getRequestPathInfo();
+ String name = null;
+ if (pathInfo != null && pathInfo.length() > 0) {
+ Matcher matcher = WINDOW_NAME_PATTERN.matcher(pathInfo);
+ if (matcher.matches()) {
+ // Skip the initial slash
+ name = matcher.group(1);
+ }
+ }
+ Root.LegacyWindow window = getWindow(name);
+ if (window != null) {
+ return window;
+ }
+ return mainWindow;
+ }
+
+ /**
+ * Sets the application's theme.
+ * <p>
+ * Note that this theme can be overridden for a specific root with
+ * {@link Application#getThemeForRoot(Root)}. Setting theme to be
+ * <code>null</code> selects the default theme. For the available theme
+ * names, see the contents of the VAADIN/themes directory.
+ * </p>
+ *
+ * @param theme
+ * the new theme for this application.
+ */
+ public void setTheme(String theme) {
+ this.theme = theme;
+ }
+
+ /**
+ * Gets the application's theme. The application's theme is the default
+ * theme used by all the roots for which a theme is not explicitly
+ * defined. If the application theme is not explicitly set,
+ * <code>null</code> is returned.
+ *
+ * @return the name of the application's theme.
+ */
+ public String getTheme() {
+ return theme;
+ }
+
+ /**
+ * This implementation returns the theme that has been set using
+ * {@link #setTheme(String)}
+ * <p>
+ * {@inheritDoc}
+ */
+
+ @Override
+ public String getThemeForRoot(Root root) {
+ return theme;
+ }
+
+ /**
+ * <p>
+ * Gets a root by name. Returns <code>null</code> if the application is
+ * not running or it does not contain a window corresponding to the
+ * name.
+ * </p>
+ *
+ * @param name
+ * the name of the requested window
+ * @return a root corresponding to the name, or <code>null</code> to use
+ * the default window
+ */
+ public Root.LegacyWindow getWindow(String name) {
+ return legacyRootNames.get(name);
+ }
+
+ /**
+ * Counter to get unique names for windows with no explicit name
+ */
+ private int namelessRootIndex = 0;
+
+ /**
+ * Adds a new browser level window to this application. Please note that
+ * Root doesn't have a name that is used in the URL - to add a named
+ * window you should instead use {@link #addWindow(Root, String)}
+ *
+ * @param root
+ * the root window to add to the application
+ * @return returns the name that has been assigned to the window
+ *
+ * @see #addWindow(Root, String)
+ */
+ public void addWindow(Root.LegacyWindow root) {
+ if (root.getName() == null) {
+ String name = Integer.toString(namelessRootIndex++);
+ root.setName(name);
+ }
+
+ legacyRootNames.put(root.getName(), root);
+ root.setApplication(this);
+ }
+
+ /**
+ * Removes the specified window from the application. This also removes
+ * all name mappings for the window (see
+ * {@link #addWindow(Root, String) and #getWindowName(Root)}.
+ *
+ * <p>
+ * Note that removing window from the application does not close the
+ * browser window - the window is only removed from the server-side.
+ * </p>
+ *
+ * @param root
+ * the root to remove
+ */
+ public void removeWindow(Root.LegacyWindow root) {
+ for (Entry<String, Root.LegacyWindow> entry : legacyRootNames
+ .entrySet()) {
+ if (entry.getValue() == root) {
+ legacyRootNames.remove(entry.getKey());
+ }
+ }
+ }
+
+ /**
+ * Gets the set of windows contained by the application.
+ *
+ * <p>
+ * Note that the returned set of windows can not be modified.
+ * </p>
+ *
+ * @return the unmodifiable collection of windows.
+ */
+ public Collection<Root.LegacyWindow> getWindows() {
+ return Collections.unmodifiableCollection(legacyRootNames.values());
+ }
+ }
+
+ /**
+ * An event sent to {@link #start(ApplicationStartEvent)} when a new
+ * Application is being started.
+ *
+ * @since 7.0
+ */
+ public static class ApplicationStartEvent implements Serializable {
+ private final URL applicationUrl;
+
+ private final Properties applicationProperties;
+
+ private final ApplicationContext context;
+
+ private final boolean productionMode;
+
+ /**
+ * @param applicationUrl
+ * the URL the application should respond to.
+ * @param applicationProperties
+ * the Application properties as specified by the deployment
+ * configuration.
+ * @param context
+ * the context application will be running in.
+ * @param productionMode
+ * flag indicating whether the application is running in
+ * production mode.
+ */
+ public ApplicationStartEvent(URL applicationUrl,
+ Properties applicationProperties, ApplicationContext context,
+ boolean productionMode) {
+ this.applicationUrl = applicationUrl;
+ this.applicationProperties = applicationProperties;
+ this.context = context;
+ this.productionMode = productionMode;
+ }
+
+ /**
+ * Gets the URL the application should respond to.
+ *
+ * @return the URL the application should respond to or
+ * <code>null</code> if the URL is not defined.
+ *
+ * @see Application#getURL()
+ */
+ public URL getApplicationUrl() {
+ return applicationUrl;
+ }
+
+ /**
+ * Gets the Application properties as specified by the deployment
+ * configuration.
+ *
+ * @return the properties configured for the applciation.
+ *
+ * @see Application#getProperty(String)
+ */
+ public Properties getApplicationProperties() {
+ return applicationProperties;
+ }
+
+ /**
+ * Gets the context application will be running in.
+ *
+ * @return the context application will be running in.
+ *
+ * @see Application#getContext()
+ */
+ public ApplicationContext getContext() {
+ return context;
+ }
+
+ /**
+ * Checks whether the application is running in production mode.
+ *
+ * @return <code>true</code> if in production mode, else
+ * <code>false</code>
+ *
+ * @see Application#isProductionMode()
+ */
+ public boolean isProductionMode() {
+ return productionMode;
+ }
+ }
+
+ private final static Logger logger = Logger.getLogger(Application.class
+ .getName());
+
+ /**
+ * Application context the application is running in.
+ */
+ private ApplicationContext context;
+
+ /**
+ * The current user or <code>null</code> if no user has logged in.
+ */
+ private Object user;
+
+ /**
+ * The application's URL.
+ */
+ private URL applicationUrl;
+
+ /**
+ * Application status.
+ */
+ private volatile boolean applicationIsRunning = false;
+
+ /**
+ * Application properties.
+ */
+ private Properties properties;
+
+ /**
+ * Default locale of the application.
+ */
+ private Locale locale;
+
+ /**
+ * List of listeners listening user changes.
+ */
+ private LinkedList<UserChangeListener> userChangeListeners = null;
+
+ /**
+ * Application resource mapping: key <-> resource.
+ */
+ private final Hashtable<ApplicationResource, String> resourceKeyMap = new Hashtable<ApplicationResource, String>();
+
+ private final Hashtable<String, ApplicationResource> keyResourceMap = new Hashtable<String, ApplicationResource>();
+
+ private long lastResourceKeyNumber = 0;
+
+ /**
+ * URL where the user is redirected to on application close, or null if
+ * application is just closed without redirection.
+ */
+ private String logoutURL = null;
+
+ /**
+ * The default SystemMessages (read-only). Change by overriding
+ * getSystemMessages() and returning CustomizedSystemMessages
+ */
+ private static final SystemMessages DEFAULT_SYSTEM_MESSAGES = new SystemMessages();
+
+ /**
+ * Application wide error handler which is used by default if an error is
+ * left unhandled.
+ */
+ private Terminal.ErrorListener errorHandler = this;
+
+ /**
+ * The converter factory that is used to provide default converters for the
+ * application.
+ */
+ private ConverterFactory converterFactory = new DefaultConverterFactory();
+
+ private LinkedList<RequestHandler> requestHandlers = new LinkedList<RequestHandler>();
+
+ private int nextRootId = 0;
+ private Map<Integer, Root> roots = new HashMap<Integer, Root>();
+
+ private boolean productionMode = true;
+
+ private final Map<String, Integer> retainOnRefreshRoots = new HashMap<String, Integer>();
+
+ private final EventRouter eventRouter = new EventRouter();
+
+ /**
+ * Keeps track of which roots have been inited.
+ * <p>
+ * TODO Investigate whether this might be derived from the different states
+ * in getRootForRrequest.
+ * </p>
+ */
+ private Set<Integer> initedRoots = new HashSet<Integer>();
+
+ /**
+ * Gets the user of the application.
+ *
+ * <p>
+ * Vaadin doesn't define of use user object in any way - it only provides
+ * this getter and setter methods for convenience. The user is any object
+ * that has been stored to the application with {@link #setUser(Object)}.
+ * </p>
+ *
+ * @return the User of the application.
+ */
+ public Object getUser() {
+ return user;
+ }
+
+ /**
+ * <p>
+ * Sets the user of the application instance. An application instance may
+ * have a user associated to it. This can be set in login procedure or
+ * application initialization.
+ * </p>
+ * <p>
+ * A component performing the user login procedure can assign the user
+ * property of the application and make the user object available to other
+ * components of the application.
+ * </p>
+ * <p>
+ * Vaadin doesn't define of use user object in any way - it only provides
+ * getter and setter methods for convenience. The user reference stored to
+ * the application can be read with {@link #getUser()}.
+ * </p>
+ *
+ * @param user
+ * the new user.
+ */
+ public void setUser(Object user) {
+ final Object prevUser = this.user;
+ if (user == prevUser || (user != null && user.equals(prevUser))) {
+ return;
+ }
+
+ this.user = user;
+ if (userChangeListeners != null) {
+ final Object[] listeners = userChangeListeners.toArray();
+ final UserChangeEvent event = new UserChangeEvent(this, user,
+ prevUser);
+ for (int i = 0; i < listeners.length; i++) {
+ ((UserChangeListener) listeners[i])
+ .applicationUserChanged(event);
+ }
+ }
+ }
+
+ /**
+ * Gets the URL of the application.
+ *
+ * <p>
+ * This is the URL what can be entered to a browser window to start the
+ * application. Navigating to the application URL shows the main window (
+ * {@link #getMainWindow()}) of the application. Note that the main window
+ * can also be shown by navigating to the window url (
+ * {@link com.vaadin.ui.Window#getURL()}).
+ * </p>
+ *
+ * @return the application's URL.
+ */
+ public URL getURL() {
+ return applicationUrl;
+ }
+
+ /**
+ * Ends the Application.
+ *
+ * <p>
+ * In effect this will cause the application stop returning any windows when
+ * asked. When the application is closed, its state is removed from the
+ * session and the browser window is redirected to the application logout
+ * url set with {@link #setLogoutURL(String)}. If the logout url has not
+ * been set, the browser window is reloaded and the application is
+ * restarted.
+ * </p>
+ * .
+ */
+ public void close() {
+ applicationIsRunning = false;
+ }
+
+ /**
+ * Starts the application on the given URL.
+ *
+ * <p>
+ * This method is called by Vaadin framework when a user navigates to the
+ * application. After this call the application corresponds to the given URL
+ * and it will return windows when asked for them. There is no need to call
+ * this method directly.
+ * </p>
+ *
+ * <p>
+ * Application properties are defined by servlet configuration object
+ * {@link javax.servlet.ServletConfig} and they are overridden by
+ * context-wide initialization parameters
+ * {@link javax.servlet.ServletContext}.
+ * </p>
+ *
+ * @param event
+ * the application start event containing details required for
+ * starting the application.
+ *
+ */
+ public void start(ApplicationStartEvent event) {
+ applicationUrl = event.getApplicationUrl();
+ productionMode = event.isProductionMode();
+ properties = event.getApplicationProperties();
+ context = event.getContext();
+ init();
+ applicationIsRunning = true;
+ }
+
+ /**
+ * Tests if the application is running or if it has been finished.
+ *
+ * <p>
+ * Application starts running when its
+ * {@link #start(URL, Properties, ApplicationContext)} method has been
+ * called and stops when the {@link #close()} is called.
+ * </p>
+ *
+ * @return <code>true</code> if the application is running,
+ * <code>false</code> if not.
+ */
+ public boolean isRunning() {
+ return applicationIsRunning;
+ }
+
+ /**
+ * <p>
+ * Main initializer of the application. The <code>init</code> method is
+ * called by the framework when the application is started, and it should
+ * perform whatever initialization operations the application needs.
+ * </p>
+ */
+ public void init() {
+ // Default implementation does nothing
+ }
+
+ /**
+ * Returns an enumeration of all the names in this application.
+ *
+ * <p>
+ * See {@link #start(URL, Properties, ApplicationContext)} how properties
+ * are defined.
+ * </p>
+ *
+ * @return an enumeration of all the keys in this property list, including
+ * the keys in the default property list.
+ *
+ */
+ public Enumeration<?> getPropertyNames() {
+ return properties.propertyNames();
+ }
+
+ /**
+ * Searches for the property with the specified name in this application.
+ * This method returns <code>null</code> if the property is not found.
+ *
+ * See {@link #start(URL, Properties, ApplicationContext)} how properties
+ * are defined.
+ *
+ * @param name
+ * the name of the property.
+ * @return the value in this property list with the specified key value.
+ */
+ public String getProperty(String name) {
+ return properties.getProperty(name);
+ }
+
+ /**
+ * Adds new resource to the application. The resource can be accessed by the
+ * user of the application.
+ *
+ * @param resource
+ * the resource to add.
+ */
+ public void addResource(ApplicationResource resource) {
+
+ // Check if the resource is already mapped
+ if (resourceKeyMap.containsKey(resource)) {
+ return;
+ }
+
+ // Generate key
+ final String key = String.valueOf(++lastResourceKeyNumber);
+
+ // Add the resource to mappings
+ resourceKeyMap.put(resource, key);
+ keyResourceMap.put(key, resource);
+ }
+
+ /**
+ * Removes the resource from the application.
+ *
+ * @param resource
+ * the resource to remove.
+ */
+ public void removeResource(ApplicationResource resource) {
+ final Object key = resourceKeyMap.get(resource);
+ if (key != null) {
+ resourceKeyMap.remove(resource);
+ keyResourceMap.remove(key);
+ }
+ }
+
+ /**
+ * Gets the relative uri of the resource. This method is intended to be
+ * called only be the terminal implementation.
+ *
+ * This method can only be called from within the processing of a UIDL
+ * request, not from a background thread.
+ *
+ * @param resource
+ * the resource to get relative location.
+ * @return the relative uri of the resource or null if called in a
+ * background thread
+ *
+ * @deprecated this method is intended to be used by the terminal only. It
+ * may be removed or moved in the future.
+ */
+ @Deprecated
+ public String getRelativeLocation(ApplicationResource resource) {
+
+ // Gets the key
+ final String key = resourceKeyMap.get(resource);
+
+ // If the resource is not registered, return null
+ if (key == null) {
+ return null;
+ }
+
+ return context.generateApplicationResourceURL(resource, key);
+ }
+
+ /**
+ * Gets the default locale for this application.
+ *
+ * By default this is the preferred locale of the user using the
+ * application. In most cases it is read from the browser defaults.
+ *
+ * @return the locale of this application.
+ */
+ public Locale getLocale() {
+ if (locale != null) {
+ return locale;
+ }
+ return Locale.getDefault();
+ }
+
+ /**
+ * Sets the default locale for this application.
+ *
+ * By default this is the preferred locale of the user using the
+ * application. In most cases it is read from the browser defaults.
+ *
+ * @param locale
+ * the Locale object.
+ *
+ */
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * <p>
+ * An event that characterizes a change in the current selection.
+ * </p>
+ * Application user change event sent when the setUser is called to change
+ * the current user of the application.
+ *
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class UserChangeEvent extends java.util.EventObject {
+
+ /**
+ * New user of the application.
+ */
+ private final Object newUser;
+
+ /**
+ * Previous user of the application.
+ */
+ private final Object prevUser;
+
+ /**
+ * Constructor for user change event.
+ *
+ * @param source
+ * the application source.
+ * @param newUser
+ * the new User.
+ * @param prevUser
+ * the previous User.
+ */
+ public UserChangeEvent(Application source, Object newUser,
+ Object prevUser) {
+ super(source);
+ this.newUser = newUser;
+ this.prevUser = prevUser;
+ }
+
+ /**
+ * Gets the new user of the application.
+ *
+ * @return the new User.
+ */
+ public Object getNewUser() {
+ return newUser;
+ }
+
+ /**
+ * Gets the previous user of the application.
+ *
+ * @return the previous Vaadin user, if user has not changed ever on
+ * application it returns <code>null</code>
+ */
+ public Object getPreviousUser() {
+ return prevUser;
+ }
+
+ /**
+ * Gets the application where the user change occurred.
+ *
+ * @return the Application.
+ */
+ public Application getApplication() {
+ return (Application) getSource();
+ }
+ }
+
+ /**
+ * The <code>UserChangeListener</code> interface for listening application
+ * user changes.
+ *
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface UserChangeListener extends EventListener, Serializable {
+
+ /**
+ * The <code>applicationUserChanged</code> method Invoked when the
+ * application user has changed.
+ *
+ * @param event
+ * the change event.
+ */
+ public void applicationUserChanged(Application.UserChangeEvent event);
+ }
+
+ /**
+ * Adds the user change listener.
+ *
+ * This allows one to get notification each time {@link #setUser(Object)} is
+ * called.
+ *
+ * @param listener
+ * the user change listener to add.
+ */
+ public void addListener(UserChangeListener listener) {
+ if (userChangeListeners == null) {
+ userChangeListeners = new LinkedList<UserChangeListener>();
+ }
+ userChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes the user change listener.
+ *
+ * @param listener
+ * the user change listener to remove.
+ */
+ public void removeListener(UserChangeListener listener) {
+ if (userChangeListeners == null) {
+ return;
+ }
+ userChangeListeners.remove(listener);
+ if (userChangeListeners.isEmpty()) {
+ userChangeListeners = null;
+ }
+ }
+
+ /**
+ * Window detach event.
+ *
+ * This event is sent each time a window is removed from the application
+ * with {@link com.vaadin.Application#removeWindow(Window)}.
+ */
+ public class WindowDetachEvent extends EventObject {
+
+ private final Window window;
+
+ /**
+ * Creates a event.
+ *
+ * @param window
+ * the Detached window.
+ */
+ public WindowDetachEvent(Window window) {
+ super(Application.this);
+ this.window = window;
+ }
+
+ /**
+ * Gets the detached window.
+ *
+ * @return the detached window.
+ */
+ public Window getWindow() {
+ return window;
+ }
+
+ /**
+ * Gets the application from which the window was detached.
+ *
+ * @return the Application.
+ */
+ public Application getApplication() {
+ return (Application) getSource();
+ }
+ }
+
+ /**
+ * Window attach event.
+ *
+ * This event is sent each time a window is attached tothe application with
+ * {@link com.vaadin.Application#addWindow(Window)}.
+ */
+ public class WindowAttachEvent extends EventObject {
+
+ private final Window window;
+
+ /**
+ * Creates a event.
+ *
+ * @param window
+ * the Attached window.
+ */
+ public WindowAttachEvent(Window window) {
+ super(Application.this);
+ this.window = window;
+ }
+
+ /**
+ * Gets the attached window.
+ *
+ * @return the attached window.
+ */
+ public Window getWindow() {
+ return window;
+ }
+
+ /**
+ * Gets the application to which the window was attached.
+ *
+ * @return the Application.
+ */
+ public Application getApplication() {
+ return (Application) getSource();
+ }
+ }
+
+ /**
+ * Window attach listener interface.
+ */
+ public interface WindowAttachListener extends Serializable {
+
+ /**
+ * Window attached
+ *
+ * @param event
+ * the window attach event.
+ */
+ public void windowAttached(WindowAttachEvent event);
+ }
+
+ /**
+ * Window detach listener interface.
+ */
+ public interface WindowDetachListener extends Serializable {
+
+ /**
+ * Window detached.
+ *
+ * @param event
+ * the window detach event.
+ */
+ public void windowDetached(WindowDetachEvent event);
+ }
+
+ /**
+ * Returns the URL user is redirected to on application close. If the URL is
+ * <code>null</code>, the application is closed normally as defined by the
+ * application running environment.
+ * <p>
+ * Desktop application just closes the application window and
+ * web-application redirects the browser to application main URL.
+ * </p>
+ *
+ * @return the URL.
+ */
+ public String getLogoutURL() {
+ return logoutURL;
+ }
+
+ /**
+ * Sets the URL user is redirected to on application close. If the URL is
+ * <code>null</code>, the application is closed normally as defined by the
+ * application running environment: Desktop application just closes the
+ * application window and web-application redirects the browser to
+ * application main URL.
+ *
+ * @param logoutURL
+ * the logoutURL to set.
+ */
+ public void setLogoutURL(String logoutURL) {
+ this.logoutURL = logoutURL;
+ }
+
+ /**
+ * Gets the SystemMessages for this application. SystemMessages are used to
+ * notify the user of various critical situations that can occur, such as
+ * session expiration, client/server out of sync, and internal server error.
+ *
+ * You can customize the messages by "overriding" this method and returning
+ * {@link CustomizedSystemMessages}. To "override" this method, re-implement
+ * this method in your application (the class that extends
+ * {@link Application}). Even though overriding static methods is not
+ * possible in Java, Vaadin selects to call the static method from the
+ * subclass instead of the original {@link #getSystemMessages()} if such a
+ * method exists.
+ *
+ * @return the SystemMessages for this application
+ */
+ public static SystemMessages getSystemMessages() {
+ return DEFAULT_SYSTEM_MESSAGES;
+ }
+
+ /**
+ * <p>
+ * Invoked by the terminal on any exception that occurs in application and
+ * is thrown by the <code>setVariable</code> to the terminal. The default
+ * implementation sets the exceptions as <code>ComponentErrors</code> to the
+ * component that initiated the exception and prints stack trace to standard
+ * error stream.
+ * </p>
+ * <p>
+ * You can safely override this method in your application in order to
+ * direct the errors to some other destination (for example log).
+ * </p>
+ *
+ * @param event
+ * the change event.
+ * @see com.vaadin.terminal.Terminal.ErrorListener#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)
+ */
+
+ @Override
+ public void terminalError(Terminal.ErrorEvent event) {
+ final Throwable t = event.getThrowable();
+ if (t instanceof SocketException) {
+ // Most likely client browser closed socket
+ getLogger().info(
+ "SocketException in CommunicationManager."
+ + " Most likely client (browser) closed socket.");
+ return;
+ }
+
+ // Finds the original source of the error/exception
+ Object owner = null;
+ if (event instanceof VariableOwner.ErrorEvent) {
+ owner = ((VariableOwner.ErrorEvent) event).getVariableOwner();
+ } else if (event instanceof ChangeVariablesErrorEvent) {
+ owner = ((ChangeVariablesErrorEvent) event).getComponent();
+ }
+
+ // Shows the error in AbstractComponent
+ if (owner instanceof AbstractComponent) {
+ ((AbstractComponent) owner).setComponentError(AbstractErrorMessage
+ .getErrorMessageForException(t));
+ }
+
+ // also print the error on console
+ getLogger().log(Level.SEVERE, "Terminal error:", t);
+ }
+
+ /**
+ * Gets the application context.
+ * <p>
+ * The application context is the environment where the application is
+ * running in. The actual implementation class of may contains quite a lot
+ * more functionality than defined in the {@link ApplicationContext}
+ * interface.
+ * </p>
+ * <p>
+ * By default, when you are deploying your application to a servlet
+ * container, the implementation class is {@link WebApplicationContext} -
+ * you can safely cast to this class and use the methods from there. When
+ * you are deploying your application as a portlet, context implementation
+ * is {@link PortletApplicationContext}.
+ * </p>
+ *
+ * @return the application context.
+ */
+ public ApplicationContext getContext() {
+ return context;
+ }
+
+ /**
+ * Override this method to return correct version number of your
+ * Application. Version information is delivered for example to Testing
+ * Tools test results. By default this returns a string "NONVERSIONED".
+ *
+ * @return version string
+ */
+ public String getVersion() {
+ return "NONVERSIONED";
+ }
+
+ /**
+ * Gets the application error handler.
+ *
+ * The default error handler is the application itself.
+ *
+ * @return Application error handler
+ */
+ public Terminal.ErrorListener getErrorHandler() {
+ return errorHandler;
+ }
+
+ /**
+ * Sets the application error handler.
+ *
+ * The default error handler is the application itself. By overriding this,
+ * you can redirect the error messages to your selected target (log for
+ * example).
+ *
+ * @param errorHandler
+ */
+ public void setErrorHandler(Terminal.ErrorListener errorHandler) {
+ this.errorHandler = errorHandler;
+ }
+
+ /**
+ * Gets the {@link ConverterFactory} used to locate a suitable
+ * {@link Converter} for fields in the application.
+ *
+ * See {@link #setConverterFactory(ConverterFactory)} for more details
+ *
+ * @return The converter factory used in the application
+ */
+ public ConverterFactory getConverterFactory() {
+ return converterFactory;
+ }
+
+ /**
+ * Sets the {@link ConverterFactory} used to locate a suitable
+ * {@link Converter} for fields in the application.
+ * <p>
+ * The {@link ConverterFactory} is used to find a suitable converter when
+ * binding data to a UI component and the data type does not match the UI
+ * component type, e.g. binding a Double to a TextField (which is based on a
+ * String).
+ * </p>
+ * <p>
+ * The {@link Converter} for an individual field can be overridden using
+ * {@link AbstractField#setConverter(Converter)} and for individual property
+ * ids in a {@link Table} using
+ * {@link Table#setConverter(Object, Converter)}.
+ * </p>
+ * <p>
+ * The converter factory must never be set to null.
+ *
+ * @param converterFactory
+ * The converter factory used in the application
+ */
+ public void setConverterFactory(ConverterFactory converterFactory) {
+ this.converterFactory = converterFactory;
+ }
+
+ /**
+ * Contains the system messages used to notify the user about various
+ * critical situations that can occur.
+ * <p>
+ * Customize by overriding the static
+ * {@link Application#getSystemMessages()} and returning
+ * {@link CustomizedSystemMessages}.
+ * </p>
+ * <p>
+ * The defaults defined in this class are:
+ * <ul>
+ * <li><b>sessionExpiredURL</b> = null</li>
+ * <li><b>sessionExpiredNotificationEnabled</b> = true</li>
+ * <li><b>sessionExpiredCaption</b> = ""</li>
+ * <li><b>sessionExpiredMessage</b> =
+ * "Take note of any unsaved data, and <u>click here</u> to continue."</li>
+ * <li><b>communicationErrorURL</b> = null</li>
+ * <li><b>communicationErrorNotificationEnabled</b> = true</li>
+ * <li><b>communicationErrorCaption</b> = "Communication problem"</li>
+ * <li><b>communicationErrorMessage</b> =
+ * "Take note of any unsaved data, and <u>click here</u> to continue."</li>
+ * <li><b>internalErrorURL</b> = null</li>
+ * <li><b>internalErrorNotificationEnabled</b> = true</li>
+ * <li><b>internalErrorCaption</b> = "Internal error"</li>
+ * <li><b>internalErrorMessage</b> = "Please notify the administrator.<br/>
+ * Take note of any unsaved data, and <u>click here</u> to continue."</li>
+ * <li><b>outOfSyncURL</b> = null</li>
+ * <li><b>outOfSyncNotificationEnabled</b> = true</li>
+ * <li><b>outOfSyncCaption</b> = "Out of sync"</li>
+ * <li><b>outOfSyncMessage</b> = "Something has caused us to be out of sync
+ * with the server.<br/>
+ * Take note of any unsaved data, and <u>click here</u> to re-sync."</li>
+ * <li><b>cookiesDisabledURL</b> = null</li>
+ * <li><b>cookiesDisabledNotificationEnabled</b> = true</li>
+ * <li><b>cookiesDisabledCaption</b> = "Cookies disabled"</li>
+ * <li><b>cookiesDisabledMessage</b> = "This application requires cookies to
+ * function.<br/>
+ * Please enable cookies in your browser and <u>click here</u> to try again.
+ * </li>
+ * </ul>
+ * </p>
+ *
+ */
+ public static class SystemMessages implements Serializable {
+ protected String sessionExpiredURL = null;
+ protected boolean sessionExpiredNotificationEnabled = true;
+ protected String sessionExpiredCaption = "Session Expired";
+ protected String sessionExpiredMessage = "Take note of any unsaved data, and <u>click here</u> to continue.";
+
+ protected String communicationErrorURL = null;
+ protected boolean communicationErrorNotificationEnabled = true;
+ protected String communicationErrorCaption = "Communication problem";
+ protected String communicationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue.";
+
+ protected String authenticationErrorURL = null;
+ protected boolean authenticationErrorNotificationEnabled = true;
+ protected String authenticationErrorCaption = "Authentication problem";
+ protected String authenticationErrorMessage = "Take note of any unsaved data, and <u>click here</u> to continue.";
+
+ protected String internalErrorURL = null;
+ protected boolean internalErrorNotificationEnabled = true;
+ protected String internalErrorCaption = "Internal error";
+ protected String internalErrorMessage = "Please notify the administrator.<br/>Take note of any unsaved data, and <u>click here</u> to continue.";
+
+ protected String outOfSyncURL = null;
+ protected boolean outOfSyncNotificationEnabled = true;
+ protected String outOfSyncCaption = "Out of sync";
+ protected String outOfSyncMessage = "Something has caused us to be out of sync with the server.<br/>Take note of any unsaved data, and <u>click here</u> to re-sync.";
+
+ protected String cookiesDisabledURL = null;
+ protected boolean cookiesDisabledNotificationEnabled = true;
+ protected String cookiesDisabledCaption = "Cookies disabled";
+ protected String cookiesDisabledMessage = "This application requires cookies to function.<br/>Please enable cookies in your browser and <u>click here</u> to try again.";
+
+ /**
+ * Use {@link CustomizedSystemMessages} to customize
+ */
+ private SystemMessages() {
+
+ }
+
+ /**
+ * @return null to indicate that the application will be restarted after
+ * session expired message has been shown.
+ */
+ public String getSessionExpiredURL() {
+ return sessionExpiredURL;
+ }
+
+ /**
+ * @return true to show session expiration message.
+ */
+ public boolean isSessionExpiredNotificationEnabled() {
+ return sessionExpiredNotificationEnabled;
+ }
+
+ /**
+ * @return "" to show no caption.
+ */
+ public String getSessionExpiredCaption() {
+ return (sessionExpiredNotificationEnabled ? sessionExpiredCaption
+ : null);
+ }
+
+ /**
+ * @return
+ * "Take note of any unsaved data, and <u>click here</u> to continue."
+ */
+ public String getSessionExpiredMessage() {
+ return (sessionExpiredNotificationEnabled ? sessionExpiredMessage
+ : null);
+ }
+
+ /**
+ * @return null to reload the application after communication error
+ * message.
+ */
+ public String getCommunicationErrorURL() {
+ return communicationErrorURL;
+ }
+
+ /**
+ * @return true to show the communication error message.
+ */
+ public boolean isCommunicationErrorNotificationEnabled() {
+ return communicationErrorNotificationEnabled;
+ }
+
+ /**
+ * @return "Communication problem"
+ */
+ public String getCommunicationErrorCaption() {
+ return (communicationErrorNotificationEnabled ? communicationErrorCaption
+ : null);
+ }
+
+ /**
+ * @return
+ * "Take note of any unsaved data, and <u>click here</u> to continue."
+ */
+ public String getCommunicationErrorMessage() {
+ return (communicationErrorNotificationEnabled ? communicationErrorMessage
+ : null);
+ }
+
+ /**
+ * @return null to reload the application after authentication error
+ * message.
+ */
+ public String getAuthenticationErrorURL() {
+ return authenticationErrorURL;
+ }
+
+ /**
+ * @return true to show the authentication error message.
+ */
+ public boolean isAuthenticationErrorNotificationEnabled() {
+ return authenticationErrorNotificationEnabled;
+ }
+
+ /**
+ * @return "Authentication problem"
+ */
+ public String getAuthenticationErrorCaption() {
+ return (authenticationErrorNotificationEnabled ? authenticationErrorCaption
+ : null);
+ }
+
+ /**
+ * @return
+ * "Take note of any unsaved data, and <u>click here</u> to continue."
+ */
+ public String getAuthenticationErrorMessage() {
+ return (authenticationErrorNotificationEnabled ? authenticationErrorMessage
+ : null);
+ }
+
+ /**
+ * @return null to reload the current URL after internal error message
+ * has been shown.
+ */
+ public String getInternalErrorURL() {
+ return internalErrorURL;
+ }
+
+ /**
+ * @return true to enable showing of internal error message.
+ */
+ public boolean isInternalErrorNotificationEnabled() {
+ return internalErrorNotificationEnabled;
+ }
+
+ /**
+ * @return "Internal error"
+ */
+ public String getInternalErrorCaption() {
+ return (internalErrorNotificationEnabled ? internalErrorCaption
+ : null);
+ }
+
+ /**
+ * @return "Please notify the administrator.<br/>
+ * Take note of any unsaved data, and <u>click here</u> to
+ * continue."
+ */
+ public String getInternalErrorMessage() {
+ return (internalErrorNotificationEnabled ? internalErrorMessage
+ : null);
+ }
+
+ /**
+ * @return null to reload the application after out of sync message.
+ */
+ public String getOutOfSyncURL() {
+ return outOfSyncURL;
+ }
+
+ /**
+ * @return true to enable showing out of sync message
+ */
+ public boolean isOutOfSyncNotificationEnabled() {
+ return outOfSyncNotificationEnabled;
+ }
+
+ /**
+ * @return "Out of sync"
+ */
+ public String getOutOfSyncCaption() {
+ return (outOfSyncNotificationEnabled ? outOfSyncCaption : null);
+ }
+
+ /**
+ * @return "Something has caused us to be out of sync with the server.<br/>
+ * Take note of any unsaved data, and <u>click here</u> to
+ * re-sync."
+ */
+ public String getOutOfSyncMessage() {
+ return (outOfSyncNotificationEnabled ? outOfSyncMessage : null);
+ }
+
+ /**
+ * Returns the URL the user should be redirected to after dismissing the
+ * "you have to enable your cookies" message. Typically null.
+ *
+ * @return A URL the user should be redirected to after dismissing the
+ * message or null to reload the current URL.
+ */
+ public String getCookiesDisabledURL() {
+ return cookiesDisabledURL;
+ }
+
+ /**
+ * Determines if "cookies disabled" messages should be shown to the end
+ * user or not. If the notification is disabled the user will be
+ * immediately redirected to the URL returned by
+ * {@link #getCookiesDisabledURL()}.
+ *
+ * @return true to show "cookies disabled" messages to the end user,
+ * false to redirect to the given URL directly
+ */
+ public boolean isCookiesDisabledNotificationEnabled() {
+ return cookiesDisabledNotificationEnabled;
+ }
+
+ /**
+ * Returns the caption of the message shown to the user when cookies are
+ * disabled in the browser.
+ *
+ * @return The caption of the "cookies disabled" message
+ */
+ public String getCookiesDisabledCaption() {
+ return (cookiesDisabledNotificationEnabled ? cookiesDisabledCaption
+ : null);
+ }
+
+ /**
+ * Returns the message shown to the user when cookies are disabled in
+ * the browser.
+ *
+ * @return The "cookies disabled" message
+ */
+ public String getCookiesDisabledMessage() {
+ return (cookiesDisabledNotificationEnabled ? cookiesDisabledMessage
+ : null);
+ }
+
+ }
+
+ /**
+ * Contains the system messages used to notify the user about various
+ * critical situations that can occur.
+ * <p>
+ * Vaadin gets the SystemMessages from your application by calling a static
+ * getSystemMessages() method. By default the
+ * Application.getSystemMessages() is used. You can customize this by
+ * defining a static MyApplication.getSystemMessages() and returning
+ * CustomizedSystemMessages. Note that getSystemMessages() is static -
+ * changing the system messages will by default change the message for all
+ * users of the application.
+ * </p>
+ * <p>
+ * The default behavior is to show a notification, and restart the
+ * application the the user clicks the message. <br/>
+ * Instead of restarting the application, you can set a specific URL that
+ * the user is taken to.<br/>
+ * Setting both caption and message to null will restart the application (or
+ * go to the specified URL) without displaying a notification.
+ * set*NotificationEnabled(false) will achieve the same thing.
+ * </p>
+ * <p>
+ * The situations are:
+ * <li>Session expired: the user session has expired, usually due to
+ * inactivity.</li>
+ * <li>Communication error: the client failed to contact the server, or the
+ * server returned and invalid response.</li>
+ * <li>Internal error: unhandled critical server error (e.g out of memory,
+ * database crash)
+ * <li>Out of sync: the client is not in sync with the server. E.g the user
+ * opens two windows showing the same application, but the application does
+ * not support this and uses the same Window instance. When the user makes
+ * changes in one of the windows - the other window is no longer in sync,
+ * and (for instance) pressing a button that is no longer present in the UI
+ * will cause a out-of-sync -situation.
+ * </p>
+ */
+
+ public static class CustomizedSystemMessages extends SystemMessages
+ implements Serializable {
+
+ /**
+ * Sets the URL to go to when the session has expired.
+ *
+ * @param sessionExpiredURL
+ * the URL to go to, or null to reload current
+ */
+ public void setSessionExpiredURL(String sessionExpiredURL) {
+ this.sessionExpiredURL = sessionExpiredURL;
+ }
+
+ /**
+ * Enables or disables the notification. If disabled, the set URL (or
+ * current) is loaded directly when next transaction between server and
+ * client happens.
+ *
+ * @param sessionExpiredNotificationEnabled
+ * true = enabled, false = disabled
+ */
+ public void setSessionExpiredNotificationEnabled(
+ boolean sessionExpiredNotificationEnabled) {
+ this.sessionExpiredNotificationEnabled = sessionExpiredNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the notification. Set to null for no caption. If
+ * both caption and message are null, client automatically forwards to
+ * sessionExpiredUrl after timeout timer expires. Timer uses value read
+ * from HTTPSession.getMaxInactiveInterval()
+ *
+ * @param sessionExpiredCaption
+ * the caption
+ */
+ public void setSessionExpiredCaption(String sessionExpiredCaption) {
+ this.sessionExpiredCaption = sessionExpiredCaption;
+ }
+
+ /**
+ * Sets the message of the notification. Set to null for no message. If
+ * both caption and message are null, client automatically forwards to
+ * sessionExpiredUrl after timeout timer expires. Timer uses value read
+ * from HTTPSession.getMaxInactiveInterval()
+ *
+ * @param sessionExpiredMessage
+ * the message
+ */
+ public void setSessionExpiredMessage(String sessionExpiredMessage) {
+ this.sessionExpiredMessage = sessionExpiredMessage;
+ }
+
+ /**
+ * Sets the URL to go to when there is a authentication error.
+ *
+ * @param authenticationErrorURL
+ * the URL to go to, or null to reload current
+ */
+ public void setAuthenticationErrorURL(String authenticationErrorURL) {
+ this.authenticationErrorURL = authenticationErrorURL;
+ }
+
+ /**
+ * Enables or disables the notification. If disabled, the set URL (or
+ * current) is loaded directly.
+ *
+ * @param authenticationErrorNotificationEnabled
+ * true = enabled, false = disabled
+ */
+ public void setAuthenticationErrorNotificationEnabled(
+ boolean authenticationErrorNotificationEnabled) {
+ this.authenticationErrorNotificationEnabled = authenticationErrorNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the notification. Set to null for no caption. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param authenticationErrorCaption
+ * the caption
+ */
+ public void setAuthenticationErrorCaption(
+ String authenticationErrorCaption) {
+ this.authenticationErrorCaption = authenticationErrorCaption;
+ }
+
+ /**
+ * Sets the message of the notification. Set to null for no message. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param authenticationErrorMessage
+ * the message
+ */
+ public void setAuthenticationErrorMessage(
+ String authenticationErrorMessage) {
+ this.authenticationErrorMessage = authenticationErrorMessage;
+ }
+
+ /**
+ * Sets the URL to go to when there is a communication error.
+ *
+ * @param communicationErrorURL
+ * the URL to go to, or null to reload current
+ */
+ public void setCommunicationErrorURL(String communicationErrorURL) {
+ this.communicationErrorURL = communicationErrorURL;
+ }
+
+ /**
+ * Enables or disables the notification. If disabled, the set URL (or
+ * current) is loaded directly.
+ *
+ * @param communicationErrorNotificationEnabled
+ * true = enabled, false = disabled
+ */
+ public void setCommunicationErrorNotificationEnabled(
+ boolean communicationErrorNotificationEnabled) {
+ this.communicationErrorNotificationEnabled = communicationErrorNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the notification. Set to null for no caption. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param communicationErrorCaption
+ * the caption
+ */
+ public void setCommunicationErrorCaption(
+ String communicationErrorCaption) {
+ this.communicationErrorCaption = communicationErrorCaption;
+ }
+
+ /**
+ * Sets the message of the notification. Set to null for no message. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param communicationErrorMessage
+ * the message
+ */
+ public void setCommunicationErrorMessage(
+ String communicationErrorMessage) {
+ this.communicationErrorMessage = communicationErrorMessage;
+ }
+
+ /**
+ * Sets the URL to go to when an internal error occurs.
+ *
+ * @param internalErrorURL
+ * the URL to go to, or null to reload current
+ */
+ public void setInternalErrorURL(String internalErrorURL) {
+ this.internalErrorURL = internalErrorURL;
+ }
+
+ /**
+ * Enables or disables the notification. If disabled, the set URL (or
+ * current) is loaded directly.
+ *
+ * @param internalErrorNotificationEnabled
+ * true = enabled, false = disabled
+ */
+ public void setInternalErrorNotificationEnabled(
+ boolean internalErrorNotificationEnabled) {
+ this.internalErrorNotificationEnabled = internalErrorNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the notification. Set to null for no caption. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param internalErrorCaption
+ * the caption
+ */
+ public void setInternalErrorCaption(String internalErrorCaption) {
+ this.internalErrorCaption = internalErrorCaption;
+ }
+
+ /**
+ * Sets the message of the notification. Set to null for no message. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param internalErrorMessage
+ * the message
+ */
+ public void setInternalErrorMessage(String internalErrorMessage) {
+ this.internalErrorMessage = internalErrorMessage;
+ }
+
+ /**
+ * Sets the URL to go to when the client is out-of-sync.
+ *
+ * @param outOfSyncURL
+ * the URL to go to, or null to reload current
+ */
+ public void setOutOfSyncURL(String outOfSyncURL) {
+ this.outOfSyncURL = outOfSyncURL;
+ }
+
+ /**
+ * Enables or disables the notification. If disabled, the set URL (or
+ * current) is loaded directly.
+ *
+ * @param outOfSyncNotificationEnabled
+ * true = enabled, false = disabled
+ */
+ public void setOutOfSyncNotificationEnabled(
+ boolean outOfSyncNotificationEnabled) {
+ this.outOfSyncNotificationEnabled = outOfSyncNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the notification. Set to null for no caption. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param outOfSyncCaption
+ * the caption
+ */
+ public void setOutOfSyncCaption(String outOfSyncCaption) {
+ this.outOfSyncCaption = outOfSyncCaption;
+ }
+
+ /**
+ * Sets the message of the notification. Set to null for no message. If
+ * both caption and message is null, the notification is disabled;
+ *
+ * @param outOfSyncMessage
+ * the message
+ */
+ public void setOutOfSyncMessage(String outOfSyncMessage) {
+ this.outOfSyncMessage = outOfSyncMessage;
+ }
+
+ /**
+ * Sets the URL to redirect to when the browser has cookies disabled.
+ *
+ * @param cookiesDisabledURL
+ * the URL to redirect to, or null to reload the current URL
+ */
+ public void setCookiesDisabledURL(String cookiesDisabledURL) {
+ this.cookiesDisabledURL = cookiesDisabledURL;
+ }
+
+ /**
+ * Enables or disables the notification for "cookies disabled" messages.
+ * If disabled, the URL returned by {@link #getCookiesDisabledURL()} is
+ * loaded directly.
+ *
+ * @param cookiesDisabledNotificationEnabled
+ * true to enable "cookies disabled" messages, false
+ * otherwise
+ */
+ public void setCookiesDisabledNotificationEnabled(
+ boolean cookiesDisabledNotificationEnabled) {
+ this.cookiesDisabledNotificationEnabled = cookiesDisabledNotificationEnabled;
+ }
+
+ /**
+ * Sets the caption of the "cookies disabled" notification. Set to null
+ * for no caption. If both caption and message is null, the notification
+ * is disabled.
+ *
+ * @param cookiesDisabledCaption
+ * the caption for the "cookies disabled" notification
+ */
+ public void setCookiesDisabledCaption(String cookiesDisabledCaption) {
+ this.cookiesDisabledCaption = cookiesDisabledCaption;
+ }
+
+ /**
+ * Sets the message of the "cookies disabled" notification. Set to null
+ * for no message. If both caption and message is null, the notification
+ * is disabled.
+ *
+ * @param cookiesDisabledMessage
+ * the message for the "cookies disabled" notification
+ */
+ public void setCookiesDisabledMessage(String cookiesDisabledMessage) {
+ this.cookiesDisabledMessage = cookiesDisabledMessage;
+ }
+
+ }
+
+ /**
+ * Application error is an error message defined on the application level.
+ *
+ * When an error occurs on the application level, this error message type
+ * should be used. This indicates that the problem is caused by the
+ * application - not by the user.
+ */
+ public class ApplicationError implements Terminal.ErrorEvent {
+ private final Throwable throwable;
+
+ public ApplicationError(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+
+ /**
+ * Gets a root for a request for which no root is already known. This method
+ * is called when the framework processes a request that does not originate
+ * from an existing root instance. This typically happens when a host page
+ * is requested.
+ *
+ * <p>
+ * Subclasses of Application may override this method to provide custom
+ * logic for choosing how to create a suitable root or for picking an
+ * already created root. If an existing root is picked, care should be taken
+ * to avoid keeping the same root open in multiple browser windows, as that
+ * will cause the states to go out of sync.
+ * </p>
+ *
+ * <p>
+ * If {@link BrowserDetails} are required to create a Root, the
+ * implementation can throw a {@link RootRequiresMoreInformationException}
+ * exception. In this case, the framework will instruct the browser to send
+ * the additional details, whereupon this method is invoked again with the
+ * browser details present in the wrapped request. Throwing the exception if
+ * the browser details are already available is not supported.
+ * </p>
+ *
+ * <p>
+ * The default implementation in {@link Application} creates a new instance
+ * of the Root class returned by {@link #getRootClassName(WrappedRequest)},
+ * which in turn uses the {@value #ROOT_PARAMETER} parameter from web.xml.
+ * If {@link DeploymentConfiguration#getClassLoader()} for the request
+ * returns a {@link ClassLoader}, it is used for loading the Root class.
+ * Otherwise the {@link ClassLoader} used to load this class is used.
+ * </p>
+ *
+ * @param request
+ * the wrapped request for which a root is needed
+ * @return a root instance to use for the request
+ * @throws RootRequiresMoreInformationException
+ * may be thrown by an implementation to indicate that
+ * {@link BrowserDetails} are required to create a root
+ *
+ * @see #getRootClassName(WrappedRequest)
+ * @see Root
+ * @see RootRequiresMoreInformationException
+ * @see WrappedRequest#getBrowserDetails()
+ *
+ * @since 7.0
+ */
+ protected Root getRoot(WrappedRequest request)
+ throws RootRequiresMoreInformationException {
+ String rootClassName = getRootClassName(request);
+ try {
+ ClassLoader classLoader = request.getDeploymentConfiguration()
+ .getClassLoader();
+ if (classLoader == null) {
+ classLoader = getClass().getClassLoader();
+ }
+ Class<? extends Root> rootClass = Class.forName(rootClassName,
+ true, classLoader).asSubclass(Root.class);
+ try {
+ Root root = rootClass.newInstance();
+ return root;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not instantiate root class "
+ + rootClassName, e);
+ }
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("Could not load root class "
+ + rootClassName, e);
+ }
+ }
+
+ /**
+ * Provides the name of the <code>Root</code> class that should be used for
+ * a request. The class must have an accessible no-args constructor.
+ * <p>
+ * The default implementation uses the {@value #ROOT_PARAMETER} parameter
+ * from web.xml.
+ * </p>
+ * <p>
+ * This method is mainly used by the default implementation of
+ * {@link #getRoot(WrappedRequest)}. If you override that method with your
+ * own functionality, the results of this method might not be used.
+ * </p>
+ *
+ * @param request
+ * the request for which a new root is required
+ * @return the name of the root class to use
+ *
+ * @since 7.0
+ */
+ protected String getRootClassName(WrappedRequest request) {
+ Object rootClassNameObj = properties.get(ROOT_PARAMETER);
+ if (rootClassNameObj instanceof String) {
+ return (String) rootClassNameObj;
+ } else {
+ throw new RuntimeException("No " + ROOT_PARAMETER
+ + " defined in web.xml");
+ }
+ }
+
+ /**
+ * Finds the theme to use for a specific root. If no specific theme is
+ * required, <code>null</code> is returned.
+ *
+ * TODO Tell what the default implementation does once it does something.
+ *
+ * @param root
+ * the root to get a theme for
+ * @return the name of the theme, or <code>null</code> if the default theme
+ * should be used
+ *
+ * @since 7.0
+ */
+ public String getThemeForRoot(Root root) {
+ Theme rootTheme = getAnnotationFor(root.getClass(), Theme.class);
+ if (rootTheme != null) {
+ return rootTheme.value();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Finds the widgetset to use for a specific root. If no specific widgetset
+ * is required, <code>null</code> is returned.
+ *
+ * TODO Tell what the default implementation does once it does something.
+ *
+ * @param root
+ * the root to get a widgetset for
+ * @return the name of the widgetset, or <code>null</code> if the default
+ * widgetset should be used
+ *
+ * @since 7.0
+ */
+ public String getWidgetsetForRoot(Root root) {
+ Widgetset rootWidgetset = getAnnotationFor(root.getClass(),
+ Widgetset.class);
+ if (rootWidgetset != null) {
+ return rootWidgetset.value();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Helper to get an annotation for a class. If the annotation is not present
+ * on the target class, it's superclasses and implemented interfaces are
+ * also searched for the annotation.
+ *
+ * @param type
+ * the target class from which the annotation should be found
+ * @param annotationType
+ * the annotation type to look for
+ * @return an annotation of the given type, or <code>null</code> if the
+ * annotation is not present on the class
+ */
+ private static <T extends Annotation> T getAnnotationFor(Class<?> type,
+ Class<T> annotationType) {
+ // Find from the class hierarchy
+ Class<?> currentType = type;
+ while (currentType != Object.class) {
+ T annotation = currentType.getAnnotation(annotationType);
+ if (annotation != null) {
+ return annotation;
+ } else {
+ currentType = currentType.getSuperclass();
+ }
+ }
+
+ // Find from an implemented interface
+ for (Class<?> iface : type.getInterfaces()) {
+ T annotation = iface.getAnnotation(annotationType);
+ if (annotation != null) {
+ return annotation;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 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 AbstractApplicationServlet}).
+ * <p>
+ * The request handlers are invoked in the revere order in which they were
+ * added to the application 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 application is invoked towards the end
+ * unless any previous handler has already produced a response.
+ * </p>
+ *
+ * @param request
+ * the wrapped 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
+ *
+ * @see #addRequestHandler(RequestHandler)
+ * @see RequestHandler
+ *
+ * @since 7.0
+ */
+ public boolean handleRequest(WrappedRequest request,
+ WrappedResponse response) throws IOException {
+ // Use a copy to avoid ConcurrentModificationException
+ for (RequestHandler handler : new ArrayList<RequestHandler>(
+ requestHandlers)) {
+ if (handler.handleRequest(this, request, response)) {
+ return true;
+ }
+ }
+ // If not handled
+ return false;
+ }
+
+ /**
+ * Adds a request handler to this application. Request handlers can be added
+ * to provide responses to requests that are not handled by the default
+ * functionality of the framework.
+ * <p>
+ * Handlers are called in reverse order of addition, so the most recently
+ * added handler will be called first.
+ * </p>
+ *
+ * @param handler
+ * the request handler to add
+ *
+ * @see #handleRequest(WrappedRequest, WrappedResponse)
+ * @see #removeRequestHandler(RequestHandler)
+ *
+ * @since 7.0
+ */
+ public void addRequestHandler(RequestHandler handler) {
+ requestHandlers.addFirst(handler);
+ }
+
+ /**
+ * Removes a request handler from the application.
+ *
+ * @param handler
+ * the request handler to remove
+ *
+ * @since 7.0
+ */
+ public void removeRequestHandler(RequestHandler handler) {
+ requestHandlers.remove(handler);
+ }
+
+ /**
+ * Gets the request handlers that are registered to the application. 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, with the iteration order
+ * according to the order they would be invoked
+ *
+ * @see #handleRequest(WrappedRequest, WrappedResponse)
+ * @see #addRequestHandler(RequestHandler)
+ * @see #removeRequestHandler(RequestHandler)
+ *
+ * @since 7.0
+ */
+ public Collection<RequestHandler> getRequestHandlers() {
+ return Collections.unmodifiableCollection(requestHandlers);
+ }
+
+ /**
+ * Find an application resource with a given key.
+ *
+ * @param key
+ * The key of the resource
+ * @return The application resource corresponding to the provided key, or
+ * <code>null</code> if no resource is registered for the key
+ *
+ * @since 7.0
+ */
+ public ApplicationResource getResource(String key) {
+ return keyResourceMap.get(key);
+ }
+
+ /**
+ * Thread local for keeping track of currently used application instance
+ *
+ * @since 7.0
+ */
+ private static final ThreadLocal<Application> currentApplication = new ThreadLocal<Application>();
+
+ private boolean rootPreserved = false;
+
+ /**
+ * Gets the currently used application. The current application is
+ * automatically defined when processing requests to the server. In other
+ * cases, (e.g. from background threads), the current application is not
+ * automatically defined.
+ *
+ * @return the current application instance if available, otherwise
+ * <code>null</code>
+ *
+ * @see #setCurrent(Application)
+ *
+ * @since 7.0
+ */
+ public static Application getCurrent() {
+ return currentApplication.get();
+ }
+
+ /**
+ * Sets the thread local for the current application. This method is used by
+ * the framework to set the current application whenever a new request is
+ * processed and it is cleared when the request has been processed.
+ * <p>
+ * The application developer can also use this method to define the current
+ * application outside the normal request handling, e.g. when initiating
+ * custom background threads.
+ * </p>
+ *
+ * @param application
+ *
+ * @see #getCurrent()
+ * @see ThreadLocal
+ *
+ * @since 7.0
+ */
+ public static void setCurrent(Application application) {
+ currentApplication.set(application);
+ }
+
+ /**
+ * Check whether this application is in production mode. If an application
+ * is in production mode, certain debugging facilities are not available.
+ *
+ * @return the status of the production mode flag
+ *
+ * @since 7.0
+ */
+ public boolean isProductionMode() {
+ return productionMode;
+ }
+
+ /**
+ * Finds the {@link Root} to which a particular request belongs. If the
+ * request originates from an existing Root, that root is returned. In other
+ * cases, the method attempts to create and initialize a new root and might
+ * throw a {@link RootRequiresMoreInformationException} if all required
+ * information is not available.
+ * <p>
+ * Please note that this method can also return a newly created
+ * <code>Root</code> which has not yet been initialized. You can use
+ * {@link #isRootInitPending(int)} with the root's id (
+ * {@link Root#getRootId()} to check whether the initialization is still
+ * pending.
+ * </p>
+ *
+ * @param request
+ * the request for which a root is desired
+ * @return a root belonging to the request
+ * @throws RootRequiresMoreInformationException
+ * if no existing root could be found and creating a new root
+ * requires additional information from the browser
+ *
+ * @see #getRoot(WrappedRequest)
+ * @see RootRequiresMoreInformationException
+ *
+ * @since 7.0
+ */
+ public Root getRootForRequest(WrappedRequest request)
+ throws RootRequiresMoreInformationException {
+ Root root = Root.getCurrent();
+ if (root != null) {
+ return root;
+ }
+ Integer rootId = getRootId(request);
+
+ synchronized (this) {
+ BrowserDetails browserDetails = request.getBrowserDetails();
+ boolean hasBrowserDetails = browserDetails != null
+ && browserDetails.getUriFragment() != null;
+
+ root = roots.get(rootId);
+
+ if (root == null && isRootPreserved()) {
+ // Check for a known root
+ if (!retainOnRefreshRoots.isEmpty()) {
+
+ Integer retainedRootId;
+ if (!hasBrowserDetails) {
+ throw new RootRequiresMoreInformationException();
+ } else {
+ String windowName = browserDetails.getWindowName();
+ retainedRootId = retainOnRefreshRoots.get(windowName);
+ }
+
+ if (retainedRootId != null) {
+ rootId = retainedRootId;
+ root = roots.get(rootId);
+ }
+ }
+ }
+
+ if (root == null) {
+ // Throws exception if root can not yet be created
+ root = getRoot(request);
+
+ // Initialize some fields for a newly created root
+ if (root.getApplication() == null) {
+ root.setApplication(this);
+ }
+ if (root.getRootId() < 0) {
+
+ if (rootId == null) {
+ // Get the next id if none defined
+ rootId = Integer.valueOf(nextRootId++);
+ }
+ root.setRootId(rootId.intValue());
+ roots.put(rootId, root);
+ }
+ }
+
+ // Set thread local here so it is available in init
+ Root.setCurrent(root);
+
+ if (!initedRoots.contains(rootId)) {
+ boolean initRequiresBrowserDetails = isRootPreserved()
+ || !root.getClass()
+ .isAnnotationPresent(EagerInit.class);
+ if (!initRequiresBrowserDetails || hasBrowserDetails) {
+ root.doInit(request);
+
+ // Remember that this root has been initialized
+ initedRoots.add(rootId);
+
+ // init() might turn on preserve so do this afterwards
+ if (isRootPreserved()) {
+ // Remember this root
+ String windowName = request.getBrowserDetails()
+ .getWindowName();
+ retainOnRefreshRoots.put(windowName, rootId);
+ }
+ }
+ }
+ } // end synchronized block
+
+ return root;
+ }
+
+ /**
+ * Internal helper to finds the root id for a request.
+ *
+ * @param request
+ * the request to get the root id for
+ * @return a root id, or <code>null</code> if no root id is defined
+ *
+ * @since 7.0
+ */
+ private static Integer getRootId(WrappedRequest request) {
+ if (request instanceof CombinedRequest) {
+ // Combined requests has the rootid parameter in the second request
+ CombinedRequest combinedRequest = (CombinedRequest) request;
+ request = combinedRequest.getSecondRequest();
+ }
+ String rootIdString = request
+ .getParameter(ApplicationConnection.ROOT_ID_PARAMETER);
+ Integer rootId = rootIdString == null ? null
+ : new Integer(rootIdString);
+ return rootId;
+ }
+
+ /**
+ * Sets whether the same Root state should be reused if the framework can
+ * detect that the application is opened in a browser window where it has
+ * previously been open. The framework attempts to discover this by checking
+ * the value of window.name in the browser.
+ * <p>
+ * NOTE that you should avoid turning this feature on/off on-the-fly when
+ * the UI is already shown, as it might not be retained as intended.
+ * </p>
+ *
+ * @param rootPreserved
+ * <code>true</code>if the same Root instance should be reused
+ * e.g. when the browser window is refreshed.
+ */
+ public void setRootPreserved(boolean rootPreserved) {
+ this.rootPreserved = rootPreserved;
+ if (!rootPreserved) {
+ retainOnRefreshRoots.clear();
+ }
+ }
+
+ /**
+ * Checks whether the same Root state should be reused if the framework can
+ * detect that the application is opened in a browser window where it has
+ * previously been open. The framework attempts to discover this by checking
+ * the value of window.name in the browser.
+ *
+ * @return <code>true</code>if the same Root instance should be reused e.g.
+ * when the browser window is refreshed.
+ */
+ public boolean isRootPreserved() {
+ return rootPreserved;
+ }
+
+ /**
+ * Checks whether there's a pending initialization for the root with the
+ * given id.
+ *
+ * @param rootId
+ * root id to check for
+ * @return <code>true</code> of the initialization is pending,
+ * <code>false</code> if the root id is not registered or if the
+ * root has already been initialized
+ *
+ * @see #getRootForRequest(WrappedRequest)
+ */
+ public boolean isRootInitPending(int rootId) {
+ return !initedRoots.contains(Integer.valueOf(rootId));
+ }
+
+ /**
+ * Gets all the roots of this application. This includes roots that have
+ * been requested but not yet initialized. Please note, that roots are not
+ * automatically removed e.g. if the browser window is closed and that there
+ * is no way to manually remove a root. Inactive roots will thus not be
+ * released for GC until the entire application is released when the session
+ * has timed out (unless there are dangling references). Improved support
+ * for releasing unused roots is planned for an upcoming alpha release of
+ * Vaadin 7.
+ *
+ * @return a collection of roots belonging to this application
+ *
+ * @since 7.0
+ */
+ public Collection<Root> getRoots() {
+ return Collections.unmodifiableCollection(roots.values());
+ }
+
+ private int connectorIdSequence = 0;
+
+ /**
+ * Generate an id for the given Connector. Connectors must not call this
+ * method more than once, the first time they need an id.
+ *
+ * @param connector
+ * A connector that has not yet been assigned an id.
+ * @return A new id for the connector
+ */
+ public String createConnectorId(ClientConnector connector) {
+ return String.valueOf(connectorIdSequence++);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(Application.class.getName());
+ }
+
+ /**
+ * Returns a Root with the given id.
+ * <p>
+ * This is meant for framework internal use.
+ * </p>
+ *
+ * @param rootId
+ * The root id
+ * @return The root with the given id or null if not found
+ */
+ public Root getRootById(int rootId) {
+ return roots.get(rootId);
+ }
+
+ public void addBootstrapListener(BootstrapListener listener) {
+ eventRouter.addListener(BootstrapFragmentResponse.class, listener,
+ BOOTSTRAP_FRAGMENT_METHOD);
+ eventRouter.addListener(BootstrapPageResponse.class, listener,
+ BOOTSTRAP_PAGE_METHOD);
+ }
+
+ public void removeBootstrapListener(BootstrapListener listener) {
+ eventRouter.removeListener(BootstrapFragmentResponse.class, listener,
+ BOOTSTRAP_FRAGMENT_METHOD);
+ eventRouter.removeListener(BootstrapPageResponse.class, listener,
+ BOOTSTRAP_PAGE_METHOD);
+ }
+
+ public void modifyBootstrapResponse(BootstrapResponse response) {
+ eventRouter.fireEvent(response);
+ }
+}
diff --git a/server/src/com/vaadin/RootRequiresMoreInformationException.java b/server/src/com/vaadin/RootRequiresMoreInformationException.java
new file mode 100644
index 0000000000..ed0fa41437
--- /dev/null
+++ b/server/src/com/vaadin/RootRequiresMoreInformationException.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin;
+
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedRequest.BrowserDetails;
+
+/**
+ * Exception that is thrown to indicate that creating or initializing the root
+ * requires information detailed from the web browser ({@link BrowserDetails})
+ * to be present.
+ *
+ * This exception may not be thrown if that information is already present in
+ * the current WrappedRequest.
+ *
+ * @see Application#getRoot(WrappedRequest)
+ * @see WrappedRequest#getBrowserDetails()
+ *
+ * @since 7.0
+ */
+public class RootRequiresMoreInformationException extends Exception {
+ // Nothing of interest here
+}
diff --git a/server/src/com/vaadin/Vaadin.gwt.xml b/server/src/com/vaadin/Vaadin.gwt.xml
new file mode 100644
index 0000000000..07d7c941e6
--- /dev/null
+++ b/server/src/com/vaadin/Vaadin.gwt.xml
@@ -0,0 +1,85 @@
+<module>
+ <!-- This GWT module inherits all Vaadin client side functionality modules.
+ This is the module you want to inherit in your client side project to be
+ able to use com.vaadin.* classes. -->
+
+ <!-- Hint for WidgetSetBuilder not to automatically update the file -->
+ <!-- WS Compiler: manually edited -->
+
+ <inherits name="com.google.gwt.user.User" />
+
+ <inherits name="com.google.gwt.http.HTTP" />
+
+ <inherits name="com.google.gwt.json.JSON" />
+
+ <inherits name="com.vaadin.terminal.gwt.VaadinBrowserSpecificOverrides" />
+
+ <source path="terminal/gwt/client" />
+ <source path="shared" />
+
+ <!-- Use own Scheduler implementation to be able to track if commands are
+ running -->
+ <replace-with class="com.vaadin.terminal.gwt.client.VSchedulerImpl">
+ <when-type-is class="com.google.gwt.core.client.impl.SchedulerImpl" />
+ </replace-with>
+
+ <!-- Generators for serializators for classes used in communication between
+ server and client -->
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.SerializerMapGenerator">
+ <when-type-is
+ class="com.vaadin.terminal.gwt.client.communication.SerializerMap" />
+ </generate-with>
+
+ <replace-with class="com.vaadin.terminal.gwt.client.VDebugConsole">
+ <when-type-is class="com.vaadin.terminal.gwt.client.Console" />
+ </replace-with>
+
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.EagerWidgetMapGenerator">
+ <when-type-is class="com.vaadin.terminal.gwt.client.WidgetMap" />
+ </generate-with>
+
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.AcceptCriteriaFactoryGenerator">
+ <when-type-is
+ class="com.vaadin.terminal.gwt.client.ui.dd.VAcceptCriterionFactory" />
+ </generate-with>
+
+ <!-- Generate client side proxies for client to server RPC interfaces -->
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyGenerator">
+ <when-type-assignable
+ class="com.vaadin.shared.communication.ServerRpc" />
+ </generate-with>
+
+ <!-- Generate client side proxies for client to server RPC interfaces -->
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.RpcProxyCreatorGenerator">
+ <when-type-assignable
+ class="com.vaadin.terminal.gwt.client.communication.RpcProxy.RpcProxyCreator" />
+ </generate-with>
+
+ <!-- Generate client side RPC manager for server to client RPC -->
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.GeneratedRpcMethodProviderGenerator">
+ <when-type-assignable
+ class="com.vaadin.terminal.gwt.client.communication.GeneratedRpcMethodProvider" />
+ </generate-with>
+
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorWidgetFactoryGenerator">
+ <when-type-assignable
+ class="com.vaadin.terminal.gwt.client.ui.ConnectorWidgetFactory" />
+ </generate-with>
+
+ <generate-with
+ class="com.vaadin.terminal.gwt.widgetsetutils.ConnectorStateFactoryGenerator">
+ <when-type-assignable
+ class="com.vaadin.terminal.gwt.client.ui.ConnectorStateFactory" />
+ </generate-with>
+
+ <!-- Use the new cross site linker to get a nocache.js without document.write -->
+ <add-linker name="xsiframe" />
+
+</module>
diff --git a/server/src/com/vaadin/Version.java b/server/src/com/vaadin/Version.java
new file mode 100644
index 0000000000..eb6d73e7e0
--- /dev/null
+++ b/server/src/com/vaadin/Version.java
@@ -0,0 +1,74 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin;
+
+import java.io.Serializable;
+
+public class Version implements Serializable {
+ /**
+ * The version number of this release. For example "6.2.0". Always in the
+ * format "major.minor.revision[.build]". The build part is optional. All of
+ * major, minor, revision must be integers.
+ */
+ private static final String VERSION;
+ /**
+ * Major version number. For example 6 in 6.2.0.
+ */
+ private static final int VERSION_MAJOR;
+
+ /**
+ * Minor version number. For example 2 in 6.2.0.
+ */
+ private static final int VERSION_MINOR;
+
+ /**
+ * Version revision number. For example 0 in 6.2.0.
+ */
+ private static final int VERSION_REVISION;
+
+ /**
+ * Build identifier. For example "nightly-20091123-c9963" in
+ * 6.2.0.nightly-20091123-c9963.
+ */
+ private static final String VERSION_BUILD;
+
+ /* Initialize version numbers from string replaced by build-script. */
+ static {
+ if ("@VERSION@".equals("@" + "VERSION" + "@")) {
+ VERSION = "9.9.9.INTERNAL-DEBUG-BUILD";
+ } else {
+ VERSION = "@VERSION@";
+ }
+ final String[] digits = VERSION.split("\\.", 4);
+ VERSION_MAJOR = Integer.parseInt(digits[0]);
+ VERSION_MINOR = Integer.parseInt(digits[1]);
+ VERSION_REVISION = Integer.parseInt(digits[2]);
+ if (digits.length == 4) {
+ VERSION_BUILD = digits[3];
+ } else {
+ VERSION_BUILD = "";
+ }
+ }
+
+ public static String getFullVersion() {
+ return VERSION;
+ }
+
+ public static int getMajorVersion() {
+ return VERSION_MAJOR;
+ }
+
+ public static int getMinorVersion() {
+ return VERSION_MINOR;
+ }
+
+ public static int getRevision() {
+ return VERSION_REVISION;
+ }
+
+ public static String getBuildIdentifier() {
+ return VERSION_BUILD;
+ }
+
+}
diff --git a/server/src/com/vaadin/annotations/AutoGenerated.java b/server/src/com/vaadin/annotations/AutoGenerated.java
new file mode 100644
index 0000000000..72c9b62a91
--- /dev/null
+++ b/server/src/com/vaadin/annotations/AutoGenerated.java
@@ -0,0 +1,18 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.annotations;
+
+/**
+ * Marker annotation for automatically generated code elements.
+ *
+ * These elements may be modified or removed by code generation.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.0
+ */
+public @interface AutoGenerated {
+
+}
diff --git a/server/src/com/vaadin/annotations/EagerInit.java b/server/src/com/vaadin/annotations/EagerInit.java
new file mode 100644
index 0000000000..c7c2702d2a
--- /dev/null
+++ b/server/src/com/vaadin/annotations/EagerInit.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+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.terminal.WrappedRequest;
+import com.vaadin.ui.Root;
+
+/**
+ * Indicates that the init method in a Root class can be called before full
+ * browser details ({@link WrappedRequest#getBrowserDetails()}) are available.
+ * This will make the UI appear more quickly, as ensuring the availability of
+ * this information typically requires an additional round trip to the client.
+ *
+ * @see Root#init(com.vaadin.terminal.WrappedRequest)
+ * @see WrappedRequest#getBrowserDetails()
+ *
+ * @since 7.0
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface EagerInit {
+ // No values
+}
diff --git a/server/src/com/vaadin/annotations/JavaScript.java b/server/src/com/vaadin/annotations/JavaScript.java
new file mode 100644
index 0000000000..357bcc3649
--- /dev/null
+++ b/server/src/com/vaadin/annotations/JavaScript.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.terminal.gwt.server.ClientConnector;
+
+/**
+ * If this annotation is present on a {@link ClientConnector} class, the
+ * framework ensures the referenced JavaScript files are loaded before the init
+ * method for the corresponding client-side connector is invoked.
+ * <p>
+ * Absolute URLs including protocol and host are used as is on the client-side.
+ * Relative urls are mapped to APP/CONNECTOR/[url] which are by default served
+ * from the classpath relative to the class where the annotation is defined.
+ * <p>
+ * Example: {@code @JavaScript( "http://host.com/file1.js", "file2.js"})} on the
+ * class com.example.MyConnector would load the file http://host.com/file1.js as
+ * is and file2.js from /com/example/file2.js on the server's classpath using
+ * the ClassLoader that was used to load com.example.MyConnector.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JavaScript {
+ /**
+ * JavaScript files to load before initializing the client-side connector.
+ *
+ * @return an array of JavaScript file urls
+ */
+ public String[] value();
+}
diff --git a/server/src/com/vaadin/annotations/StyleSheet.java b/server/src/com/vaadin/annotations/StyleSheet.java
new file mode 100644
index 0000000000..d082cb8d30
--- /dev/null
+++ b/server/src/com/vaadin/annotations/StyleSheet.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.terminal.gwt.server.ClientConnector;
+
+/**
+ * If this annotation is present on a {@link ClientConnector} class, the
+ * framework ensures the referenced style sheets are loaded before the init
+ * method for the corresponding client-side connector is invoked.
+ * <p>
+ * Example: {@code @StyleSheet( "http://host.com/file1.css", "file2.css"})} on
+ * the class com.example.MyConnector would load the file
+ * http://host.com/file1.css as is and file2.css from /com/example/file2.css on
+ * the server's classpath using the ClassLoader that was used to load
+ * com.example.MyConnector.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface StyleSheet {
+ /**
+ * Style sheets to load before initializing the client-side connector.
+ *
+ * @return an array of style sheet urls
+ */
+ public String[] value();
+}
diff --git a/server/src/com/vaadin/annotations/Theme.java b/server/src/com/vaadin/annotations/Theme.java
new file mode 100644
index 0000000000..7c62b07741
--- /dev/null
+++ b/server/src/com/vaadin/annotations/Theme.java
@@ -0,0 +1,24 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.ui.Root;
+
+/**
+ * Defines a specific theme for a {@link Root}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Theme {
+ /**
+ * @return simple name of the theme
+ */
+ public String value();
+}
diff --git a/server/src/com/vaadin/annotations/Widgetset.java b/server/src/com/vaadin/annotations/Widgetset.java
new file mode 100644
index 0000000000..99113f73f9
--- /dev/null
+++ b/server/src/com/vaadin/annotations/Widgetset.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+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.ui.Root;
+
+/**
+ * Defines a specific theme for a {@link Root}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Widgetset {
+ /**
+ * @return name of the widgetset
+ */
+ public String value();
+
+}
diff --git a/server/src/com/vaadin/annotations/package.html b/server/src/com/vaadin/annotations/package.html
new file mode 100644
index 0000000000..d789e9b5df
--- /dev/null
+++ b/server/src/com/vaadin/annotations/package.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<p>Contains annotations used in Vaadin. Note that some annotations
+are also found in other packages e.g., {@link com.vaadin.ui.ClientWidget}.</p>
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/data/Buffered.java b/server/src/com/vaadin/data/Buffered.java
new file mode 100644
index 0000000000..1387cb965b
--- /dev/null
+++ b/server/src/com/vaadin/data/Buffered.java
@@ -0,0 +1,280 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Validator.InvalidValueException;
+
+/**
+ * <p>
+ * Defines the interface to commit and discard changes to an object, supporting
+ * read-through and write-through modes.
+ * </p>
+ *
+ * <p>
+ * <i>Read-through mode</i> means that the value read from the buffered object
+ * is constantly up to date with the data source. <i>Write-through</i> mode
+ * means that all changes to the object are immediately updated to the data
+ * source.
+ * </p>
+ *
+ * <p>
+ * Since these modes are independent, their combinations may result in some
+ * behaviour that may sound surprising.
+ * </p>
+ *
+ * <p>
+ * For example, if a <code>Buffered</code> object is in read-through mode but
+ * not in write-through mode, the result is an object whose value is updated
+ * directly from the data source only if it's not locally modified. If the value
+ * is locally modified, retrieving the value from the object would result in a
+ * value that is different than the one stored in the data source, even though
+ * the object is in read-through mode.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Buffered extends Serializable {
+
+ /**
+ * Updates all changes since the previous commit to the data source. The
+ * value stored in the object will always be updated into the data source
+ * when <code>commit</code> is called.
+ *
+ * @throws SourceException
+ * if the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ * @throws InvalidValueException
+ * if the operation fails because validation is enabled and the
+ * values do not validate
+ */
+ public void commit() throws SourceException, InvalidValueException;
+
+ /**
+ * Discards all changes since last commit. The object updates its value from
+ * the data source.
+ *
+ * @throws SourceException
+ * if the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ */
+ public void discard() throws SourceException;
+
+ /**
+ * Tests if the object is in write-through mode. If the object is in
+ * write-through mode, all modifications to it will result in
+ * <code>commit</code> being called after the modification.
+ *
+ * @return <code>true</code> if the object is in write-through mode,
+ * <code>false</code> if it's not.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public boolean isWriteThrough();
+
+ /**
+ * Sets the object's write-through mode to the specified status. When
+ * switching the write-through mode on, the <code>commit</code> operation
+ * will be performed.
+ *
+ * @param writeThrough
+ * Boolean value to indicate if the object should be in
+ * write-through mode after the call.
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source.
+ * @throws InvalidValueException
+ * If the implicit commit operation fails because of a
+ * validation error.
+ *
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public void setWriteThrough(boolean writeThrough) throws SourceException,
+ InvalidValueException;
+
+ /**
+ * Tests if the object is in read-through mode. If the object is in
+ * read-through mode, retrieving its value will result in the value being
+ * first updated from the data source to the object.
+ * <p>
+ * The only exception to this rule is that when the object is not in
+ * write-through mode and it's buffer contains a modified value, the value
+ * retrieved from the object will be the locally modified value in the
+ * buffer which may differ from the value in the data source.
+ * </p>
+ *
+ * @return <code>true</code> if the object is in read-through mode,
+ * <code>false</code> if it's not.
+ * @deprecated Use {@link #isBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public boolean isReadThrough();
+
+ /**
+ * Sets the object's read-through mode to the specified status. When
+ * switching read-through mode on, the object's value is updated from the
+ * data source.
+ *
+ * @param readThrough
+ * Boolean value to indicate if the object should be in
+ * read-through mode after the call.
+ *
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Deprecated
+ public void setReadThrough(boolean readThrough) throws SourceException;
+
+ /**
+ * Sets the object's buffered mode to the specified status.
+ * <p>
+ * When the object is in buffered mode, an internal buffer will be used to
+ * store changes until {@link #commit()} is called. Calling
+ * {@link #discard()} will revert the internal buffer to the value of the
+ * data source.
+ * </p>
+ * <p>
+ * This is an easier way to use {@link #setReadThrough(boolean)} and
+ * {@link #setWriteThrough(boolean)} and not as error prone. Changing
+ * buffered mode will change both the read through and write through state
+ * of the object.
+ * </p>
+ * <p>
+ * Mixing calls to {@link #setBuffered(boolean)}/{@link #isBuffered()} and
+ * {@link #setReadThrough(boolean)}/{@link #isReadThrough()} or
+ * {@link #setWriteThrough(boolean)}/{@link #isWriteThrough()} is generally
+ * a bad idea.
+ * </p>
+ *
+ * @param buffered
+ * true if buffered mode should be turned on, false otherwise
+ * @since 7.0
+ */
+ public void setBuffered(boolean buffered);
+
+ /**
+ * Checks the buffered mode of this Object.
+ * <p>
+ * This method only returns true if both read and write buffering is used.
+ * </p>
+ *
+ * @return true if buffered mode is on, false otherwise
+ * @since 7.0
+ */
+ public boolean isBuffered();
+
+ /**
+ * Tests if the value stored in the object has been modified since it was
+ * last updated from the data source.
+ *
+ * @return <code>true</code> if the value in the object has been modified
+ * since the last data source update, <code>false</code> if not.
+ */
+ public boolean isModified();
+
+ /**
+ * An exception that signals that one or more exceptions occurred while a
+ * buffered object tried to access its data source or if there is a problem
+ * in processing a data source.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class SourceException extends RuntimeException implements
+ Serializable {
+
+ /** Source class implementing the buffered interface */
+ private final Buffered source;
+
+ /** Original cause of the source exception */
+ private Throwable[] causes = {};
+
+ /**
+ * Creates a source exception that does not include a cause.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ */
+ public SourceException(Buffered source) {
+ this.source = source;
+ }
+
+ /**
+ * Creates a source exception from a cause exception.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ * @param cause
+ * the original cause for this exception.
+ */
+ public SourceException(Buffered source, Throwable cause) {
+ this.source = source;
+ causes = new Throwable[] { cause };
+ }
+
+ /**
+ * Creates a source exception from multiple causes.
+ *
+ * @param source
+ * the source object implementing the Buffered interface.
+ * @param causes
+ * the original causes for this exception.
+ */
+ public SourceException(Buffered source, Throwable[] causes) {
+ this.source = source;
+ this.causes = causes;
+ }
+
+ /**
+ * Gets the cause of the exception.
+ *
+ * @return The (first) cause for the exception, null if no cause.
+ */
+ @Override
+ public final Throwable getCause() {
+ if (causes.length == 0) {
+ return null;
+ }
+ return causes[0];
+ }
+
+ /**
+ * Gets all the causes for this exception.
+ *
+ * @return throwables that caused this exception
+ */
+ public final Throwable[] getCauses() {
+ return causes;
+ }
+
+ /**
+ * Gets a source of the exception.
+ *
+ * @return the Buffered object which generated this exception.
+ */
+ public Buffered getSource() {
+ return source;
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/BufferedValidatable.java b/server/src/com/vaadin/data/BufferedValidatable.java
new file mode 100644
index 0000000000..ce1d44fce6
--- /dev/null
+++ b/server/src/com/vaadin/data/BufferedValidatable.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * This interface defines the combination of <code>Validatable</code> and
+ * <code>Buffered</code> interfaces. The combination of the interfaces defines
+ * if the invalid data is committed to datasource.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface BufferedValidatable extends Buffered, Validatable,
+ Serializable {
+
+ /**
+ * Tests if the invalid data is committed to datasource. The default is
+ * <code>false</code>.
+ */
+ public boolean isInvalidCommitted();
+
+ /**
+ * Sets if the invalid data should be committed to datasource. The default
+ * is <code>false</code>.
+ */
+ public void setInvalidCommitted(boolean isCommitted);
+}
diff --git a/server/src/com/vaadin/data/Collapsible.java b/server/src/com/vaadin/data/Collapsible.java
new file mode 100644
index 0000000000..06c96b7ea7
--- /dev/null
+++ b/server/src/com/vaadin/data/Collapsible.java
@@ -0,0 +1,68 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data;
+
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.Ordered;
+
+/**
+ * Container needed by large lazy loading hierarchies displayed e.g. in
+ * TreeTable.
+ * <p>
+ * Container of this type gets notified when a subtree is opened/closed in a
+ * component displaying its content. This allows container to lazy load subtrees
+ * and release memory when a sub-tree is no longer displayed.
+ * <p>
+ * Methods from {@link Container.Ordered} (and from {@linkContainer.Indexed} if
+ * implemented) are expected to work as in "preorder" of the currently visible
+ * hierarchy. This means for example that the return value of size method
+ * changes when subtree is collapsed/expanded. In other words items in collapsed
+ * sub trees should be "ignored" by container when the container is accessed
+ * with methods introduced in {@link Container.Ordered} or
+ * {@linkContainer.Indexed}. From the accessors point of view, items in
+ * collapsed subtrees don't exist.
+ * <p>
+ *
+ */
+public interface Collapsible extends Hierarchical, Ordered {
+
+ /**
+ * <p>
+ * Collapsing the {@link Item} indicated by <code>itemId</code> hides all
+ * children, and their respective children, from the {@link Container}.
+ * </p>
+ *
+ * <p>
+ * If called on a leaf {@link Item}, this method does nothing.
+ * </p>
+ *
+ * @param itemId
+ * the identifier of the collapsed {@link Item}
+ * @param collapsed
+ * <code>true</code> if you want to collapse the children below
+ * this {@link Item}. <code>false</code> if you want to
+ * uncollapse the children.
+ */
+ public void setCollapsed(Object itemId, boolean collapsed);
+
+ /**
+ * <p>
+ * Checks whether the {@link Item}, identified by <code>itemId</code> is
+ * collapsed or not.
+ * </p>
+ *
+ * <p>
+ * If an {@link Item} is "collapsed" its children are not included in
+ * methods used to list Items in this container.
+ * </p>
+ *
+ * @param itemId
+ * The {@link Item}'s identifier that is to be checked.
+ * @return <code>true</code> iff the {@link Item} identified by
+ * <code>itemId</code> is currently collapsed, otherwise
+ * <code>false</code>.
+ */
+ public boolean isCollapsed(Object itemId);
+
+}
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java
new file mode 100644
index 0000000000..f4c0ed9794
--- /dev/null
+++ b/server/src/com/vaadin/data/Container.java
@@ -0,0 +1,1105 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * <p>
+ * A specialized set of identified Items. Basically the Container is a set of
+ * {@link Item}s, but it imposes certain constraints on its contents. These
+ * constraints state the following:
+ * </p>
+ *
+ * <ul>
+ * <li>All Items in the Container must have the same number of Properties.
+ * <li>All Items in the Container must have the same Property ID's (see
+ * {@link Item#getItemPropertyIds()}).
+ * <li>All Properties in the Items corresponding to the same Property ID must
+ * have the same data type.
+ * <li>All Items within a container are uniquely identified by their non-null
+ * IDs.
+ * </ul>
+ *
+ * <p>
+ * The Container can be visualized as a representation of a relational database
+ * table. Each Item in the Container represents a row in the table, and all
+ * cells in a column (identified by a Property ID) have the same data type. Note
+ * that as with the cells in a database table, no Property in a Container may be
+ * empty, though they may contain <code>null</code> values.
+ * </p>
+ *
+ * <p>
+ * Note that though uniquely identified, the Items in a Container are not
+ * necessarily {@link Container.Ordered ordered} or {@link Container.Indexed
+ * indexed}.
+ * </p>
+ *
+ * <p>
+ * Containers can derive Item ID's from the item properties or use other,
+ * container specific or user specified identifiers.
+ * </p>
+ *
+ * <p>
+ * If a container is {@link Filterable filtered} or {@link Sortable sorted},
+ * most of the the methods of the container interface and its subinterfaces
+ * (container size, {@link #containsId(Object)}, iteration and indices etc.)
+ * relate to the filtered and sorted view, not to the full container contents.
+ * See individual method javadoc for exceptions to this (adding and removing
+ * items).
+ * </p>
+ *
+ * <p>
+ * <img src=doc-files/Container_full.gif>
+ * </p>
+ *
+ * <p>
+ * The Container interface is split to several subinterfaces so that a class can
+ * implement only the ones it needs.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Container extends Serializable {
+
+ /**
+ * Gets the {@link Item} with the given Item ID from the Container. If the
+ * Container does not contain the requested Item, <code>null</code> is
+ * returned.
+ *
+ * Containers should not return Items that are filtered out.
+ *
+ * @param itemId
+ * ID of the {@link Item} to retrieve
+ * @return the {@link Item} with the given ID or <code>null</code> if the
+ * Item is not found in the Container
+ */
+ public Item getItem(Object itemId);
+
+ /**
+ * Gets the ID's of all Properties stored in the Container. The ID's cannot
+ * be modified through the returned collection.
+ *
+ * @return unmodifiable collection of Property IDs
+ */
+ public Collection<?> getContainerPropertyIds();
+
+ /**
+ * Gets the ID's of all visible (after filtering and sorting) Items stored
+ * in the Container. The ID's cannot be modified through the returned
+ * collection.
+ *
+ * If the container is {@link Ordered}, the collection returned by this
+ * method should follow that order. If the container is {@link Sortable},
+ * the items should be in the sorted order.
+ *
+ * Calling this method for large lazy containers can be an expensive
+ * operation and should be avoided when practical.
+ *
+ * @return unmodifiable collection of Item IDs
+ */
+ public Collection<?> getItemIds();
+
+ /**
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container. If the Container does not contain the item or it is filtered
+ * out, or the Container does not have the Property, <code>null</code> is
+ * returned.
+ *
+ * @param itemId
+ * ID of the visible Item which contains the Property
+ * @param propertyId
+ * ID of the Property to retrieve
+ * @return Property with the given ID or <code>null</code>
+ */
+ public Property<?> getContainerProperty(Object itemId, Object propertyId);
+
+ /**
+ * Gets the data type of all Properties identified by the given Property ID.
+ *
+ * @param propertyId
+ * ID identifying the Properties
+ * @return data type of the Properties
+ */
+ public Class<?> getType(Object propertyId);
+
+ /**
+ * Gets the number of visible Items in the Container.
+ *
+ * Filtering can hide items so that they will not be visible through the
+ * container API.
+ *
+ * @return number of Items in the Container
+ */
+ public int size();
+
+ /**
+ * Tests if the Container contains the specified Item.
+ *
+ * Filtering can hide items so that they will not be visible through the
+ * container API, and this method should respect visibility of items (i.e.
+ * only indicate visible items as being in the container) if feasible for
+ * the container.
+ *
+ * @param itemId
+ * ID the of Item to be tested
+ * @return boolean indicating if the Container holds the specified Item
+ */
+ public boolean containsId(Object itemId);
+
+ /**
+ * Creates a new Item with the given ID in the Container.
+ *
+ * <p>
+ * The new Item is returned, and it is ready to have its Properties
+ * modified. Returns <code>null</code> if the operation fails or the
+ * Container already contains a Item with the given ID.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to be created
+ * @return Created new Item, or <code>null</code> in case of a failure
+ * @throws UnsupportedOperationException
+ * if adding an item with an explicit item ID is not supported
+ * by the container
+ */
+ public Item addItem(Object itemId) throws UnsupportedOperationException;
+
+ /**
+ * Creates a new Item into the Container, and assign it an automatic ID.
+ *
+ * <p>
+ * The new ID is returned, or <code>null</code> if the operation fails.
+ * After a successful call you can use the {@link #getItem(Object ItemId)
+ * <code>getItem</code>}method to fetch the Item.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @return ID of the newly created Item, or <code>null</code> in case of a
+ * failure
+ * @throws UnsupportedOperationException
+ * if adding an item without an explicit item ID is not
+ * supported by the container
+ */
+ public Object addItem() throws UnsupportedOperationException;
+
+ /**
+ * Removes the Item identified by <code>ItemId</code> from the Container.
+ *
+ * <p>
+ * Containers that support filtering should also allow removing an item that
+ * is currently filtered out.
+ * </p>
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to remove
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing individual items
+ */
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Adds a new Property to all Items in the Container. The Property ID, data
+ * type and default value of the new Property are given as parameters.
+ *
+ * This functionality is optional.
+ *
+ * @param propertyId
+ * ID of the Property
+ * @param type
+ * Data type of the new Property
+ * @param defaultValue
+ * The value all created Properties are initialized to
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support explicitly adding container
+ * properties
+ */
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException;
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ * Note that the Property will be removed from all Items in the Container.
+ *
+ * This functionality is optional.
+ *
+ * @param propertyId
+ * ID of the Property to remove
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing container
+ * properties
+ */
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * <p>
+ * Note that Property ID and type information is preserved. This
+ * functionality is optional.
+ * </p>
+ *
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the container does not support removing all items
+ */
+ public boolean removeAllItems() throws UnsupportedOperationException;
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be traversed in
+ * order.
+ *
+ * <p>
+ * If the container is filtered or sorted, the traversal applies to the
+ * filtered and sorted view.
+ * </p>
+ * <p>
+ * The <code>addItemAfter()</code> methods should apply filters to the added
+ * item after inserting it, possibly hiding it immediately. If the container
+ * is being sorted, they may add items at the correct sorted position
+ * instead of the given position. See also {@link Filterable} and
+ * {@link Sortable} for more information.
+ * </p>
+ */
+ public interface Ordered extends Container {
+
+ /**
+ * Gets the ID of the Item following the Item that corresponds to
+ * <code>itemId</code>. If the given Item is the last or not found in
+ * the Container, <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of a visible Item in the Container
+ * @return ID of the next visible Item or <code>null</code>
+ */
+ public Object nextItemId(Object itemId);
+
+ /**
+ * Gets the ID of the Item preceding the Item that corresponds to
+ * <code>itemId</code>. If the given Item is the first or not found in
+ * the Container, <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of a visible Item in the Container
+ * @return ID of the previous visible Item or <code>null</code>
+ */
+ public Object prevItemId(Object itemId);
+
+ /**
+ * Gets the ID of the first Item in the Container.
+ *
+ * @return ID of the first visible Item in the Container
+ */
+ public Object firstItemId();
+
+ /**
+ * Gets the ID of the last Item in the Container..
+ *
+ * @return ID of the last visible Item in the Container
+ */
+ public Object lastItemId();
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the first
+ * Item in the Container.
+ *
+ * @param itemId
+ * ID of an Item in the Container
+ * @return <code>true</code> if the Item is first visible item in the
+ * Container, <code>false</code> if not
+ */
+ public boolean isFirstId(Object itemId);
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the last Item
+ * in the Container.
+ *
+ * @return <code>true</code> if the Item is last visible item in the
+ * Container, <code>false</code> if not
+ */
+ public boolean isLastId(Object itemId);
+
+ /**
+ * Adds a new item after the given item.
+ * <p>
+ * Adding an item after null item adds the item as first item of the
+ * ordered container.
+ * </p>
+ *
+ * @see Ordered Ordered: adding items in filtered or sorted containers
+ *
+ * @param previousItemId
+ * Id of the visible item in ordered container after which to
+ * insert the new item.
+ * @return item id the the created new item or null if the operation
+ * fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Adds a new item after the given item.
+ * <p>
+ * Adding an item after null item adds the item as first item of the
+ * ordered container.
+ * </p>
+ *
+ * @see Ordered Ordered: adding items in filtered or sorted containers
+ *
+ * @param previousItemId
+ * Id of the visible item in ordered container after which to
+ * insert the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new item or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException;
+
+ }
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be sorted.
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is sorted, all
+ * relevant operations of these interfaces should only use the filtered and
+ * sorted contents and the filtered indices to the container. Indices or
+ * item identifiers in the public API refer to the visible view unless
+ * otherwise stated. However, the <code>addItem*()</code> methods may add
+ * items that will be filtered out after addition or moved to another
+ * position based on sorting.
+ * </p>
+ * <p>
+ * How sorting is performed when a {@link Hierarchical} container implements
+ * {@link Sortable} is implementation specific and should be documented in
+ * the implementing class. However, the recommended approach is sorting the
+ * roots and the sets of children of each item separately.
+ * </p>
+ * <p>
+ * Depending on the container type, sorting a container may permanently
+ * change the internal order of items in the container.
+ * </p>
+ */
+ public interface Sortable extends Ordered {
+
+ /**
+ * Sort method.
+ *
+ * Sorts the container items.
+ *
+ * Sorting a container can irreversibly change the order of its items or
+ * only change the order temporarily, depending on the container.
+ *
+ * @param propertyId
+ * Array of container property IDs, whose values are used to
+ * sort the items in container as primary, secondary, ...
+ * sorting criterion. All of the item IDs must be in the
+ * collection returned by
+ * {@link #getSortableContainerPropertyIds()}
+ * @param ascending
+ * Array of sorting order flags corresponding to each
+ * property ID used in sorting. If this array is shorter than
+ * propertyId array, ascending order is assumed for items
+ * where the order is not specified. Use <code>true</code> to
+ * sort in ascending order, <code>false</code> to use
+ * descending order.
+ */
+ void sort(Object[] propertyId, boolean[] ascending);
+
+ /**
+ * Gets the container property IDs which can be used to sort the items.
+ *
+ * @return the IDs of the properties that can be used for sorting the
+ * container
+ */
+ Collection<?> getSortableContainerPropertyIds();
+
+ }
+
+ /**
+ * Interface for Container classes whose {@link Item}s can be accessed by
+ * their position in the container.
+ * <p>
+ * If the container is filtered or sorted, all indices refer to the filtered
+ * and sorted view. However, the <code>addItemAt()</code> methods may add
+ * items that will be filtered out after addition or moved to another
+ * position based on sorting.
+ * </p>
+ */
+ public interface Indexed extends Ordered {
+
+ /**
+ * Gets the index of the Item corresponding to the itemId. The following
+ * is <code>true</code> for the returned index: 0 <= index < size(), or
+ * index = -1 if there is no visible item with that id in the container.
+ *
+ * @param itemId
+ * ID of an Item in the Container
+ * @return index of the Item, or -1 if (the filtered and sorted view of)
+ * the Container does not include the Item
+ */
+ public int indexOfId(Object itemId);
+
+ /**
+ * Gets the ID of an Item by an index number.
+ *
+ * @param index
+ * Index of the requested id in (the filtered and sorted view
+ * of) the Container
+ * @return ID of the Item in the given index
+ */
+ public Object getIdByIndex(int index);
+
+ /**
+ * Adds a new item at given index (in the filtered view).
+ * <p>
+ * The indices of the item currently in the given position and all the
+ * following items are incremented.
+ * </p>
+ * <p>
+ * This method should apply filters to the added item after inserting
+ * it, possibly hiding it immediately. If the container is being sorted,
+ * the item may be added at the correct sorted position instead of the
+ * given position. See {@link Indexed}, {@link Ordered},
+ * {@link Filterable} and {@link Sortable} for more information.
+ * </p>
+ *
+ * @param index
+ * Index (in the filtered and sorted view) to add the new
+ * item.
+ * @return item id of the created item or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Object addItemAt(int index) throws UnsupportedOperationException;
+
+ /**
+ * Adds a new item at given index (in the filtered view).
+ * <p>
+ * The indexes of the item currently in the given position and all the
+ * following items are incremented.
+ * </p>
+ * <p>
+ * This method should apply filters to the added item after inserting
+ * it, possibly hiding it immediately. If the container is being sorted,
+ * the item may be added at the correct sorted position instead of the
+ * given position. See {@link Indexed}, {@link Filterable} and
+ * {@link Sortable} for more information.
+ * </p>
+ *
+ * @param index
+ * Index (in the filtered and sorted view) at which to add
+ * the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new {@link Item} or null if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported by the container
+ */
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException;
+
+ }
+
+ /**
+ * <p>
+ * Interface for <code>Container</code> classes whose Items can be arranged
+ * hierarchically. This means that the Items in the container belong in a
+ * tree-like structure, with the following quirks:
+ * </p>
+ *
+ * <ul>
+ * <li>The Item structure may have more than one root elements
+ * <li>The Items in the hierarchy can be declared explicitly to be able or
+ * unable to have children.
+ * </ul>
+ */
+ public interface Hierarchical extends Container {
+
+ /**
+ * Gets the IDs of all Items that are children of the specified Item.
+ * The returned collection is unmodifiable.
+ *
+ * @param itemId
+ * ID of the Item whose children the caller is interested in
+ * @return An unmodifiable {@link java.util.Collection collection}
+ * containing the IDs of all other Items that are children in
+ * the container hierarchy
+ */
+ public Collection<?> getChildren(Object itemId);
+
+ /**
+ * Gets the ID of the parent Item of the specified Item.
+ *
+ * @param itemId
+ * ID of the Item whose parent the caller wishes to find out.
+ * @return the ID of the parent Item. Will be <code>null</code> if the
+ * specified Item is a root element.
+ */
+ public Object getParent(Object itemId);
+
+ /**
+ * Gets the IDs of all Items in the container that don't have a parent.
+ * Such items are called <code>root</code> Items. The returned
+ * collection is unmodifiable.
+ *
+ * @return An unmodifiable {@link java.util.Collection collection}
+ * containing IDs of all root elements of the container
+ */
+ public Collection<?> rootItemIds();
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be
+ * able to have children. (
+ * <code>{@link #areChildrenAllowed(Object)} == true</code> ). It is
+ * also possible to detach a node from the hierarchy (and thus make it
+ * root) by setting the parent <code>null</code>.
+ * </p>
+ *
+ * <p>
+ * This operation is optional.
+ * </p>
+ *
+ * @param itemId
+ * ID of the item to be set as the child of the Item
+ * identified with <code>newParentId</code>
+ * @param newParentId
+ * ID of the Item that's to be the new parent of the Item
+ * identified with <code>itemId</code>
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException;
+
+ /**
+ * Tests if the Item with given ID can have children.
+ *
+ * @param itemId
+ * ID of the Item in the container whose child capability is
+ * to be tested
+ * @return <code>true</code> if the specified Item exists in the
+ * Container and it can have children, <code>false</code> if
+ * it's not found from the container or it can't have children.
+ */
+ public boolean areChildrenAllowed(Object itemId);
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item
+ * identified with <code>itemId</code> already has children and
+ * <code>{@link #areChildrenAllowed(Object)}</code> is false this method
+ * fails and <code>false</code> is returned.
+ * </p>
+ * <p>
+ * The children must be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)}or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * <p>
+ * This operation is optional. If it is not implemented, the method
+ * always returns <code>false</code>.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item in the container whose child capability is
+ * to be set
+ * @param areChildrenAllowed
+ * boolean value specifying if the Item can have children or
+ * not
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ public boolean setChildrenAllowed(Object itemId,
+ boolean areChildrenAllowed)
+ throws UnsupportedOperationException;
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> is a root Item.
+ * The hierarchical container can have more than one root and must have
+ * at least one unless it is empty. The {@link #getParent(Object itemId)}
+ * method always returns <code>null</code> for root Items.
+ *
+ * @param itemId
+ * ID of the Item whose root status is to be tested
+ * @return <code>true</code> if the specified Item is a root,
+ * <code>false</code> if not
+ */
+ public boolean isRoot(Object itemId);
+
+ /**
+ * <p>
+ * Tests if the Item specified with <code>itemId</code> has child Items
+ * or if it is a leaf. The {@link #getChildren(Object itemId)} method
+ * always returns <code>null</code> for leaf Items.
+ * </p>
+ *
+ * <p>
+ * Note that being a leaf does not imply whether or not an Item is
+ * allowed to have children.
+ * </p>
+ * .
+ *
+ * @param itemId
+ * ID of the Item to be tested
+ * @return <code>true</code> if the specified Item has children,
+ * <code>false</code> if not (is a leaf)
+ */
+ public boolean hasChildren(Object itemId);
+
+ /**
+ * <p>
+ * Removes the Item identified by <code>ItemId</code> from the
+ * Container.
+ * </p>
+ *
+ * <p>
+ * Note that this does not remove any children the item might have.
+ * </p>
+ *
+ * @param itemId
+ * ID of the Item to remove
+ * @return <code>true</code> if the operation succeeded,
+ * <code>false</code> if not
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException;
+ }
+
+ /**
+ * Interface that is implemented by containers which allow reducing their
+ * visible contents based on a set of filters. This interface has been
+ * renamed from {@link Filterable}, and implementing the new
+ * {@link Filterable} instead of or in addition to {@link SimpleFilterable}
+ * is recommended. This interface might be removed in future Vaadin
+ * versions.
+ * <p>
+ * When a set of filters are set, only items that match all the filters are
+ * included in the visible contents of the container. Still new items that
+ * do not match filters can be added to the container. Multiple filters can
+ * be added and the container remembers the state of the filters. When
+ * multiple filters are added, all filters must match for an item to be
+ * visible in the container.
+ * </p>
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is filtered, all
+ * operations of these interfaces should only use the filtered contents and
+ * the filtered indices to the container.
+ * </p>
+ * <p>
+ * How filtering is performed when a {@link Hierarchical} container
+ * implements {@link SimpleFilterable} is implementation specific and should
+ * be documented in the implementing class.
+ * </p>
+ * <p>
+ * Adding items (if supported) to a filtered {@link Ordered} or
+ * {@link Indexed} container should insert them immediately after the
+ * indicated visible item. The unfiltered position of items added at index
+ * 0, at index {@link com.vaadin.data.Container#size()} or at an undefined
+ * position is up to the implementation.
+ * </p>
+ * <p>
+ * The functionality of SimpleFilterable can be implemented using the
+ * {@link Filterable} API and {@link SimpleStringFilter}.
+ * </p>
+ *
+ * @since 5.0 (renamed from Filterable to SimpleFilterable in 6.6)
+ */
+ public interface SimpleFilterable extends Container, Serializable {
+
+ /**
+ * Add a filter for given property.
+ *
+ * The API {@link Filterable#addContainerFilter(Filter)} is recommended
+ * instead of this method. A {@link SimpleStringFilter} can be used with
+ * the new API to implement the old string filtering functionality.
+ *
+ * The filter accepts items for which toString() of the value of the
+ * given property contains or starts with given filterString. Other
+ * items are not visible in the container when filtered.
+ *
+ * If a container has multiple filters, only items accepted by all
+ * filters are visible.
+ *
+ * @param propertyId
+ * Property for which the filter is applied to.
+ * @param filterString
+ * String that must match the value of the property
+ * @param ignoreCase
+ * Determine if the casing can be ignored when comparing
+ * strings.
+ * @param onlyMatchPrefix
+ * Only match prefixes; no other matches are included.
+ */
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix);
+
+ /**
+ * Remove all filters from all properties.
+ */
+ public void removeAllContainerFilters();
+
+ /**
+ * Remove all filters from the given property.
+ *
+ * @param propertyId
+ * for which to remove filters
+ */
+ public void removeContainerFilters(Object propertyId);
+ }
+
+ /**
+ * Filter interface for container filtering.
+ *
+ * If a filter does not support in-memory filtering,
+ * {@link #passesFilter(Item)} should throw
+ * {@link UnsupportedOperationException}.
+ *
+ * Lazy containers must be able to map filters to their internal
+ * representation (e.g. SQL or JPA 2.0 Criteria).
+ *
+ * An {@link UnsupportedFilterException} can be thrown by the container if a
+ * particular filter is not supported by the container.
+ *
+ * An {@link Filter} should implement {@link #equals(Object)} and
+ * {@link #hashCode()} correctly to avoid duplicate filter registrations
+ * etc.
+ *
+ * @see Filterable
+ *
+ * @since 6.6
+ */
+ public interface Filter extends Serializable {
+
+ /**
+ * Check if an item passes the filter (in-memory filtering).
+ *
+ * @param itemId
+ * identifier of the item being filtered; may be null when
+ * the item is being added to the container
+ * @param item
+ * the item being filtered
+ * @return true if the item is accepted by this filter
+ * @throws UnsupportedOperationException
+ * if the filter cannot be used for in-memory filtering
+ */
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException;
+
+ /**
+ * Check if a change in the value of a property can affect the filtering
+ * result. May always return true, at the cost of performance.
+ *
+ * If the filter cannot determine whether it may depend on the property
+ * or not, should return true.
+ *
+ * @param propertyId
+ * @return true if the filtering result may/does change based on changes
+ * to the property identified by propertyId
+ */
+ public boolean appliesToProperty(Object propertyId);
+
+ }
+
+ /**
+ * Interface that is implemented by containers which allow reducing their
+ * visible contents based on a set of filters.
+ * <p>
+ * When a set of filters are set, only items that match all the filters are
+ * included in the visible contents of the container. Still new items that
+ * do not match filters can be added to the container. Multiple filters can
+ * be added and the container remembers the state of the filters. When
+ * multiple filters are added, all filters must match for an item to be
+ * visible in the container.
+ * </p>
+ * <p>
+ * When an {@link Ordered} or {@link Indexed} container is filtered, all
+ * operations of these interfaces should only use the filtered and sorted
+ * contents and the filtered indices to the container. Indices or item
+ * identifiers in the public API refer to the visible view unless otherwise
+ * stated. However, the <code>addItem*()</code> methods may add items that
+ * will be filtered out after addition or moved to another position based on
+ * sorting.
+ * </p>
+ * <p>
+ * How filtering is performed when a {@link Hierarchical} container
+ * implements {@link Filterable} is implementation specific and should be
+ * documented in the implementing class.
+ * </p>
+ * <p>
+ * Adding items (if supported) to a filtered {@link Ordered} or
+ * {@link Indexed} container should insert them immediately after the
+ * indicated visible item. However, the unfiltered position of items added
+ * at index 0, at index {@link com.vaadin.data.Container#size()} or at an
+ * undefined position is up to the implementation.
+ * </p>
+ *
+ * <p>
+ * This API replaces the old Filterable interface, renamed to
+ * {@link SimpleFilterable} in Vaadin 6.6.
+ * </p>
+ *
+ * @since 6.6
+ */
+ public interface Filterable extends Container, Serializable {
+ /**
+ * Adds a filter for the container.
+ *
+ * If a container has multiple filters, only items accepted by all
+ * filters are visible.
+ *
+ * @throws UnsupportedFilterException
+ * if the filter is not supported by the container
+ */
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException;
+
+ /**
+ * Removes a filter from the container.
+ *
+ * This requires that the equals() method considers the filters as
+ * equivalent (same instance or properly implemented equals() method).
+ */
+ public void removeContainerFilter(Filter filter);
+
+ /**
+ * Remove all active filters from the container.
+ */
+ public void removeAllContainerFilters();
+
+ }
+
+ /**
+ * Interface implemented by viewer classes capable of using a Container as a
+ * data source.
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * The new data source Item
+ */
+ public void setContainerDataSource(Container newDataSource);
+
+ /**
+ * Gets the Container serving as the data source of the viewer.
+ *
+ * @return data source Container
+ */
+ public Container getContainerDataSource();
+
+ }
+
+ /**
+ * <p>
+ * Interface implemented by the editor classes supporting editing the
+ * Container. Implementing this interface means that the Container serving
+ * as the data source of the editor can be modified through it.
+ * </p>
+ * <p>
+ * Note that not implementing the <code>Container.Editor</code> interface
+ * does not restrict the class from editing the Container contents
+ * internally.
+ * </p>
+ */
+ public interface Editor extends Container.Viewer, Serializable {
+
+ }
+
+ /* Contents change event */
+
+ /**
+ * An <code>Event</code> object specifying the Container whose Item set has
+ * changed (items added, removed or reordered).
+ *
+ * A simple property value change is not an item set change.
+ */
+ public interface ItemSetChangeEvent extends Serializable {
+
+ /**
+ * Gets the Property where the event occurred.
+ *
+ * @return source of the event
+ */
+ public Container getContainer();
+ }
+
+ /**
+ * Container Item set change listener interface.
+ *
+ * An item set change refers to addition, removal or reordering of items in
+ * the container. A simple property value change is not an item set change.
+ */
+ public interface ItemSetChangeListener extends Serializable {
+
+ /**
+ * Lets the listener know a Containers visible (filtered and/or sorted,
+ * if applicable) Item set has changed.
+ *
+ * @param event
+ * change event text
+ */
+ public void containerItemSetChange(Container.ItemSetChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>ItemSetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>ItemSetChangeEvent</code> when its contents
+ * are modified.
+ *
+ * An item set change refers to addition, removal or reordering of items in
+ * the container. A simple property value change is not an item set change.
+ *
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface ItemSetChangeNotifier extends Serializable {
+
+ /**
+ * Adds an Item set change listener for the object.
+ *
+ * @param listener
+ * listener to be added
+ */
+ public void addListener(Container.ItemSetChangeListener listener);
+
+ /**
+ * Removes the Item set change listener from the object.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(Container.ItemSetChangeListener listener);
+ }
+
+ /* Property set change event */
+
+ /**
+ * An <code>Event</code> object specifying the Container whose Property set
+ * has changed.
+ *
+ * A property set change means the addition, removal or other structural
+ * changes to the properties of a container. Changes concerning the set of
+ * items in the container and their property values are not property set
+ * changes.
+ */
+ public interface PropertySetChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Container whose contents have been modified.
+ *
+ * @return Source Container of the event.
+ */
+ public Container getContainer();
+ }
+
+ /**
+ * The listener interface for receiving <code>PropertySetChangeEvent</code>
+ * objects.
+ *
+ * A property set change means the addition, removal or other structural
+ * change of the properties (supported property IDs) of a container. Changes
+ * concerning the set of items in the container and their property values
+ * are not property set changes.
+ */
+ public interface PropertySetChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the set of property IDs supported by the
+ * Container has changed.
+ *
+ * @param event
+ * Change event.
+ */
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event);
+ }
+
+ /**
+ * <p>
+ * The interface for adding and removing <code>PropertySetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>PropertySetChangeEvent</code> when the set
+ * of property IDs supported by the container is modified.
+ * </p>
+ *
+ * <p>
+ * A property set change means the addition, removal or other structural
+ * changes to the properties of a container. Changes concerning the set of
+ * items in the container and their property values are not property set
+ * changes.
+ * </p>
+ *
+ * <p>
+ * Note that the general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface PropertySetChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new Property set change listener for this Container.
+ *
+ * @param listener
+ * The new Listener to be registered
+ */
+ public void addListener(Container.PropertySetChangeListener listener);
+
+ /**
+ * Removes a previously registered Property set change listener.
+ *
+ * @param listener
+ * Listener to be removed
+ */
+ public void removeListener(Container.PropertySetChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Item.java b/server/src/com/vaadin/data/Item.java
new file mode 100644
index 0000000000..98b95aecff
--- /dev/null
+++ b/server/src/com/vaadin/data/Item.java
@@ -0,0 +1,180 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * <p>
+ * Provides a mechanism for handling a set of Properties, each associated to a
+ * locally unique non-null identifier. The interface is split into subinterfaces
+ * to enable a class to implement only the functionalities it needs.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Item extends Serializable {
+
+ /**
+ * Gets the Property corresponding to the given Property ID stored in the
+ * Item. If the Item does not contain the Property, <code>null</code> is
+ * returned.
+ *
+ * @param id
+ * identifier of the Property to get
+ * @return the Property with the given ID or <code>null</code>
+ */
+ public Property<?> getItemProperty(Object id);
+
+ /**
+ * Gets the collection of IDs of all Properties stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties stored
+ * the Item
+ */
+ public Collection<?> getItemPropertyIds();
+
+ /**
+ * Tries to add a new Property into the Item.
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param id
+ * ID of the new Property
+ * @param property
+ * the Property to be added and associated with the id
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the operation is not supported.
+ */
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException;
+
+ /**
+ * Removes the Property identified by ID from the Item.
+ *
+ * <p>
+ * This functionality is optional.
+ * </p>
+ *
+ * @param id
+ * ID of the Property to be removed
+ * @return <code>true</code> if the operation succeeded
+ * @throws UnsupportedOperationException
+ * if the operation is not supported. <code>false</code> if not
+ */
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException;
+
+ /**
+ * Interface implemented by viewer classes capable of using an Item as a
+ * data source.
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Item that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * The new data source Item
+ */
+ public void setItemDataSource(Item newDataSource);
+
+ /**
+ * Gets the Item serving as the data source of the viewer.
+ *
+ * @return data source Item
+ */
+ public Item getItemDataSource();
+ }
+
+ /**
+ * Interface implemented by the <code>Editor</code> classes capable of
+ * editing the Item. Implementing this interface means that the Item serving
+ * as the data source of the editor can be modified through it.
+ * <p>
+ * Note : Not implementing the <code>Item.Editor</code> interface does not
+ * restrict the class from editing the contents of an internally.
+ * </p>
+ */
+ public interface Editor extends Item.Viewer, Serializable {
+
+ }
+
+ /* Property set change event */
+
+ /**
+ * An <code>Event</code> object specifying the Item whose contents has been
+ * changed through the <code>Property</code> interface.
+ * <p>
+ * Note: The values stored in the Properties may change without triggering
+ * this event.
+ * </p>
+ */
+ public interface PropertySetChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Item whose contents has been modified.
+ *
+ * @return source Item of the event
+ */
+ public Item getItem();
+ }
+
+ /**
+ * The listener interface for receiving <code>PropertySetChangeEvent</code>
+ * objects.
+ */
+ public interface PropertySetChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the Item's property set has changed.
+ *
+ * @param event
+ * Property set change event object
+ */
+ public void itemPropertySetChange(Item.PropertySetChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>PropertySetChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>PropertySetChangeEvent</code> when its
+ * Property set is modified.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ */
+ public interface PropertySetChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new property set change listener for this Item.
+ *
+ * @param listener
+ * The new Listener to be registered.
+ */
+ public void addListener(Item.PropertySetChangeListener listener);
+
+ /**
+ * Removes a previously registered property set change listener.
+ *
+ * @param listener
+ * Listener to be removed.
+ */
+ public void removeListener(Item.PropertySetChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Property.java b/server/src/com/vaadin/data/Property.java
new file mode 100644
index 0000000000..9fab642381
--- /dev/null
+++ b/server/src/com/vaadin/data/Property.java
@@ -0,0 +1,402 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * The <code>Property</code> is a simple data object that contains one typed
+ * value. This interface contains methods to inspect and modify the stored value
+ * and its type, and the object's read-only state.
+ * </p>
+ *
+ * <p>
+ * The <code>Property</code> also defines the events
+ * <code>ReadOnlyStatusChangeEvent</code> and <code>ValueChangeEvent</code>, and
+ * the associated <code>listener</code> and <code>notifier</code> interfaces.
+ * </p>
+ *
+ * <p>
+ * The <code>Property.Viewer</code> interface should be used to attach the
+ * Property to an external data source. This way the value in the data source
+ * can be inspected using the <code>Property</code> interface.
+ * </p>
+ *
+ * <p>
+ * The <code>Property.editor</code> interface should be implemented if the value
+ * needs to be changed through the implementing class.
+ * </p>
+ *
+ * @param T
+ * type of values of the property
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Property<T> extends Serializable {
+
+ /**
+ * Gets the value stored in the Property. The returned object is compatible
+ * with the class returned by getType().
+ *
+ * @return the value stored in the Property
+ */
+ public T getValue();
+
+ /**
+ * Sets the value of the Property.
+ * <p>
+ * Implementing this functionality is optional. If the functionality is
+ * missing, one should declare the Property to be in read-only mode and
+ * throw <code>Property.ReadOnlyException</code> in this function.
+ * </p>
+ *
+ * Note : Since Vaadin 7.0, setting the value of a non-String property as a
+ * String is no longer supported.
+ *
+ * @param newValue
+ * New value of the Property. This should be assignable to the
+ * type returned by getType
+ *
+ * @throws Property.ReadOnlyException
+ * if the object is in read-only mode
+ */
+ public void setValue(Object newValue) throws Property.ReadOnlyException;
+
+ /**
+ * Returns the type of the Property. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ public Class<? extends T> getType();
+
+ /**
+ * Tests if the Property is in read-only mode. In read-only mode calls to
+ * the method <code>setValue</code> will throw
+ * <code>ReadOnlyException</code> and will not modify the value of the
+ * Property.
+ *
+ * @return <code>true</code> if the Property is in read-only mode,
+ * <code>false</code> if it's not
+ */
+ public boolean isReadOnly();
+
+ /**
+ * Sets the Property's read-only mode to the specified status.
+ *
+ * This functionality is optional, but all properties must implement the
+ * <code>isReadOnly</code> mode query correctly.
+ *
+ * @param newStatus
+ * new read-only status of the Property
+ */
+ public void setReadOnly(boolean newStatus);
+
+ /**
+ * A Property that is capable of handle a transaction that can end in commit
+ * or rollback.
+ *
+ * Note that this does not refer to e.g. database transactions but rather
+ * two-phase commit that allows resetting old field values on a form etc. if
+ * the commit of one of the properties fails after others have already been
+ * committed. If
+ *
+ * @param <T>
+ * The type of the property
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ */
+ public interface Transactional<T> extends Property<T> {
+
+ /**
+ * Starts a transaction.
+ *
+ * <p>
+ * If the value is set during a transaction the value must not replace
+ * the original value until {@link #commit()} is called. Still,
+ * {@link #getValue()} must return the current value set in the
+ * transaction. Calling {@link #rollback()} while in a transaction must
+ * rollback the value to what it was before the transaction started.
+ * </p>
+ * <p>
+ * {@link ValueChangeEvent}s must not be emitted for internal value
+ * changes during a transaction. If the value changes as a result of
+ * {@link #commit()}, a {@link ValueChangeEvent} should be emitted.
+ * </p>
+ */
+ public void startTransaction();
+
+ /**
+ * Commits and ends the transaction that is in progress.
+ * <p>
+ * If the value is changed as a result of this operation, a
+ * {@link ValueChangeEvent} is emitted if such are supported.
+ * <p>
+ * This method has no effect if there is no transaction is in progress.
+ * <p>
+ * This method must never throw an exception.
+ */
+ public void commit();
+
+ /**
+ * Aborts and rolls back the transaction that is in progress.
+ * <p>
+ * The value is reset to the value before the transaction started. No
+ * {@link ValueChangeEvent} is emitted as a result of this.
+ * <p>
+ * This method has no effect if there is no transaction is in progress.
+ * <p>
+ * This method must never throw an exception.
+ */
+ public void rollback();
+ }
+
+ /**
+ * <code>Exception</code> object that signals that a requested Property
+ * modification failed because it's in read-only mode.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class ReadOnlyException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>ReadOnlyException</code> without a detail
+ * message.
+ */
+ public ReadOnlyException() {
+ }
+
+ /**
+ * Constructs a new <code>ReadOnlyException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public ReadOnlyException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Interface implemented by the viewer classes capable of using a Property
+ * as a data source.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Viewer extends Serializable {
+
+ /**
+ * Sets the Property that serves as the data source of the viewer.
+ *
+ * @param newDataSource
+ * the new data source Property
+ */
+ public void setPropertyDataSource(Property newDataSource);
+
+ /**
+ * Gets the Property serving as the data source of the viewer.
+ *
+ * @return the Property serving as the viewers data source
+ */
+ public Property getPropertyDataSource();
+ }
+
+ /**
+ * Interface implemented by the editor classes capable of editing the
+ * Property.
+ * <p>
+ * Implementing this interface means that the Property serving as the data
+ * source of the editor can be modified through the editor. It does not
+ * restrict the editor from editing the Property internally, though if the
+ * Property is in a read-only mode, attempts to modify it will result in the
+ * <code>ReadOnlyException</code> being thrown.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Editor extends Property.Viewer, Serializable {
+
+ }
+
+ /* Value change event */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose value has been
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeEvent extends Serializable {
+
+ /**
+ * Retrieves the Property that has been modified.
+ *
+ * @return source Property of the event
+ */
+ public Property getProperty();
+ }
+
+ /**
+ * The <code>listener</code> interface for receiving
+ * <code>ValueChangeEvent</code> objects.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that the Property's value has changed.
+ *
+ * @param event
+ * value change event object
+ */
+ public void valueChange(Property.ValueChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>ValueChangeEvent</code>
+ * listeners. If a Property wishes to allow other objects to receive
+ * <code>ValueChangeEvent</code> generated by it, it must implement this
+ * interface.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ValueChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new value change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered
+ */
+ public void addListener(Property.ValueChangeListener listener);
+
+ /**
+ * Removes a previously registered value change listener.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(Property.ValueChangeListener listener);
+ }
+
+ /* ReadOnly Status change event */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has been changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeEvent extends Serializable {
+
+ /**
+ * Property whose read-only state has changed.
+ *
+ * @return source Property of the event.
+ */
+ public Property getProperty();
+ }
+
+ /**
+ * The listener interface for receiving
+ * <code>ReadOnlyStatusChangeEvent</code> objects.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeListener extends Serializable {
+
+ /**
+ * Notifies this listener that a Property's read-only status has
+ * changed.
+ *
+ * @param event
+ * Read-only status change event object
+ */
+ public void readOnlyStatusChange(
+ Property.ReadOnlyStatusChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing
+ * <code>ReadOnlyStatusChangeEvent</code> listeners. If a Property wishes to
+ * allow other objects to receive <code>ReadOnlyStatusChangeEvent</code>
+ * generated by it, it must implement this interface.
+ * <p>
+ * Note : The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ReadOnlyStatusChangeNotifier extends Serializable {
+
+ /**
+ * Registers a new read-only status change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered
+ */
+ public void addListener(Property.ReadOnlyStatusChangeListener listener);
+
+ /**
+ * Removes a previously registered read-only status change listener.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(
+ Property.ReadOnlyStatusChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/Validatable.java b/server/src/com/vaadin/data/Validatable.java
new file mode 100644
index 0000000000..4a7a0fda10
--- /dev/null
+++ b/server/src/com/vaadin/data/Validatable.java
@@ -0,0 +1,110 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * <p>
+ * Interface for validatable objects. Defines methods to verify if the object's
+ * value is valid or not, and to add, remove and list registered validators of
+ * the object.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ * @see com.vaadin.data.Validator
+ */
+public interface Validatable extends Serializable {
+
+ /**
+ * <p>
+ * Adds a new validator for this object. The validator's
+ * {@link Validator#validate(Object)} method is activated every time the
+ * object's value needs to be verified, that is, when the {@link #isValid()}
+ * method is called. This usually happens when the object's value changes.
+ * </p>
+ *
+ * @param validator
+ * the new validator
+ */
+ void addValidator(Validator validator);
+
+ /**
+ * <p>
+ * Removes a previously registered validator from the object. The specified
+ * validator is removed from the object and its <code>validate</code> method
+ * is no longer called in {@link #isValid()}.
+ * </p>
+ *
+ * @param validator
+ * the validator to remove
+ */
+ void removeValidator(Validator validator);
+
+ /**
+ * <p>
+ * Lists all validators currently registered for the object. If no
+ * validators are registered, returns <code>null</code>.
+ * </p>
+ *
+ * @return collection of validators or <code>null</code>
+ */
+ public Collection<Validator> getValidators();
+
+ /**
+ * <p>
+ * Tests the current value of the object against all registered validators.
+ * The registered validators are iterated and for each the
+ * {@link Validator#validate(Object)} method is called. If any validator
+ * throws the {@link Validator.InvalidValueException} this method returns
+ * <code>false</code>.
+ * </p>
+ *
+ * @return <code>true</code> if the registered validators concur that the
+ * value is valid, <code>false</code> otherwise
+ */
+ public boolean isValid();
+
+ /**
+ * <p>
+ * Checks the validity of the validatable. If the validatable is valid this
+ * method should do nothing, and if it's not valid, it should throw
+ * <code>Validator.InvalidValueException</code>
+ * </p>
+ *
+ * @throws Validator.InvalidValueException
+ * if the value is not valid
+ */
+ public void validate() throws Validator.InvalidValueException;
+
+ /**
+ * <p>
+ * Checks the validabtable object accept invalid values.The default value is
+ * <code>true</code>.
+ * </p>
+ *
+ */
+ public boolean isInvalidAllowed();
+
+ /**
+ * <p>
+ * Should the validabtable object accept invalid values. Supporting this
+ * configuration possibility is optional. By default invalid values are
+ * allowed.
+ * </p>
+ *
+ * @param invalidValueAllowed
+ *
+ * @throws UnsupportedOperationException
+ * if the setInvalidAllowed is not supported.
+ */
+ public void setInvalidAllowed(boolean invalidValueAllowed)
+ throws UnsupportedOperationException;
+
+}
diff --git a/server/src/com/vaadin/data/Validator.java b/server/src/com/vaadin/data/Validator.java
new file mode 100644
index 0000000000..768a02babe
--- /dev/null
+++ b/server/src/com/vaadin/data/Validator.java
@@ -0,0 +1,175 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
+
+/**
+ * Interface that implements a method for validating if an {@link Object} is
+ * valid or not.
+ * <p>
+ * Implementors of this class can be added to any
+ * {@link com.vaadin.data.Validatable Validatable} implementor to verify its
+ * value.
+ * </p>
+ * <p>
+ * {@link #validate(Object)} can be used to check if a value is valid. An
+ * {@link InvalidValueException} with an appropriate validation error message is
+ * thrown if the value is not valid.
+ * </p>
+ * <p>
+ * Validators must not have any side effects.
+ * </p>
+ * <p>
+ * Since Vaadin 7, the method isValid(Object) does not exist in the interface -
+ * {@link #validate(Object)} should be used instead, and the exception caught
+ * where applicable. Concrete classes implementing {@link Validator} can still
+ * internally implement and use isValid(Object) for convenience or to ease
+ * migration from earlier Vaadin versions.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Validator extends Serializable {
+
+ /**
+ * Checks the given value against this validator. If the value is valid the
+ * method does nothing. If the value is invalid, an
+ * {@link InvalidValueException} is thrown.
+ *
+ * @param value
+ * the value to check
+ * @throws Validator.InvalidValueException
+ * if the value is invalid
+ */
+ public void validate(Object value) throws Validator.InvalidValueException;
+
+ /**
+ * Exception that is thrown by a {@link Validator} when a value is invalid.
+ *
+ * <p>
+ * The default implementation of InvalidValueException does not support HTML
+ * in error messages. To enable HTML support, override
+ * {@link #getHtmlMessage()} and use the subclass in validators.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public class InvalidValueException extends RuntimeException {
+
+ /**
+ * Array of one or more validation errors that are causing this
+ * validation error.
+ */
+ private InvalidValueException[] causes = null;
+
+ /**
+ * Constructs a new {@code InvalidValueException} with the specified
+ * message.
+ *
+ * @param message
+ * The detail message of the problem.
+ */
+ public InvalidValueException(String message) {
+ this(message, new InvalidValueException[] {});
+ }
+
+ /**
+ * Constructs a new {@code InvalidValueException} with a set of causing
+ * validation exceptions. The causing validation exceptions are included
+ * when the exception is painted to the client.
+ *
+ * @param message
+ * The detail message of the problem.
+ * @param causes
+ * One or more {@code InvalidValueException}s that caused
+ * this exception.
+ */
+ public InvalidValueException(String message,
+ InvalidValueException[] causes) {
+ super(message);
+ if (causes == null) {
+ throw new NullPointerException(
+ "Possible causes array must not be null");
+ }
+
+ this.causes = causes;
+ }
+
+ /**
+ * Check if the error message should be hidden.
+ *
+ * An empty (null or "") message is invisible unless it contains nested
+ * exceptions that are visible.
+ *
+ * @return true if the error message should be hidden, false otherwise
+ */
+ public boolean isInvisible() {
+ String msg = getMessage();
+ if (msg != null && msg.length() > 0) {
+ return false;
+ }
+ if (causes != null) {
+ for (int i = 0; i < causes.length; i++) {
+ if (!causes[i].isInvisible()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the message of the error in HTML.
+ *
+ * Note that this API may change in future versions.
+ */
+ public String getHtmlMessage() {
+ return AbstractApplicationServlet
+ .safeEscapeForHtml(getLocalizedMessage());
+ }
+
+ /**
+ * Returns the {@code InvalidValueExceptions} that caused this
+ * exception.
+ *
+ * @return An array containing the {@code InvalidValueExceptions} that
+ * caused this exception. Returns an empty array if this
+ * exception was not caused by other exceptions.
+ */
+ public InvalidValueException[] getCauses() {
+ return causes;
+ }
+
+ }
+
+ /**
+ * A specific type of {@link InvalidValueException} that indicates that
+ * validation failed because the value was empty. What empty means is up to
+ * the thrower.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.3.0
+ */
+ @SuppressWarnings("serial")
+ public class EmptyValueException extends Validator.InvalidValueException {
+
+ public EmptyValueException(String message) {
+ super(message);
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
new file mode 100644
index 0000000000..b8efa5b1e4
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.validator.BeanValidator;
+import com.vaadin.ui.Field;
+
+public class BeanFieldGroup<T> extends FieldGroup {
+
+ private Class<T> beanType;
+
+ private static Boolean beanValidationImplementationAvailable = null;
+
+ public BeanFieldGroup(Class<T> beanType) {
+ this.beanType = beanType;
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId) {
+ if (getItemDataSource() != null) {
+ return super.getPropertyType(propertyId);
+ } else {
+ // Data source not set so we need to figure out the type manually
+ /*
+ * toString should never really be needed as propertyId should be of
+ * form "fieldName" or "fieldName.subField[.subField2]" but the
+ * method declaration comes from parent.
+ */
+ java.lang.reflect.Field f;
+ try {
+ f = getField(beanType, propertyId.toString());
+ return f.getType();
+ } catch (SecurityException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'.", e);
+ } catch (NoSuchFieldException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'. The propertyId was not found in "
+ + beanType.getName(), e);
+ }
+ }
+ }
+
+ private static java.lang.reflect.Field getField(Class<?> cls,
+ String propertyId) throws SecurityException, NoSuchFieldException {
+ if (propertyId.contains(".")) {
+ String[] parts = propertyId.split("\\.", 2);
+ // Get the type of the field in the "cls" class
+ java.lang.reflect.Field field1 = getField(cls, parts[0]);
+ // Find the rest from the sub type
+ return getField(field1.getType(), parts[1]);
+ } else {
+ try {
+ // Try to find the field directly in the given class
+ java.lang.reflect.Field field1 = cls
+ .getDeclaredField(propertyId);
+ return field1;
+ } catch (NoSuchFieldError e) {
+ // Try super classes until we reach Object
+ Class<?> superClass = cls.getSuperclass();
+ if (superClass != Object.class) {
+ return getField(superClass, propertyId);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for setting the data source directly using a bean. This
+ * method wraps the bean in a {@link BeanItem} and calls
+ * {@link #setItemDataSource(Item)}.
+ *
+ * @param bean
+ * The bean to use as data source.
+ */
+ public void setItemDataSource(T bean) {
+ setItemDataSource(new BeanItem(bean));
+ }
+
+ @Override
+ public void setItemDataSource(Item item) {
+ if (!(item instanceof BeanItem)) {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " only supports BeanItems as item data source");
+ }
+ super.setItemDataSource(item);
+ }
+
+ @Override
+ public BeanItem<T> getItemDataSource() {
+ return (BeanItem<T>) super.getItemDataSource();
+ }
+
+ @Override
+ public void bind(Field field, Object propertyId) {
+ if (getItemDataSource() != null) {
+ // The data source is set so the property must be found in the item.
+ // If it is not we try to add it.
+ try {
+ getItemProperty(propertyId);
+ } catch (BindException e) {
+ // Not found, try to add a nested property;
+ // BeanItem property ids are always strings so this is safe
+ getItemDataSource().addNestedProperty((String) propertyId);
+ }
+ }
+
+ super.bind(field, propertyId);
+ }
+
+ @Override
+ protected void configureField(Field<?> field) {
+ super.configureField(field);
+ // Add Bean validators if there are annotations
+ if (isBeanValidationImplementationAvailable()) {
+ BeanValidator validator = new BeanValidator(beanType,
+ getPropertyId(field).toString());
+ field.addValidator(validator);
+ if (field.getLocale() != null) {
+ validator.setLocale(field.getLocale());
+ }
+ }
+ }
+
+ /**
+ * Checks whether a bean validation implementation (e.g. Hibernate Validator
+ * or Apache Bean Validation) is available.
+ *
+ * TODO move this method to some more generic location
+ *
+ * @return true if a JSR-303 bean validation implementation is available
+ */
+ protected static boolean isBeanValidationImplementationAvailable() {
+ if (beanValidationImplementationAvailable != null) {
+ return beanValidationImplementationAvailable;
+ }
+ try {
+ Class<?> validationClass = Class
+ .forName("javax.validation.Validation");
+ Method buildFactoryMethod = validationClass
+ .getMethod("buildDefaultValidatorFactory");
+ Object factory = buildFactoryMethod.invoke(null);
+ beanValidationImplementationAvailable = (factory != null);
+ } catch (Exception e) {
+ // no bean validation implementation available
+ beanValidationImplementationAvailable = false;
+ }
+ return beanValidationImplementationAvailable;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/fieldgroup/Caption.java b/server/src/com/vaadin/data/fieldgroup/Caption.java
new file mode 100644
index 0000000000..b990b720cd
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/Caption.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Caption {
+ String value();
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
new file mode 100644
index 0000000000..be0db328f2
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.EnumSet;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.FieldGroup.BindException;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.AbstractTextField;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.OptionGroup;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.TextField;
+
+public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
+
+ public static final Object CAPTION_PROPERTY_ID = "Caption";
+
+ @Override
+ public <T extends Field> T createField(Class<?> type, Class<T> fieldType) {
+ if (Enum.class.isAssignableFrom(type)) {
+ return createEnumField(type, fieldType);
+ } else if (Boolean.class.isAssignableFrom(type)
+ || boolean.class.isAssignableFrom(type)) {
+ return createBooleanField(fieldType);
+ }
+ if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return fieldType.cast(createAbstractTextField(fieldType
+ .asSubclass(AbstractTextField.class)));
+ } else if (fieldType == RichTextArea.class) {
+ return fieldType.cast(createRichTextArea());
+ }
+ return createDefaultField(type, fieldType);
+ }
+
+ protected RichTextArea createRichTextArea() {
+ RichTextArea rta = new RichTextArea();
+ rta.setImmediate(true);
+
+ return rta;
+ }
+
+ private <T extends Field> T createEnumField(Class<?> type,
+ Class<T> fieldType) {
+ if (AbstractSelect.class.isAssignableFrom(fieldType)) {
+ AbstractSelect s = createCompatibleSelect((Class<? extends AbstractSelect>) fieldType);
+ populateWithEnumData(s, (Class<? extends Enum>) type);
+ return (T) s;
+ }
+
+ return null;
+ }
+
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ AbstractSelect select;
+ if (fieldType.isAssignableFrom(ListSelect.class)) {
+ select = new ListSelect();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(NativeSelect.class)) {
+ select = new NativeSelect();
+ } else if (fieldType.isAssignableFrom(OptionGroup.class)) {
+ select = new OptionGroup();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(Table.class)) {
+ Table t = new Table();
+ t.setSelectable(true);
+ select = t;
+ } else {
+ select = new ComboBox(null);
+ }
+ select.setImmediate(true);
+ select.setNullSelectionAllowed(false);
+
+ return select;
+ }
+
+ protected <T extends Field> T createBooleanField(Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(CheckBox.class)) {
+ CheckBox cb = new CheckBox(null);
+ cb.setImmediate(true);
+ return (T) cb;
+ } else if (AbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField((Class<? extends AbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ protected <T extends AbstractTextField> T createAbstractTextField(
+ Class<T> fieldType) {
+ if (fieldType == AbstractTextField.class) {
+ fieldType = (Class<T>) TextField.class;
+ }
+ try {
+ T field = fieldType.newInstance();
+ field.setImmediate(true);
+ return field;
+ } catch (Exception e) {
+ throw new BindException("Could not create a field of type "
+ + fieldType, e);
+ }
+ }
+
+ /**
+ * Fallback when no specific field has been created. Typically returns a
+ * TextField.
+ *
+ * @param <T>
+ * The type of field to create
+ * @param type
+ * The type of data that should be edited
+ * @param fieldType
+ * The type of field to create
+ * @return A field capable of editing the data or null if no field could be
+ * created
+ */
+ protected <T extends Field> T createDefaultField(Class<?> type,
+ Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(TextField.class)) {
+ return fieldType.cast(createAbstractTextField(TextField.class));
+ }
+ return null;
+ }
+
+ /**
+ * Populates the given select with all the enums in the given {@link Enum}
+ * class. Uses {@link Enum}.toString() for caption.
+ *
+ * @param select
+ * The select to populate
+ * @param enumClass
+ * The Enum class to use
+ */
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ select.removeAllItems();
+ for (Object p : select.getContainerPropertyIds()) {
+ select.removeContainerProperty(p);
+ }
+ select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, "");
+ select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID);
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ Item newItem = select.addItem(r);
+ newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString());
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
new file mode 100644
index 0000000000..3df19f5bc9
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -0,0 +1,978 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Validator.InvalidValueException;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Form;
+
+/**
+ * FieldGroup provides an easy way of binding fields to data and handling
+ * commits of these fields.
+ * <p>
+ * The functionality of FieldGroup is similar to {@link Form} but
+ * {@link FieldGroup} does not handle layouts in any way. The typical use case
+ * is to create a layout outside the FieldGroup and then use FieldGroup to bind
+ * the fields to a data source.
+ * </p>
+ * <p>
+ * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
+ * Using the buildAndBind methods {@link FieldGroup} can create fields for you
+ * using a FieldGroupFieldFactory but you still have to add them to the correct
+ * position in your layout.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ */
+public class FieldGroup implements Serializable {
+
+ private static final Logger logger = Logger.getLogger(FieldGroup.class
+ .getName());
+
+ private Item itemDataSource;
+ private boolean buffered = true;
+
+ private boolean enabled = true;
+ private boolean readOnly = false;
+
+ private HashMap<Object, Field<?>> propertyIdToField = new HashMap<Object, Field<?>>();
+ private LinkedHashMap<Field<?>, Object> fieldToPropertyId = new LinkedHashMap<Field<?>, Object>();
+ private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>();
+
+ /**
+ * The field factory used by builder methods.
+ */
+ private FieldGroupFieldFactory fieldFactory = new DefaultFieldGroupFieldFactory();
+
+ /**
+ * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a
+ * data source for the field binder.
+ *
+ */
+ public FieldGroup() {
+
+ }
+
+ /**
+ * Constructs a field binder that uses the given data source.
+ *
+ * @param itemDataSource
+ * The data source to bind the fields to
+ */
+ public FieldGroup(Item itemDataSource) {
+ setItemDataSource(itemDataSource);
+ }
+
+ /**
+ * Updates the item that is used by this FieldBinder. Rebinds all fields to
+ * the properties in the new item.
+ *
+ * @param itemDataSource
+ * The new item to use
+ */
+ public void setItemDataSource(Item itemDataSource) {
+ this.itemDataSource = itemDataSource;
+
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ bind(f, fieldToPropertyId.get(f));
+ }
+ }
+
+ /**
+ * Gets the item used by this FieldBinder. Note that you must call
+ * {@link #commit()} for the item to be updated unless buffered mode has
+ * been switched off.
+ *
+ * @see #setBuffered(boolean)
+ * @see #commit()
+ *
+ * @return The item used by this FieldBinder
+ */
+ public Item getItemDataSource() {
+ return itemDataSource;
+ }
+
+ /**
+ * Checks the buffered mode for the bound fields.
+ * <p>
+ *
+ * @see #setBuffered(boolean) for more details on buffered mode
+ *
+ * @see Field#isBuffered()
+ * @return true if buffered mode is on, false otherwise
+ *
+ */
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /**
+ * Sets the buffered mode for the bound fields.
+ * <p>
+ * When buffered mode is on the item will not be updated until
+ * {@link #commit()} is called. If buffered mode is off the item will be
+ * updated once the fields are updated.
+ * </p>
+ * <p>
+ * The default is to use buffered mode.
+ * </p>
+ *
+ * @see Field#setBuffered(boolean)
+ * @param buffered
+ * true to turn on buffered mode, false otherwise
+ */
+ public void setBuffered(boolean buffered) {
+ if (buffered == this.buffered) {
+ return;
+ }
+
+ this.buffered = buffered;
+ for (Field<?> field : getFields()) {
+ field.setBuffered(buffered);
+ }
+ }
+
+ /**
+ * Returns the enabled status for the fields.
+ * <p>
+ * Note that this will not accurately represent the enabled status of all
+ * fields if you change the enabled status of the fields through some other
+ * method than {@link #setEnabled(boolean)}.
+ *
+ * @return true if the fields are enabled, false otherwise
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Updates the enabled state of all bound fields.
+ *
+ * @param fieldsEnabled
+ * true to enable all bound fields, false to disable them
+ */
+ public void setEnabled(boolean fieldsEnabled) {
+ enabled = fieldsEnabled;
+ for (Field<?> field : getFields()) {
+ field.setEnabled(fieldsEnabled);
+ }
+ }
+
+ /**
+ * Returns the read only status for the fields.
+ * <p>
+ * Note that this will not accurately represent the read only status of all
+ * fields if you change the read only status of the fields through some
+ * other method than {@link #setReadOnly(boolean)}.
+ *
+ * @return true if the fields are set to read only, false otherwise
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Updates the read only state of all bound fields.
+ *
+ * @param fieldsReadOnly
+ * true to set all bound fields to read only, false to set them
+ * to read write
+ */
+ public void setReadOnly(boolean fieldsReadOnly) {
+ readOnly = fieldsReadOnly;
+ }
+
+ /**
+ * Returns a collection of all fields that have been bound.
+ * <p>
+ * The fields are not returned in any specific order.
+ * </p>
+ *
+ * @return A collection with all bound Fields
+ */
+ public Collection<Field<?>> getFields() {
+ return fieldToPropertyId.keySet();
+ }
+
+ /**
+ * Binds the field with the given propertyId from the current item. If an
+ * item has not been set then the binding is postponed until the item is set
+ * using {@link #setItemDataSource(Item)}.
+ * <p>
+ * This method also adds validators when applicable.
+ * </p>
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the property id is already bound to another field by this
+ * field binder
+ */
+ public void bind(Field<?> field, Object propertyId) throws BindException {
+ if (propertyIdToField.containsKey(propertyId)
+ && propertyIdToField.get(propertyId) != field) {
+ throw new BindException("Property id " + propertyId
+ + " is already bound to another field");
+ }
+ fieldToPropertyId.put(field, propertyId);
+ propertyIdToField.put(propertyId, field);
+ if (itemDataSource == null) {
+ // Will be bound when data source is set
+ return;
+ }
+
+ field.setPropertyDataSource(wrapInTransactionalProperty(getItemProperty(propertyId)));
+ configureField(field);
+ }
+
+ private <T> Property.Transactional<T> wrapInTransactionalProperty(
+ Property<T> itemProperty) {
+ return new TransactionalPropertyWrapper<T>(itemProperty);
+ }
+
+ /**
+ * Gets the property with the given property id from the item.
+ *
+ * @param propertyId
+ * The id if the property to find
+ * @return The property with the given id from the item
+ * @throws BindException
+ * If the property was not found in the item or no item has been
+ * set
+ */
+ protected Property<?> getItemProperty(Object propertyId)
+ throws BindException {
+ Item item = getItemDataSource();
+ if (item == null) {
+ throw new BindException("Could not lookup property with id "
+ + propertyId + " as no item has been set");
+ }
+ Property<?> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException("A property with id " + propertyId
+ + " was not found in the item");
+ }
+ return p;
+ }
+
+ /**
+ * Detaches the field from its property id and removes it from this
+ * FieldBinder.
+ * <p>
+ * Note that the field is not detached from its property data source if it
+ * is no longer connected to the same property id it was bound to using this
+ * FieldBinder.
+ *
+ * @param field
+ * The field to detach
+ * @throws BindException
+ * If the field is not bound by this field binder or not bound
+ * to the correct property id
+ */
+ public void unbind(Field<?> field) throws BindException {
+ Object propertyId = fieldToPropertyId.get(field);
+ if (propertyId == null) {
+ throw new BindException(
+ "The given field is not part of this FieldBinder");
+ }
+
+ Property fieldDataSource = field.getPropertyDataSource();
+ if (fieldDataSource instanceof TransactionalPropertyWrapper) {
+ fieldDataSource = ((TransactionalPropertyWrapper) fieldDataSource)
+ .getWrappedProperty();
+ }
+ if (fieldDataSource == getItemProperty(propertyId)) {
+ field.setPropertyDataSource(null);
+ }
+ fieldToPropertyId.remove(field);
+ propertyIdToField.remove(propertyId);
+ }
+
+ /**
+ * Configures a field with the settings set for this FieldBinder.
+ * <p>
+ * By default this updates the buffered, read only and enabled state of the
+ * field. Also adds validators when applicable.
+ *
+ * @param field
+ * The field to update
+ */
+ protected void configureField(Field<?> field) {
+ field.setBuffered(isBuffered());
+
+ field.setEnabled(isEnabled());
+ field.setReadOnly(isReadOnly());
+ }
+
+ /**
+ * Gets the type of the property with the given property id.
+ *
+ * @param propertyId
+ * The propertyId. Must be find
+ * @return The type of the property
+ */
+ protected Class<?> getPropertyType(Object propertyId) throws BindException {
+ if (getItemDataSource() == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No item data source has been set.");
+ }
+ Property<?> p = getItemDataSource().getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException(
+ "Property type for '"
+ + propertyId
+ + "' could not be determined. No property with that id was found.");
+ }
+
+ return p.getType();
+ }
+
+ /**
+ * Returns a collection of all property ids that have been bound to fields.
+ * <p>
+ * Note that this will return property ids even before the item has been
+ * set. In that case it returns the property ids that will be bound once the
+ * item is set.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of bound property ids
+ */
+ public Collection<Object> getBoundPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIdToField.keySet());
+ }
+
+ /**
+ * Returns a collection of all property ids that exist in the item set using
+ * {@link #setItemDataSource(Item)} but have not been bound to fields.
+ * <p>
+ * Will always return an empty collection before an item has been set using
+ * {@link #setItemDataSource(Item)}.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of property ids that have not been bound to fields
+ */
+ public Collection<Object> getUnboundPropertyIds() {
+ if (getItemDataSource() == null) {
+ return new ArrayList<Object>();
+ }
+ List<Object> unboundPropertyIds = new ArrayList<Object>();
+ unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
+ unboundPropertyIds.removeAll(propertyIdToField.keySet());
+ return unboundPropertyIds;
+ }
+
+ /**
+ * Commits all changes done to the bound fields.
+ * <p>
+ * Calls all {@link CommitHandler}s before and after committing the field
+ * changes to the item data source. The whole commit is aborted and state is
+ * restored to what it was before commit was called if any
+ * {@link CommitHandler} throws a CommitException or there is a problem
+ * committing the fields
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ */
+ public void commit() throws CommitException {
+ if (!isBuffered()) {
+ // Not using buffered mode, nothing to do
+ return;
+ }
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .startTransaction();
+ }
+ try {
+ firePreCommitEvent();
+ // Commit the field values to the properties
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ f.commit();
+ }
+ firePostCommitEvent();
+
+ // Commit the properties
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .commit();
+ }
+
+ } catch (Exception e) {
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ try {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .rollback();
+ } catch (Exception rollbackException) {
+ // FIXME: What to do ?
+ }
+ }
+
+ throw new CommitException("Commit failed", e);
+ }
+
+ }
+
+ /**
+ * Sends a preCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePreCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.preCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Sends a postCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePostCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.postCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Discards all changes done to the bound fields.
+ * <p>
+ * Only has effect if buffered mode is used.
+ *
+ */
+ public void discard() {
+ for (Field<?> f : fieldToPropertyId.keySet()) {
+ try {
+ f.discard();
+ } catch (Exception e) {
+ // TODO: handle exception
+ // What can we do if discard fails other than try to discard all
+ // other fields?
+ }
+ }
+ }
+
+ /**
+ * Returns the field that is bound to the given property id
+ *
+ * @param propertyId
+ * The property id to use to lookup the field
+ * @return The field that is bound to the property id or null if no field is
+ * bound to that property id
+ */
+ public Field<?> getField(Object propertyId) {
+ return propertyIdToField.get(propertyId);
+ }
+
+ /**
+ * Returns the property id that is bound to the given field
+ *
+ * @param field
+ * The field to use to lookup the property id
+ * @return The property id that is bound to the field or null if the field
+ * is not bound to any property id by this FieldBinder
+ */
+ public Object getPropertyId(Field<?> field) {
+ return fieldToPropertyId.get(field);
+ }
+
+ /**
+ * Adds a commit handler.
+ * <p>
+ * The commit handler is called before the field values are committed to the
+ * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item
+ * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a
+ * {@link CommitHandler} throws a CommitException the whole commit is
+ * aborted and the fields retain their old values.
+ *
+ * @param commitHandler
+ * The commit handler to add
+ */
+ public void addCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.add(commitHandler);
+ }
+
+ /**
+ * Removes the given commit handler.
+ *
+ * @see #addCommitHandler(CommitHandler)
+ *
+ * @param commitHandler
+ * The commit handler to remove
+ */
+ public void removeCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.remove(commitHandler);
+ }
+
+ /**
+ * Returns a list of all commit handlers for this {@link FieldGroup}.
+ * <p>
+ * Use {@link #addCommitHandler(CommitHandler)} and
+ * {@link #removeCommitHandler(CommitHandler)} to register or unregister a
+ * commit handler.
+ *
+ * @return A collection of commit handlers
+ */
+ protected Collection<CommitHandler> getCommitHandlers() {
+ return Collections.unmodifiableCollection(commitHandlers);
+ }
+
+ /**
+ * CommitHandlers are used by {@link FieldGroup#commit()} as part of the
+ * commit transactions. CommitHandlers can perform custom operations as part
+ * of the commit and cause the commit to be aborted by throwing a
+ * {@link CommitException}.
+ */
+ public interface CommitHandler extends Serializable {
+ /**
+ * Called before changes are committed to the field and the item is
+ * updated.
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void preCommit(CommitEvent commitEvent) throws CommitException;
+
+ /**
+ * Called after changes are committed to the fields and the item is
+ * updated..
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void postCommit(CommitEvent commitEvent) throws CommitException;
+ }
+
+ /**
+ * FIXME javadoc
+ *
+ */
+ public static class CommitEvent implements Serializable {
+ private FieldGroup fieldBinder;
+
+ private CommitEvent(FieldGroup fieldBinder) {
+ this.fieldBinder = fieldBinder;
+ }
+
+ /**
+ * Returns the field binder that this commit relates to
+ *
+ * @return The FieldBinder that is being committed.
+ */
+ public FieldGroup getFieldBinder() {
+ return fieldBinder;
+ }
+
+ }
+
+ /**
+ * Checks the validity of the bound fields.
+ * <p>
+ * Call the {@link Field#validate()} for the fields to get the individual
+ * error messages.
+ *
+ * @return true if all bound fields are valid, false otherwise.
+ */
+ public boolean isValid() {
+ try {
+ for (Field<?> field : getFields()) {
+ field.validate();
+ }
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if any bound field has been modified.
+ *
+ * @return true if at least on field has been modified, false otherwise
+ */
+ public boolean isModified() {
+ for (Field<?> field : getFields()) {
+ if (field.isModified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @return The field factory in use
+ *
+ */
+ public FieldGroupFieldFactory getFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+ }
+
+ /**
+ * Binds member fields found in the given object.
+ * <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. All non-null fields for which a property id can
+ * be determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * @PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age = new TextField("Age"); ... }
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.bindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and the age TextField to a "age"
+ * property.
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to bind
+ * @throws BindException
+ * If there is a problem binding a field
+ */
+ public void bindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, false);
+ }
+
+ /**
+ * Binds member fields found in the given object and builds 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. All non-null fields for which a property id can
+ * be determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * @PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age;
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.buildAndBindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and builds an age TextField using
+ * the field factory and then binds it to the "age" property.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ public void buildAndBindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, true);
+ }
+
+ /**
+ * Binds member fields found in the given object and optionally builds
+ * 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.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ protected void buildAndBindMemberFields(Object objectWithMemberFields,
+ boolean buildFields) throws BindException {
+ Class<?> objectClass = objectWithMemberFields.getClass();
+
+ for (java.lang.reflect.Field memberField : objectClass
+ .getDeclaredFields()) {
+
+ if (!Field.class.isAssignableFrom(memberField.getType())) {
+ // Process next field
+ continue;
+ }
+
+ PropertyId propertyIdAnnotation = memberField
+ .getAnnotation(PropertyId.class);
+
+ Class<? extends Field> fieldType = (Class<? extends Field>) memberField
+ .getType();
+
+ Object propertyId = null;
+ if (propertyIdAnnotation != null) {
+ // @PropertyId(propertyId) always overrides property id
+ propertyId = propertyIdAnnotation.value();
+ } else {
+ propertyId = memberField.getName();
+ }
+
+ // Ensure that the property id exists
+ Class<?> propertyType;
+
+ try {
+ propertyType = getPropertyType(propertyId);
+ } catch (BindException e) {
+ // Property id was not found, skip this field
+ continue;
+ }
+
+ Field<?> field;
+ try {
+ // Get the field from the object
+ field = (Field<?>) ReflectTools.getJavaFieldValue(
+ objectWithMemberFields, memberField);
+ } catch (Exception e) {
+ // If we cannot determine the value, just skip the field and try
+ // the next one
+ continue;
+ }
+
+ if (field == null && buildFields) {
+ Caption captionAnnotation = memberField
+ .getAnnotation(Caption.class);
+ String caption;
+ if (captionAnnotation != null) {
+ caption = captionAnnotation.value();
+ } else {
+ caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ }
+
+ // Create the component (Field)
+ field = build(caption, propertyType, fieldType);
+
+ // Store it in the field
+ try {
+ ReflectTools.setJavaFieldValue(objectWithMemberFields,
+ memberField, field);
+ } catch (IllegalArgumentException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (IllegalAccessException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (InvocationTargetException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ }
+ }
+
+ if (field != null) {
+ // Bind it to the property id
+ bind(field, propertyId);
+ }
+ }
+ }
+
+ public static class CommitException extends Exception {
+
+ public CommitException() {
+ super();
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(String message, Throwable cause) {
+ super(message, cause);
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(String message) {
+ super(message);
+ // TODO Auto-generated constructor stub
+ }
+
+ public CommitException(Throwable cause) {
+ super(cause);
+ // TODO Auto-generated constructor stub
+ }
+
+ }
+
+ public static class BindException extends RuntimeException {
+
+ public BindException(String message) {
+ super(message);
+ }
+
+ public BindException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ }
+
+ /**
+ * Builds a field and binds it to the given property id using the field
+ * binder.
+ *
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field
+ */
+ public Field<?> buildAndBind(Object propertyId) throws BindException {
+ String caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ return buildAndBind(caption, propertyId);
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field. Can be any type of {@link Field}.
+ */
+ public Field<?> buildAndBind(String caption, Object propertyId)
+ throws BindException {
+ Class<?> type = getPropertyType(propertyId);
+ return buildAndBind(caption, propertyId, Field.class);
+
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder. Ensures the new field is of the given type.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If the field could not be created
+ * @return The created and bound field. Can be any type of {@link Field}.
+ */
+
+ public <T extends Field> T buildAndBind(String caption, Object propertyId,
+ Class<T> fieldType) throws BindException {
+ Class<?> type = getPropertyType(propertyId);
+
+ T field = build(caption, type, fieldType);
+ bind(field, propertyId);
+
+ return field;
+ }
+
+ /**
+ * Creates a field based on the given data type.
+ * <p>
+ * The data type is the type that we want to edit using the field. The field
+ * type is the type of field we want to create, can be {@link Field} if any
+ * Field is good.
+ * </p>
+ *
+ * @param caption
+ * The caption for the new field
+ * @param dataType
+ * The data model type that we want to edit using the field
+ * @param fieldType
+ * The type of field that we want to create
+ * @return A Field capable of editing the given type
+ * @throws BindException
+ * If the field could not be created
+ */
+ protected <T extends Field> T build(String caption, Class<?> dataType,
+ Class<T> fieldType) throws BindException {
+ T field = getFieldFactory().createField(dataType, fieldType);
+ if (field == null) {
+ throw new BindException("Unable to build a field of type "
+ + fieldType.getName() + " for editing "
+ + dataType.getName());
+ }
+
+ field.setCaption(caption);
+ return field;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
new file mode 100644
index 0000000000..80c012cbdc
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Field;
+
+/**
+ * Factory interface for creating new Field-instances based on the data type
+ * that should be edited.
+ *
+ * @author Vaadin Ltd.
+ * @version @version@
+ * @since 7.0
+ */
+public interface FieldGroupFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the data type that we want to edit
+ *
+ * @param dataType
+ * The type that we want to edit using the field
+ * @param fieldType
+ * The type of field we want to create. If set to {@link Field}
+ * then any type of field is accepted
+ * @return A field that can be assigned to the given fieldType and that is
+ * capable of editing the given type of data
+ */
+ <T extends Field> T createField(Class<?> dataType, Class<T> fieldType);
+}
diff --git a/server/src/com/vaadin/data/fieldgroup/PropertyId.java b/server/src/com/vaadin/data/fieldgroup/PropertyId.java
new file mode 100644
index 0000000000..268047401d
--- /dev/null
+++ b/server/src/com/vaadin/data/fieldgroup/PropertyId.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PropertyId {
+ String value();
+}
diff --git a/server/src/com/vaadin/data/package.html b/server/src/com/vaadin/data/package.html
new file mode 100644
index 0000000000..a14ea1ac88
--- /dev/null
+++ b/server/src/com/vaadin/data/package.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<p>Contains interfaces for the data layer, mainly for binding typed
+data and data collections to components, and for validating data.</p>
+
+<h2>Data binding</h2>
+
+<p>The package contains a three-tiered structure for typed data
+objects and collections of them:</p>
+
+<ul>
+ <li>A {@link com.vaadin.data.Property Property} represents a
+ single, typed data value.
+
+ <li>An {@link com.vaadin.data.Item Item} embodies a set of <i>Properties</i>.
+ A locally unique (inside the {@link com.vaadin.data.Item Item})
+ Property identifier corresponds to each Property inside the Item.</li>
+ <li>A {@link com.vaadin.data.Container Container} contains a set
+ of Items, each corresponding to a locally unique Item identifier. Note
+ that Container imposes a few restrictions on the data stored in it, see
+ {@link com.vaadin.data.Container Container} for further information.</li>
+</ul>
+
+<p>For more information on the data model, see the <a
+ href="http://vaadin.com/book/-/page/datamodel.html">Data model
+chapter</a> in Book of Vaadin.</p>
+
+<h2>Buffering</h2>
+
+<p>A {@link com.vaadin.data.Buffered Buffered} implementor is able
+to track and buffer changes and commit or discard them later.</p>
+
+<h2>Validation</h2>
+
+<p>{@link com.vaadin.data.Validator Validator} implementations are
+used to validate data, typically the value of a {@link
+com.vaadin.ui.Field Field}. One or more {@link com.vaadin.data.Validator
+Validators} can be added to a {@link com.vaadin.data.Validatable
+Validatable} implementor and then used to validate the value of the
+Validatable. </p>
+
+<!-- Put @see and @since tags down here. -->
+</body>
+</html>
diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
new file mode 100644
index 0000000000..2f428d2cb6
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
@@ -0,0 +1,856 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Filterable;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.SimpleFilterable;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.util.MethodProperty.MethodException;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * An abstract base class for in-memory containers for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class and explicitly adding or removing properties is not
+ * supported. Only beans of the same type can be added to the container.
+ * </p>
+ *
+ * <p>
+ * Subclasses should implement any public methods adding items to the container,
+ * typically calling the protected methods {@link #addItem(Object, Object)},
+ * {@link #addItemAfter(Object, Object, Object)} and
+ * {@link #addItemAt(int, Object, Object)}.
+ * </p>
+ *
+ * @param <IDTYPE>
+ * The type of the item identifier
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @since 6.5
+ */
+public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
+ AbstractInMemoryContainer<IDTYPE, String, BeanItem<BEANTYPE>> implements
+ Filterable, SimpleFilterable, Sortable, ValueChangeListener,
+ PropertySetChangeNotifier {
+
+ /**
+ * Resolver that maps beans to their (item) identifiers, removing the need
+ * to explicitly specify item identifiers when there is no need to customize
+ * this.
+ *
+ * Note that beans can also be added with an explicit id even if a resolver
+ * has been set.
+ *
+ * @param <IDTYPE>
+ * @param <BEANTYPE>
+ *
+ * @since 6.5
+ */
+ public static interface BeanIdResolver<IDTYPE, BEANTYPE> extends
+ Serializable {
+ /**
+ * Return the item identifier for a bean.
+ *
+ * @param bean
+ * @return
+ */
+ public IDTYPE getIdForBean(BEANTYPE bean);
+ }
+
+ /**
+ * A item identifier resolver that returns the value of a bean property.
+ *
+ * The bean must have a getter for the property, and the getter must return
+ * an object of type IDTYPE.
+ */
+ protected class PropertyBasedBeanIdResolver implements
+ BeanIdResolver<IDTYPE, BEANTYPE> {
+
+ private final Object propertyId;
+
+ public PropertyBasedBeanIdResolver(Object propertyId) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException(
+ "Property identifier must not be null");
+ }
+ this.propertyId = propertyId;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public IDTYPE getIdForBean(BEANTYPE bean)
+ throws IllegalArgumentException {
+ VaadinPropertyDescriptor<BEANTYPE> pd = model.get(propertyId);
+ if (null == pd) {
+ throw new IllegalStateException("Property " + propertyId
+ + " not found");
+ }
+ try {
+ Property<IDTYPE> property = (Property<IDTYPE>) pd
+ .createProperty(bean);
+ return property.getValue();
+ } catch (MethodException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ }
+
+ /**
+ * The resolver that finds the item ID for a bean, or null not to use
+ * automatic resolving.
+ *
+ * Methods that add a bean without specifying an ID must not be called if no
+ * resolver has been set.
+ */
+ private BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver = null;
+
+ /**
+ * Maps all item ids in the container (including filtered) to their
+ * corresponding BeanItem.
+ */
+ private final Map<IDTYPE, BeanItem<BEANTYPE>> itemIdToItem = new HashMap<IDTYPE, BeanItem<BEANTYPE>>();
+
+ /**
+ * The type of the beans in the container.
+ */
+ private final Class<? super BEANTYPE> type;
+
+ /**
+ * A description of the properties found in beans of type {@link #type}.
+ * Determines the property ids that are present in the container.
+ */
+ private LinkedHashMap<String, VaadinPropertyDescriptor<BEANTYPE>> model;
+
+ /**
+ * Constructs a {@code AbstractBeanContainer} for beans of the given type.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ protected AbstractBeanContainer(Class<? super BEANTYPE> type) {
+ if (type == null) {
+ throw new IllegalArgumentException(
+ "The bean type passed to AbstractBeanContainer must not be null");
+ }
+ this.type = type;
+ model = BeanItem.getPropertyDescriptors((Class<BEANTYPE>) type);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return model.get(propertyId).getPropertyType();
+ }
+
+ /**
+ * Create a BeanItem for a bean using pre-parsed bean metadata (based on
+ * {@link #getBeanType()}).
+ *
+ * @param bean
+ * @return created {@link BeanItem} or null if bean is null
+ */
+ protected BeanItem<BEANTYPE> createBeanItem(BEANTYPE bean) {
+ return bean == null ? null : new BeanItem<BEANTYPE>(bean, model);
+ }
+
+ /**
+ * Returns the type of beans this Container can contain.
+ *
+ * This comes from the bean type constructor parameter, and bean metadata
+ * (including container properties) is based on this.
+ *
+ * @return
+ */
+ public Class<? super BEANTYPE> getBeanType() {
+ return type;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return model.keySet();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ int origSize = size();
+
+ internalRemoveAllItems();
+
+ // detach listeners from all Items
+ for (Item item : itemIdToItem.values()) {
+ removeAllValueChangeListeners(item);
+ }
+ itemIdToItem.clear();
+
+ // fire event only if the visible view changed, regardless of whether
+ // filtered out items were removed or not
+ if (origSize != 0) {
+ fireItemSetChange();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItem(java.lang.Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> getItem(Object itemId) {
+ // TODO return only if visible?
+ return getUnfilteredItem(itemId);
+ }
+
+ @Override
+ protected BeanItem<BEANTYPE> getUnfilteredItem(Object itemId) {
+ return itemIdToItem.get(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItemIds()
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<IDTYPE> getItemIds() {
+ return (List<IDTYPE>) super.getItemIds();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ Item item = getItem(itemId);
+ if (item == null) {
+ return null;
+ }
+ return item.getItemProperty(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ // TODO should also remove items that are filtered out
+ int origSize = size();
+ Item item = getItem(itemId);
+ int position = indexOfId(itemId);
+
+ if (internalRemoveItem(itemId)) {
+ // detach listeners from Item
+ removeAllValueChangeListeners(item);
+
+ // remove item
+ itemIdToItem.remove(itemId);
+
+ // fire event only if the visible view changed, regardless of
+ // whether filtered out items were removed or not
+ if (size() != origSize) {
+ fireItemRemoved(position, itemId);
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Re-filter the container when one of the monitored properties changes.
+ */
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ // if a property that is used in a filter is changed, refresh filtering
+ filterAll();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.Filterable#addContainerFilter(java.lang.Object,
+ * java.lang.String, boolean, boolean)
+ */
+ @Override
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ try {
+ addFilter(new SimpleStringFilter(propertyId, filterString,
+ ignoreCase, onlyMatchPrefix));
+ } catch (UnsupportedFilterException e) {
+ // the filter instance created here is always valid for in-memory
+ // containers
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Filterable#removeAllContainerFilters()
+ */
+ @Override
+ public void removeAllContainerFilters() {
+ if (!getFilters().isEmpty()) {
+ for (Item item : itemIdToItem.values()) {
+ removeAllValueChangeListeners(item);
+ }
+ removeAllFilters();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.Filterable#removeContainerFilters(java.lang
+ * .Object)
+ */
+ @Override
+ public void removeContainerFilters(Object propertyId) {
+ Collection<Filter> removedFilters = super.removeFilters(propertyId);
+ if (!removedFilters.isEmpty()) {
+ // stop listening to change events for the property
+ for (Item item : itemIdToItem.values()) {
+ removeValueChangeListener(item, propertyId);
+ }
+ }
+ }
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ addFilter(filter);
+ }
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ removeFilter(filter);
+ }
+
+ /**
+ * Make this container listen to the given property provided it notifies
+ * when its value changes.
+ *
+ * @param item
+ * The {@link Item} that contains the property
+ * @param propertyId
+ * The id of the property
+ */
+ private void addValueChangeListener(Item item, Object propertyId) {
+ Property<?> property = item.getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ // avoid multiple notifications for the same property if
+ // multiple filters are in use
+ ValueChangeNotifier notifier = (ValueChangeNotifier) property;
+ notifier.removeListener(this);
+ notifier.addListener(this);
+ }
+ }
+
+ /**
+ * Remove this container as a listener for the given property.
+ *
+ * @param item
+ * The {@link Item} that contains the property
+ * @param propertyId
+ * The id of the property
+ */
+ private void removeValueChangeListener(Item item, Object propertyId) {
+ Property<?> property = item.getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property).removeListener(this);
+ }
+ }
+
+ /**
+ * Remove this contains as a listener for all the properties in the given
+ * {@link Item}.
+ *
+ * @param item
+ * The {@link Item} that contains the properties
+ */
+ private void removeAllValueChangeListeners(Item item) {
+ for (Object propertyId : item.getItemPropertyIds()) {
+ removeValueChangeListener(item, propertyId);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getSortablePropertyIds();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sortContainer(propertyId, ascending);
+ }
+
+ @Override
+ public ItemSorter getItemSorter() {
+ return super.getItemSorter();
+ }
+
+ @Override
+ public void setItemSorter(ItemSorter itemSorter) {
+ super.setItemSorter(itemSorter);
+ }
+
+ @Override
+ protected void registerNewItem(int position, IDTYPE itemId,
+ BeanItem<BEANTYPE> item) {
+ itemIdToItem.put(itemId, item);
+
+ // add listeners to be able to update filtering on property
+ // changes
+ for (Filter filter : getFilters()) {
+ for (String propertyId : getContainerPropertyIds()) {
+ if (filter.appliesToProperty(propertyId)) {
+ // addValueChangeListener avoids adding duplicates
+ addValueChangeListener(item, propertyId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check that a bean can be added to the container (is of the correct type
+ * for the container).
+ *
+ * @param bean
+ * @return
+ */
+ private boolean validateBean(BEANTYPE bean) {
+ return bean != null && getBeanType().isAssignableFrom(bean.getClass());
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * Note: the behavior of this method changed in Vaadin 6.6 - now items are
+ * added at the very end of the unfiltered container and not after the last
+ * visible item if filtering is used.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ protected BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAtEnd(itemId, createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds the bean after the given bean.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ protected BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId,
+ IDTYPE newItemId, BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAfter(previousItemId, newItemId,
+ createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The item id for the bean to add to the container.
+ * @param bean
+ * The bean to add to the container.
+ *
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ protected BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId,
+ BEANTYPE bean) {
+ if (!validateBean(bean)) {
+ return null;
+ }
+ return internalAddItemAt(index, newItemId, createBeanItem(bean), true);
+ }
+
+ /**
+ * Adds a bean to the container using the bean item id resolver to find its
+ * identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItem(Object, Object)
+ *
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBean(BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItem(itemId, bean);
+ }
+
+ /**
+ * Adds a bean to the container after a specified item identifier, using the
+ * bean item id resolver to find its identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItemAfter(Object, Object, Object)
+ *
+ * @param previousItemId
+ * the identifier of the bean after which this bean should be
+ * added, null to add to the beginning
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId,
+ BEANTYPE bean) throws IllegalStateException,
+ IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItemAfter(previousItemId, itemId, bean);
+ }
+
+ /**
+ * Adds a bean at a specified (filtered view) position in the container
+ * using the bean item id resolver to find its identifier.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * @see #addItemAfter(Object, Object, Object)
+ *
+ * @param index
+ * the index (in the filtered view) at which to add the item
+ * @param bean
+ * the bean to add
+ * @return BeanItem<BEANTYPE> item added or null
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if an identifier cannot be resolved for the bean
+ */
+ protected BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ if (bean == null) {
+ return null;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+ return addItemAt(index, itemId, bean);
+ }
+
+ /**
+ * Adds all the beans from a {@link Collection} in one operation using the
+ * bean item identifier resolver. More efficient than adding them one by
+ * one.
+ *
+ * A bean id resolver must be set before calling this method.
+ *
+ * Note: the behavior of this method changed in Vaadin 6.6 - now items are
+ * added at the very end of the unfiltered container and not after the last
+ * visible item if filtering is used.
+ *
+ * @param collection
+ * The collection of beans to add. Must not be null.
+ * @throws IllegalStateException
+ * if no bean identifier resolver has been set
+ * @throws IllegalArgumentException
+ * if the resolver returns a null itemId for one of the beans in
+ * the collection
+ */
+ protected void addAll(Collection<? extends BEANTYPE> collection)
+ throws IllegalStateException, IllegalArgumentException {
+ boolean modified = false;
+ for (BEANTYPE bean : collection) {
+ // TODO skipping invalid beans - should not allow them in javadoc?
+ if (bean == null
+ || !getBeanType().isAssignableFrom(bean.getClass())) {
+ continue;
+ }
+ IDTYPE itemId = resolveBeanId(bean);
+ if (itemId == null) {
+ throw new IllegalArgumentException(
+ "Resolved identifier for a bean must not be null");
+ }
+
+ if (internalAddItemAtEnd(itemId, createBeanItem(bean), false) != null) {
+ modified = true;
+ }
+ }
+
+ if (modified) {
+ // Filter the contents when all items have been added
+ if (isFiltered()) {
+ filterAll();
+ } else {
+ fireItemSetChange();
+ }
+ }
+ }
+
+ /**
+ * Use the bean resolver to get the identifier for a bean.
+ *
+ * @param bean
+ * @return resolved bean identifier, null if could not be resolved
+ * @throws IllegalStateException
+ * if no bean resolver is set
+ */
+ protected IDTYPE resolveBeanId(BEANTYPE bean) {
+ if (beanIdResolver == null) {
+ throw new IllegalStateException(
+ "Bean item identifier resolver is required.");
+ }
+ return beanIdResolver.getIdForBean(bean);
+ }
+
+ /**
+ * Sets the resolver that finds the item id for a bean, or null not to use
+ * automatic resolving.
+ *
+ * Methods that add a bean without specifying an id must not be called if no
+ * resolver has been set.
+ *
+ * Note that methods taking an explicit id can be used whether a resolver
+ * has been defined or not.
+ *
+ * @param beanIdResolver
+ * to use or null to disable automatic id resolution
+ */
+ protected void setBeanIdResolver(
+ BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) {
+ this.beanIdResolver = beanIdResolver;
+ }
+
+ /**
+ * Returns the resolver that finds the item ID for a bean.
+ *
+ * @return resolver used or null if automatic item id resolving is disabled
+ */
+ public BeanIdResolver<IDTYPE, BEANTYPE> getBeanIdResolver() {
+ return beanIdResolver;
+ }
+
+ /**
+ * Create an item identifier resolver using a named bean property.
+ *
+ * @param propertyId
+ * property identifier, which must map to a getter in BEANTYPE
+ * @return created resolver
+ */
+ protected BeanIdResolver<IDTYPE, BEANTYPE> createBeanPropertyResolver(
+ Object propertyId) {
+ return new PropertyBasedBeanIdResolver(propertyId);
+ }
+
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Use addNestedContainerProperty(String) to add container properties to a "
+ + getClass().getSimpleName());
+ }
+
+ /**
+ * Adds a property for the container and all its items.
+ *
+ * Primarily for internal use, may change in future versions.
+ *
+ * @param propertyId
+ * @param propertyDescriptor
+ * @return true if the property was added
+ */
+ protected final boolean addContainerProperty(String propertyId,
+ VaadinPropertyDescriptor<BEANTYPE> propertyDescriptor) {
+ if (null == propertyId || null == propertyDescriptor) {
+ return false;
+ }
+
+ // Fails if the Property is already present
+ if (model.containsKey(propertyId)) {
+ return false;
+ }
+
+ model.put(propertyId, propertyDescriptor);
+ for (BeanItem<BEANTYPE> item : itemIdToItem.values()) {
+ item.addItemProperty(propertyId,
+ propertyDescriptor.createProperty(item.getBean()));
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Adds a nested container property for the container, e.g.
+ * "manager.address.street".
+ *
+ * All intermediate getters must exist and must return non-null values when
+ * the property value is accessed.
+ *
+ * @see NestedMethodProperty
+ *
+ * @param propertyId
+ * @return true if the property was added
+ */
+ public boolean addNestedContainerProperty(String propertyId) {
+ return addContainerProperty(propertyId, new NestedPropertyDescriptor(
+ propertyId, type));
+ }
+
+ /**
+ * Adds a nested container properties for all sub-properties of a named
+ * property to the container. The named property itself is removed from the
+ * model as its subproperties are added.
+ *
+ * All intermediate getters must exist and must return non-null values when
+ * the property value is accessed.
+ *
+ * @see NestedMethodProperty
+ * @see #addNestedContainerProperty(String)
+ *
+ * @param propertyId
+ */
+ @SuppressWarnings("unchecked")
+ public void addNestedContainerBean(String propertyId) {
+ Class<?> propertyType = getType(propertyId);
+ LinkedHashMap<String, VaadinPropertyDescriptor<Object>> pds = BeanItem
+ .getPropertyDescriptors((Class<Object>) propertyType);
+ for (String subPropertyId : pds.keySet()) {
+ String qualifiedPropertyId = propertyId + "." + subPropertyId;
+ NestedPropertyDescriptor<BEANTYPE> pd = new NestedPropertyDescriptor<BEANTYPE>(
+ qualifiedPropertyId, (Class<BEANTYPE>) type);
+ model.put(qualifiedPropertyId, pd);
+ model.remove(propertyId);
+ for (BeanItem<BEANTYPE> item : itemIdToItem.values()) {
+ item.addItemProperty(propertyId,
+ pd.createProperty(item.getBean()));
+ item.removeItemProperty(propertyId);
+ }
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+ }
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ // Fails if the Property is not present
+ if (!model.containsKey(propertyId)) {
+ return false;
+ }
+
+ // Removes the Property to Property list and types
+ model.remove(propertyId);
+
+ // If remove the Property from all Items
+ for (final Iterator<IDTYPE> i = getAllItemIds().iterator(); i.hasNext();) {
+ getUnfilteredItem(i.next()).removeItemProperty(propertyId);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/AbstractContainer.java b/server/src/com/vaadin/data/util/AbstractContainer.java
new file mode 100644
index 0000000000..7d96c2d757
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractContainer.java
@@ -0,0 +1,251 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+
+/**
+ * Abstract container class that manages event listeners and sending events to
+ * them ({@link PropertySetChangeNotifier}, {@link ItemSetChangeNotifier}).
+ *
+ * Note that this class provides the internal implementations for both types of
+ * events and notifiers as protected methods, but does not implement the
+ * {@link PropertySetChangeNotifier} and {@link ItemSetChangeNotifier}
+ * interfaces directly. This way, subclasses can choose not to implement them.
+ * Subclasses implementing those interfaces should also override the
+ * corresponding {@link #addListener()} and {@link #removeListener()} methods to
+ * make them public.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractContainer implements Container {
+
+ /**
+ * List of all Property set change event listeners.
+ */
+ private Collection<Container.PropertySetChangeListener> propertySetChangeListeners = null;
+
+ /**
+ * List of all container Item set change event listeners.
+ */
+ private Collection<Container.ItemSetChangeListener> itemSetChangeListeners = null;
+
+ /**
+ * An <code>event</code> object specifying the container whose Property set
+ * has changed.
+ *
+ * This class does not provide information about which properties were
+ * concerned by the change, but subclasses can provide additional
+ * information about the changes.
+ */
+ protected static class BasePropertySetChangeEvent extends EventObject
+ implements Container.PropertySetChangeEvent, Serializable {
+
+ protected BasePropertySetChangeEvent(Container source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ /**
+ * An <code>event</code> object specifying the container whose Item set has
+ * changed.
+ *
+ * This class does not provide information about the exact changes
+ * performed, but subclasses can add provide additional information about
+ * the changes.
+ */
+ protected static class BaseItemSetChangeEvent extends EventObject implements
+ Container.ItemSetChangeEvent, Serializable {
+
+ protected BaseItemSetChangeEvent(Container source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ // PropertySetChangeNotifier
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link PropertySetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ protected void addListener(Container.PropertySetChangeListener listener) {
+ if (getPropertySetChangeListeners() == null) {
+ setPropertySetChangeListeners(new LinkedList<Container.PropertySetChangeListener>());
+ }
+ getPropertySetChangeListeners().add(listener);
+ }
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link PropertySetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.
+ * PropertySetChangeListener)
+ */
+ protected void removeListener(Container.PropertySetChangeListener listener) {
+ if (getPropertySetChangeListeners() != null) {
+ getPropertySetChangeListeners().remove(listener);
+ }
+ }
+
+ // ItemSetChangeNotifier
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link ItemSetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ protected void addListener(Container.ItemSetChangeListener listener) {
+ if (getItemSetChangeListeners() == null) {
+ setItemSetChangeListeners(new LinkedList<Container.ItemSetChangeListener>());
+ }
+ getItemSetChangeListeners().add(listener);
+ }
+
+ /**
+ * Implementation of the corresponding method in
+ * {@link ItemSetChangeNotifier}, override with the corresponding public
+ * method and implement the interface to use this.
+ *
+ * @see ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ protected void removeListener(Container.ItemSetChangeListener listener) {
+ if (getItemSetChangeListeners() != null) {
+ getItemSetChangeListeners().remove(listener);
+ }
+ }
+
+ /**
+ * Sends a simple Property set change event to all interested listeners.
+ */
+ protected void fireContainerPropertySetChange() {
+ fireContainerPropertySetChange(new BasePropertySetChangeEvent(this));
+ }
+
+ /**
+ * Sends a Property set change event to all interested listeners.
+ *
+ * Use {@link #fireContainerPropertySetChange()} instead of this method
+ * unless additional information about the exact changes is available and
+ * should be included in the event.
+ *
+ * @param event
+ * the property change event to send, optionally with additional
+ * information
+ */
+ protected void fireContainerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ if (getPropertySetChangeListeners() != null) {
+ final Object[] l = getPropertySetChangeListeners().toArray();
+ for (int i = 0; i < l.length; i++) {
+ ((Container.PropertySetChangeListener) l[i])
+ .containerPropertySetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Sends a simple Item set change event to all interested listeners,
+ * indicating that anything in the contents may have changed (items added,
+ * removed etc.).
+ */
+ protected void fireItemSetChange() {
+ fireItemSetChange(new BaseItemSetChangeEvent(this));
+ }
+
+ /**
+ * Sends an Item set change event to all registered interested listeners.
+ *
+ * @param event
+ * the item set change event to send, optionally with additional
+ * information
+ */
+ protected void fireItemSetChange(ItemSetChangeEvent event) {
+ if (getItemSetChangeListeners() != null) {
+ final Object[] l = getItemSetChangeListeners().toArray();
+ for (int i = 0; i < l.length; i++) {
+ ((Container.ItemSetChangeListener) l[i])
+ .containerItemSetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Sets the property set change listener collection. For internal use only.
+ *
+ * @param propertySetChangeListeners
+ */
+ protected void setPropertySetChangeListeners(
+ Collection<Container.PropertySetChangeListener> propertySetChangeListeners) {
+ this.propertySetChangeListeners = propertySetChangeListeners;
+ }
+
+ /**
+ * Returns the property set change listener collection. For internal use
+ * only.
+ */
+ protected Collection<Container.PropertySetChangeListener> getPropertySetChangeListeners() {
+ return propertySetChangeListeners;
+ }
+
+ /**
+ * Sets the item set change listener collection. For internal use only.
+ *
+ * @param itemSetChangeListeners
+ */
+ protected void setItemSetChangeListeners(
+ Collection<Container.ItemSetChangeListener> itemSetChangeListeners) {
+ this.itemSetChangeListeners = itemSetChangeListeners;
+ }
+
+ /**
+ * Returns the item set change listener collection. For internal use only.
+ */
+ protected Collection<Container.ItemSetChangeListener> getItemSetChangeListeners() {
+ return itemSetChangeListeners;
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Container.PropertySetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertySetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetChangeListeners);
+ }
+ } else if (Container.ItemSetChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (itemSetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(itemSetChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
new file mode 100644
index 0000000000..b7832756f2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
@@ -0,0 +1,941 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * Abstract {@link Container} class that handles common functionality for
+ * in-memory containers. Concrete in-memory container classes can either inherit
+ * this class, inherit {@link AbstractContainer}, or implement the
+ * {@link Container} interface directly.
+ *
+ * Adding and removing items (if desired) must be implemented in subclasses by
+ * overriding the appropriate add*Item() and remove*Item() and removeAllItems()
+ * methods, calling the corresponding
+ * {@link #internalAddItemAfter(Object, Object, Item)},
+ * {@link #internalAddItemAt(int, Object, Item)},
+ * {@link #internalAddItemAtEnd(Object, Item, boolean)},
+ * {@link #internalRemoveItem(Object)} and {@link #internalRemoveAllItems()}
+ * methods.
+ *
+ * By default, adding and removing container properties is not supported, and
+ * subclasses need to implement {@link #getContainerPropertyIds()}. Optionally,
+ * subclasses can override {@link #addContainerProperty(Object, Class, Object)}
+ * and {@link #removeContainerProperty(Object)} to implement them.
+ *
+ * Features:
+ * <ul>
+ * <li> {@link Container.Ordered}
+ * <li> {@link Container.Indexed}
+ * <li> {@link Filterable} and {@link SimpleFilterable} (internal implementation,
+ * does not implement the interface directly)
+ * <li> {@link Sortable} (internal implementation, does not implement the
+ * interface directly)
+ * </ul>
+ *
+ * To implement {@link Sortable}, subclasses need to implement
+ * {@link #getSortablePropertyIds()} and call the superclass method
+ * {@link #sortContainer(Object[], boolean[])} in the method
+ * <code>sort(Object[], boolean[])</code>.
+ *
+ * To implement {@link Filterable}, subclasses need to implement the methods
+ * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
+ * (calling {@link #addFilter(Filter)}),
+ * {@link Filterable#removeAllContainerFilters()} (calling
+ * {@link #removeAllFilters()}) and
+ * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
+ * (calling {@link #removeFilter(com.vaadin.data.Container.Filter)}).
+ *
+ * To implement {@link SimpleFilterable}, subclasses also need to implement the
+ * methods
+ * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
+ * and {@link SimpleFilterable#removeContainerFilters(Object)} calling
+ * {@link #addFilter(com.vaadin.data.Container.Filter)} and
+ * {@link #removeFilters(Object)} respectively.
+ *
+ * @param <ITEMIDTYPE>
+ * the class of item identifiers in the container, use Object if can
+ * be any class
+ * @param <PROPERTYIDCLASS>
+ * the class of property identifiers for the items in the container,
+ * use Object if can be any class
+ * @param <ITEMCLASS>
+ * the (base) class of the Item instances in the container, use
+ * {@link Item} if unknown
+ *
+ * @since 6.6
+ */
+public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITEMCLASS extends Item>
+ extends AbstractContainer implements ItemSetChangeNotifier,
+ Container.Indexed {
+
+ /**
+ * An ordered {@link List} of all item identifiers in the container,
+ * including those that have been filtered out.
+ *
+ * Must not be null.
+ */
+ private List<ITEMIDTYPE> allItemIds;
+
+ /**
+ * An ordered {@link List} of item identifiers in the container after
+ * filtering, excluding those that have been filtered out.
+ *
+ * This is what the external API of the {@link Container} interface and its
+ * subinterfaces shows (e.g. {@link #size()}, {@link #nextItemId(Object)}).
+ *
+ * If null, the full item id list is used instead.
+ */
+ private List<ITEMIDTYPE> filteredItemIds;
+
+ /**
+ * Filters that are applied to the container to limit the items visible in
+ * it
+ */
+ private Set<Filter> filters = new HashSet<Filter>();
+
+ /**
+ * The item sorter which is used for sorting the container.
+ */
+ private ItemSorter itemSorter = new DefaultItemSorter();
+
+ // Constructors
+
+ /**
+ * Constructor for an abstract in-memory container.
+ */
+ protected AbstractInMemoryContainer() {
+ setAllItemIds(new ListSet<ITEMIDTYPE>());
+ }
+
+ // Container interface methods with more specific return class
+
+ // default implementation, can be overridden
+ @Override
+ public ITEMCLASS getItem(Object itemId) {
+ if (containsId(itemId)) {
+ return getUnfilteredItem(itemId);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get an item even if filtered out.
+ *
+ * For internal use only.
+ *
+ * @param itemId
+ * @return
+ */
+ protected abstract ITEMCLASS getUnfilteredItem(Object itemId);
+
+ // cannot override getContainerPropertyIds() and getItemIds(): if subclass
+ // uses Object as ITEMIDCLASS or PROPERTYIDCLASS, Collection<Object> cannot
+ // be cast to Collection<MyInterface>
+
+ // public abstract Collection<PROPERTYIDCLASS> getContainerPropertyIds();
+ // public abstract Collection<ITEMIDCLASS> getItemIds();
+
+ // Container interface method implementations
+
+ @Override
+ public int size() {
+ return getVisibleItemIds().size();
+ }
+
+ @Override
+ public boolean containsId(Object itemId) {
+ // only look at visible items after filtering
+ if (itemId == null) {
+ return false;
+ } else {
+ return getVisibleItemIds().contains(itemId);
+ }
+ }
+
+ @Override
+ public List<?> getItemIds() {
+ return Collections.unmodifiableList(getVisibleItemIds());
+ }
+
+ // Container.Ordered
+
+ @Override
+ public ITEMIDTYPE nextItemId(Object itemId) {
+ int index = indexOfId(itemId);
+ if (index >= 0 && index < size() - 1) {
+ return getIdByIndex(index + 1);
+ } else {
+ // out of bounds
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE prevItemId(Object itemId) {
+ int index = indexOfId(itemId);
+ if (index > 0) {
+ return getIdByIndex(index - 1);
+ } else {
+ // out of bounds
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE firstItemId() {
+ if (size() > 0) {
+ return getIdByIndex(0);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ITEMIDTYPE lastItemId() {
+ if (size() > 0) {
+ return getIdByIndex(size() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ return itemId.equals(firstItemId());
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ return itemId.equals(lastItemId());
+ }
+
+ // Container.Indexed
+
+ @Override
+ public ITEMIDTYPE getIdByIndex(int index) {
+ return getVisibleItemIds().get(index);
+ }
+
+ @Override
+ public int indexOfId(Object itemId) {
+ return getVisibleItemIds().indexOf(itemId);
+ }
+
+ // methods that are unsupported by default, override to support
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding items not supported. Override the relevant addItem*() methods if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing items not supported. Override the removeItem() method if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing items not supported. Override the removeAllItems() method if required as specified in AbstractInMemoryContainer javadoc.");
+ }
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Adding container properties not supported. Override the addContainerProperty() method if required.");
+ }
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Removing container properties not supported. Override the addContainerProperty() method if required.");
+ }
+
+ // ItemSetChangeNotifier
+
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ // internal methods
+
+ // Filtering support
+
+ /**
+ * Filter the view to recreate the visible item list from the unfiltered
+ * items, and send a notification if the set of visible items changed in any
+ * way.
+ */
+ protected void filterAll() {
+ if (doFilterContainer(!getFilters().isEmpty())) {
+ fireItemSetChange();
+ }
+ }
+
+ /**
+ * Filters the data in the container and updates internal data structures.
+ * This method should reset any internal data structures and then repopulate
+ * them so {@link #getItemIds()} and other methods only return the filtered
+ * items.
+ *
+ * @param hasFilters
+ * true if filters has been set for the container, false
+ * otherwise
+ * @return true if the item set has changed as a result of the filtering
+ */
+ protected boolean doFilterContainer(boolean hasFilters) {
+ if (!hasFilters) {
+ boolean changed = getAllItemIds().size() != getVisibleItemIds()
+ .size();
+ setFilteredItemIds(null);
+ return changed;
+ }
+
+ // Reset filtered list
+ List<ITEMIDTYPE> originalFilteredItemIds = getFilteredItemIds();
+ boolean wasUnfiltered = false;
+ if (originalFilteredItemIds == null) {
+ originalFilteredItemIds = Collections.emptyList();
+ wasUnfiltered = true;
+ }
+ setFilteredItemIds(new ListSet<ITEMIDTYPE>());
+
+ // Filter
+ boolean equal = true;
+ Iterator<ITEMIDTYPE> origIt = originalFilteredItemIds.iterator();
+ for (final Iterator<ITEMIDTYPE> i = getAllItemIds().iterator(); i
+ .hasNext();) {
+ final ITEMIDTYPE id = i.next();
+ if (passesFilters(id)) {
+ // filtered list comes from the full list, can use ==
+ equal = equal && origIt.hasNext() && origIt.next() == id;
+ getFilteredItemIds().add(id);
+ }
+ }
+
+ return (wasUnfiltered && !getAllItemIds().isEmpty()) || !equal
+ || origIt.hasNext();
+ }
+
+ /**
+ * Checks if the given itemId passes the filters set for the container. The
+ * caller should make sure the itemId exists in the container. For
+ * non-existing itemIds the behavior is undefined.
+ *
+ * @param itemId
+ * An itemId that exists in the container.
+ * @return true if the itemId passes all filters or no filters are set,
+ * false otherwise.
+ */
+ protected boolean passesFilters(Object itemId) {
+ ITEMCLASS item = getUnfilteredItem(itemId);
+ if (getFilters().isEmpty()) {
+ return true;
+ }
+ final Iterator<Filter> i = getFilters().iterator();
+ while (i.hasNext()) {
+ final Filter f = i.next();
+ if (!f.passesFilter(itemId, item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Adds a container filter and re-filter the view.
+ *
+ * The filter must implement Filter and its sub-filters (if any) must also
+ * be in-memory filterable.
+ *
+ * This can be used to implement
+ * {@link Filterable#addContainerFilter(com.vaadin.data.Container.Filter)}
+ * and optionally also
+ * {@link SimpleFilterable#addContainerFilter(Object, String, boolean, boolean)}
+ * (with {@link SimpleStringFilter}).
+ *
+ * Note that in some cases, incompatible filters cannot be detected when
+ * added and an {@link UnsupportedFilterException} may occur when performing
+ * filtering.
+ *
+ * @throws UnsupportedFilterException
+ * if the filter is detected as not supported by the container
+ */
+ protected void addFilter(Filter filter) throws UnsupportedFilterException {
+ getFilters().add(filter);
+ filterAll();
+ }
+
+ /**
+ * Remove a specific container filter and re-filter the view (if necessary).
+ *
+ * This can be used to implement
+ * {@link Filterable#removeContainerFilter(com.vaadin.data.Container.Filter)}
+ * .
+ */
+ protected void removeFilter(Filter filter) {
+ for (Iterator<Filter> iterator = getFilters().iterator(); iterator
+ .hasNext();) {
+ Filter f = iterator.next();
+ if (f.equals(filter)) {
+ iterator.remove();
+ filterAll();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Remove all container filters for all properties and re-filter the view.
+ *
+ * This can be used to implement
+ * {@link Filterable#removeAllContainerFilters()}.
+ */
+ protected void removeAllFilters() {
+ if (getFilters().isEmpty()) {
+ return;
+ }
+ getFilters().clear();
+ filterAll();
+ }
+
+ /**
+ * Checks if there is a filter that applies to a given property.
+ *
+ * @param propertyId
+ * @return true if there is an active filter for the property
+ */
+ protected boolean isPropertyFiltered(Object propertyId) {
+ if (getFilters().isEmpty() || propertyId == null) {
+ return false;
+ }
+ final Iterator<Filter> i = getFilters().iterator();
+ while (i.hasNext()) {
+ final Filter f = i.next();
+ if (f.appliesToProperty(propertyId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Remove all container filters for a given property identifier and
+ * re-filter the view. This also removes filters applying to multiple
+ * properties including the one identified by propertyId.
+ *
+ * This can be used to implement
+ * {@link Filterable#removeContainerFilters(Object)}.
+ *
+ * @param propertyId
+ * @return Collection<Filter> removed filters
+ */
+ protected Collection<Filter> removeFilters(Object propertyId) {
+ if (getFilters().isEmpty() || propertyId == null) {
+ return Collections.emptyList();
+ }
+ List<Filter> removedFilters = new LinkedList<Filter>();
+ for (Iterator<Filter> iterator = getFilters().iterator(); iterator
+ .hasNext();) {
+ Filter f = iterator.next();
+ if (f.appliesToProperty(propertyId)) {
+ removedFilters.add(f);
+ iterator.remove();
+ }
+ }
+ if (!removedFilters.isEmpty()) {
+ filterAll();
+ return removedFilters;
+ }
+ return Collections.emptyList();
+ }
+
+ // sorting
+
+ /**
+ * Returns the ItemSorter used for comparing items in a sort. See
+ * {@link #setItemSorter(ItemSorter)} for more information.
+ *
+ * @return The ItemSorter used for comparing two items in a sort.
+ */
+ protected ItemSorter getItemSorter() {
+ return itemSorter;
+ }
+
+ /**
+ * Sets the ItemSorter used for comparing items in a sort. The
+ * {@link ItemSorter#compare(Object, Object)} method is called with item ids
+ * to perform the sorting. A default ItemSorter is used if this is not
+ * explicitly set.
+ *
+ * @param itemSorter
+ * The ItemSorter used for comparing two items in a sort (not
+ * null).
+ */
+ protected void setItemSorter(ItemSorter itemSorter) {
+ this.itemSorter = itemSorter;
+ }
+
+ /**
+ * Sort base implementation to be used to implement {@link Sortable}.
+ *
+ * Subclasses should call this from a public
+ * {@link #sort(Object[], boolean[])} method when implementing Sortable.
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ protected void sortContainer(Object[] propertyId, boolean[] ascending) {
+ if (!(this instanceof Sortable)) {
+ throw new UnsupportedOperationException(
+ "Cannot sort a Container that does not implement Sortable");
+ }
+
+ // Set up the item sorter for the sort operation
+ getItemSorter().setSortProperties((Sortable) this, propertyId,
+ ascending);
+
+ // Perform the actual sort
+ doSort();
+
+ // Post sort updates
+ if (isFiltered()) {
+ filterAll();
+ } else {
+ fireItemSetChange();
+ }
+
+ }
+
+ /**
+ * Perform the sorting of the data structures in the container. This is
+ * invoked when the <code>itemSorter</code> has been prepared for the sort
+ * operation. Typically this method calls
+ * <code>Collections.sort(aCollection, getItemSorter())</code> on all arrays
+ * (containing item ids) that need to be sorted.
+ *
+ */
+ protected void doSort() {
+ Collections.sort(getAllItemIds(), getItemSorter());
+ }
+
+ /**
+ * Returns the sortable property identifiers for the container. Can be used
+ * to implement {@link Sortable#getSortableContainerPropertyIds()}.
+ */
+ protected Collection<?> getSortablePropertyIds() {
+ LinkedList<Object> sortables = new LinkedList<Object>();
+ for (Object propertyId : getContainerPropertyIds()) {
+ Class<?> propertyType = getType(propertyId);
+ if (Comparable.class.isAssignableFrom(propertyType)
+ || propertyType.isPrimitive()) {
+ sortables.add(propertyId);
+ }
+ }
+ return sortables;
+ }
+
+ // removing items
+
+ /**
+ * Removes all items from the internal data structures of this class. This
+ * can be used to implement {@link #removeAllItems()} in subclasses.
+ *
+ * No notification is sent, the caller has to fire a suitable item set
+ * change notification.
+ */
+ protected void internalRemoveAllItems() {
+ // Removes all Items
+ getAllItemIds().clear();
+ if (isFiltered()) {
+ getFilteredItemIds().clear();
+ }
+ }
+
+ /**
+ * Removes a single item from the internal data structures of this class.
+ * This can be used to implement {@link #removeItem(Object)} in subclasses.
+ *
+ * No notification is sent, the caller has to fire a suitable item set
+ * change notification.
+ *
+ * @param itemId
+ * the identifier of the item to remove
+ * @return true if an item was successfully removed, false if failed to
+ * remove or no such item
+ */
+ protected boolean internalRemoveItem(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ boolean result = getAllItemIds().remove(itemId);
+ if (result && isFiltered()) {
+ getFilteredItemIds().remove(itemId);
+ }
+
+ return result;
+ }
+
+ // adding items
+
+ /**
+ * Adds the bean to all internal data structures at the given position.
+ * Fails if an item with itemId is already in the container. Returns a the
+ * item if it was added successfully, null otherwise.
+ *
+ * <p>
+ * Caller should initiate filtering after calling this method.
+ * </p>
+ *
+ * For internal use only - subclasses should use
+ * {@link #internalAddItemAtEnd(Object, Item, boolean)},
+ * {@link #internalAddItemAt(int, Object, Item, boolean)} and
+ * {@link #internalAddItemAfter(Object, Object, Item, boolean)} instead.
+ *
+ * @param position
+ * The position at which the item should be inserted in the
+ * unfiltered collection of items
+ * @param itemId
+ * The item identifier for the item to insert
+ * @param item
+ * The item to insert
+ *
+ * @return ITEMCLASS if the item was added successfully, null otherwise
+ */
+ private ITEMCLASS internalAddAt(int position, ITEMIDTYPE itemId,
+ ITEMCLASS item) {
+ if (position < 0 || position > getAllItemIds().size() || itemId == null
+ || item == null) {
+ return null;
+ }
+ // Make sure that the item has not been added previously
+ if (getAllItemIds().contains(itemId)) {
+ return null;
+ }
+
+ // "filteredList" will be updated in filterAll() which should be invoked
+ // by the caller after calling this method.
+ getAllItemIds().add(position, itemId);
+ registerNewItem(position, itemId, item);
+
+ return item;
+ }
+
+ /**
+ * Add an item at the end of the container, and perform filtering if
+ * necessary. An event is fired if the filtered view changes.
+ *
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAtEnd(ITEMIDTYPE newItemId,
+ ITEMCLASS item, boolean filter) {
+ ITEMCLASS newItem = internalAddAt(getAllItemIds().size(), newItemId,
+ item);
+ if (newItem != null && filter) {
+ // TODO filter only this item, use fireItemAdded()
+ filterAll();
+ if (!isFiltered()) {
+ // TODO hack: does not detect change in filterAll() in this case
+ fireItemAdded(indexOfId(newItemId), newItemId, item);
+ }
+ }
+ return newItem;
+ }
+
+ /**
+ * Add an item after a given (visible) item, and perform filtering. An event
+ * is fired if the filtered view changes.
+ *
+ * The new item is added at the beginning if previousItemId is null.
+ *
+ * @param previousItemId
+ * item id of a visible item after which to add the new item, or
+ * null to add at the beginning
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAfter(ITEMIDTYPE previousItemId,
+ ITEMIDTYPE newItemId, ITEMCLASS item, boolean filter) {
+ // only add if the previous item is visible
+ ITEMCLASS newItem = null;
+ if (previousItemId == null) {
+ newItem = internalAddAt(0, newItemId, item);
+ } else if (containsId(previousItemId)) {
+ newItem = internalAddAt(
+ getAllItemIds().indexOf(previousItemId) + 1, newItemId,
+ item);
+ }
+ if (newItem != null && filter) {
+ // TODO filter only this item, use fireItemAdded()
+ filterAll();
+ if (!isFiltered()) {
+ // TODO hack: does not detect change in filterAll() in this case
+ fireItemAdded(indexOfId(newItemId), newItemId, item);
+ }
+ }
+ return newItem;
+ }
+
+ /**
+ * Add an item at a given (visible after filtering) item index, and perform
+ * filtering. An event is fired if the filtered view changes.
+ *
+ * @param index
+ * position where to add the item (visible/view index)
+ * @param newItemId
+ * @param item
+ * new item to add
+ * @param filter
+ * true to perform filtering and send event after adding the
+ * item, false to skip these operations for batch inserts - if
+ * false, caller needs to make sure these operations are
+ * performed at the end of the batch
+ * @return item added or null if no item was added
+ */
+ protected ITEMCLASS internalAddItemAt(int index, ITEMIDTYPE newItemId,
+ ITEMCLASS item, boolean filter) {
+ if (index < 0 || index > size()) {
+ return null;
+ } else if (index == 0) {
+ // add before any item, visible or not
+ return internalAddItemAfter(null, newItemId, item, filter);
+ } else {
+ // if index==size(), adds immediately after last visible item
+ return internalAddItemAfter(getIdByIndex(index - 1), newItemId,
+ item, filter);
+ }
+ }
+
+ /**
+ * Registers a new item as having been added to the container. This can
+ * involve storing the item or any relevant information about it in internal
+ * container-specific collections if necessary, as well as registering
+ * listeners etc.
+ *
+ * The full identifier list in {@link AbstractInMemoryContainer} has already
+ * been updated to reflect the new item when this method is called.
+ *
+ * @param position
+ * @param itemId
+ * @param item
+ */
+ protected void registerNewItem(int position, ITEMIDTYPE itemId,
+ ITEMCLASS item) {
+ }
+
+ // item set change notifications
+
+ /**
+ * Notify item set change listeners that an item has been added to the
+ * container.
+ *
+ * Unless subclasses specify otherwise, the default notification indicates a
+ * full refresh.
+ *
+ * @param postion
+ * position of the added item in the view (if visible)
+ * @param itemId
+ * id of the added item
+ * @param item
+ * the added item
+ */
+ protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) {
+ fireItemSetChange();
+ }
+
+ /**
+ * Notify item set change listeners that an item has been removed from the
+ * container.
+ *
+ * Unless subclasses specify otherwise, the default notification indicates a
+ * full refresh.
+ *
+ * @param postion
+ * position of the removed item in the view prior to removal (if
+ * was visible)
+ * @param itemId
+ * id of the removed item, of type {@link Object} to satisfy
+ * {@link Container#removeItem(Object)} API
+ */
+ protected void fireItemRemoved(int position, Object itemId) {
+ fireItemSetChange();
+ }
+
+ // visible and filtered item identifier lists
+
+ /**
+ * Returns the internal list of visible item identifiers after filtering.
+ *
+ * For internal use only.
+ */
+ protected List<ITEMIDTYPE> getVisibleItemIds() {
+ if (isFiltered()) {
+ return getFilteredItemIds();
+ } else {
+ return getAllItemIds();
+ }
+ }
+
+ /**
+ * Returns true is the container has active filters.
+ *
+ * @return true if the container is currently filtered
+ */
+ protected boolean isFiltered() {
+ return filteredItemIds != null;
+ }
+
+ /**
+ * Internal helper method to set the internal list of filtered item
+ * identifiers. Should not be used outside this class except for
+ * implementing clone(), may disappear from future versions.
+ *
+ * @param filteredItemIds
+ */
+ @Deprecated
+ protected void setFilteredItemIds(List<ITEMIDTYPE> filteredItemIds) {
+ this.filteredItemIds = filteredItemIds;
+ }
+
+ /**
+ * Internal helper method to get the internal list of filtered item
+ * identifiers. Should not be used outside this class except for
+ * implementing clone(), may disappear from future versions - use
+ * {@link #getVisibleItemIds()} in other contexts.
+ *
+ * @return List<ITEMIDTYPE>
+ */
+ protected List<ITEMIDTYPE> getFilteredItemIds() {
+ return filteredItemIds;
+ }
+
+ /**
+ * Internal helper method to set the internal list of all item identifiers.
+ * Should not be used outside this class except for implementing clone(),
+ * may disappear from future versions.
+ *
+ * @param allItemIds
+ */
+ @Deprecated
+ protected void setAllItemIds(List<ITEMIDTYPE> allItemIds) {
+ this.allItemIds = allItemIds;
+ }
+
+ /**
+ * Internal helper method to get the internal list of all item identifiers.
+ * Avoid using this method outside this class, may disappear in future
+ * versions.
+ *
+ * @return List<ITEMIDTYPE>
+ */
+ protected List<ITEMIDTYPE> getAllItemIds() {
+ return allItemIds;
+ }
+
+ /**
+ * Set the internal collection of filters without performing filtering.
+ *
+ * This method is mostly for internal use, use
+ * {@link #addFilter(com.vaadin.data.Container.Filter)} and
+ * <code>remove*Filter*</code> (which also re-filter the container) instead
+ * when possible.
+ *
+ * @param filters
+ */
+ protected void setFilters(Set<Filter> filters) {
+ this.filters = filters;
+ }
+
+ /**
+ * Returns the internal collection of filters. The returned collection
+ * should not be modified by callers outside this class.
+ *
+ * @return Set<Filter>
+ */
+ protected Set<Filter> getFilters() {
+ return filters;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/AbstractProperty.java b/server/src/com/vaadin/data/util/AbstractProperty.java
new file mode 100644
index 0000000000..373a8dfd58
--- /dev/null
+++ b/server/src/com/vaadin/data/util/AbstractProperty.java
@@ -0,0 +1,226 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+
+import com.vaadin.data.Property;
+
+/**
+ * Abstract base class for {@link Property} implementations.
+ *
+ * Handles listener management for {@link ValueChangeListener}s and
+ * {@link ReadOnlyStatusChangeListener}s.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractProperty<T> implements Property<T>,
+ Property.ValueChangeNotifier, Property.ReadOnlyStatusChangeNotifier {
+
+ /**
+ * List of listeners who are interested in the read-only status changes of
+ * the Property
+ */
+ private LinkedList<ReadOnlyStatusChangeListener> readOnlyStatusChangeListeners = null;
+
+ /**
+ * List of listeners who are interested in the value changes of the Property
+ */
+ private LinkedList<ValueChangeListener> valueChangeListeners = null;
+
+ /**
+ * Is the Property read-only?
+ */
+ private boolean readOnly;
+
+ /**
+ * {@inheritDoc}
+ *
+ * Override for additional restrictions on what is considered a read-only
+ * property.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ boolean oldStatus = isReadOnly();
+ readOnly = newStatus;
+ if (oldStatus != isReadOnly()) {
+ fireReadOnlyStatusChange();
+ }
+ }
+
+ /**
+ * Returns the value of the <code>Property</code> in human readable textual
+ * format.
+ *
+ * @return String representation of the value stored in the Property
+ * @deprecated use {@link #getValue()} instead and possibly toString on that
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use Property.getValue() instead of " + getClass()
+ + ".toString()");
+ }
+
+ /* Events */
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has been changed.
+ */
+ protected static class ReadOnlyStatusChangeEvent extends
+ java.util.EventObject implements Property.ReadOnlyStatusChangeEvent {
+
+ /**
+ * Constructs a new read-only status change event for this object.
+ *
+ * @param source
+ * source object of the event.
+ */
+ protected ReadOnlyStatusChangeEvent(Property source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property whose read-only state has changed.
+ *
+ * @return source Property of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ /**
+ * Registers a new read-only status change listener for this Property.
+ *
+ * @param listener
+ * the new Listener to be registered.
+ */
+ @Override
+ public void addListener(Property.ReadOnlyStatusChangeListener listener) {
+ if (readOnlyStatusChangeListeners == null) {
+ readOnlyStatusChangeListeners = new LinkedList<ReadOnlyStatusChangeListener>();
+ }
+ readOnlyStatusChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously registered read-only status change listener.
+ *
+ * @param listener
+ * the listener to be removed.
+ */
+ @Override
+ public void removeListener(Property.ReadOnlyStatusChangeListener listener) {
+ if (readOnlyStatusChangeListeners != null) {
+ readOnlyStatusChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a read only status change event to all registered listeners.
+ */
+ protected void fireReadOnlyStatusChange() {
+ if (readOnlyStatusChangeListeners != null) {
+ final Object[] l = readOnlyStatusChangeListeners.toArray();
+ final Property.ReadOnlyStatusChangeEvent event = new ReadOnlyStatusChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ReadOnlyStatusChangeListener) l[i])
+ .readOnlyStatusChange(event);
+ }
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying the Property whose value has been
+ * changed.
+ */
+ private static class ValueChangeEvent extends java.util.EventObject
+ implements Property.ValueChangeEvent {
+
+ /**
+ * Constructs a new value change event for this object.
+ *
+ * @param source
+ * source object of the event.
+ */
+ protected ValueChangeEvent(Property source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property whose value has changed.
+ *
+ * @return source Property of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ @Override
+ public void addListener(ValueChangeListener listener) {
+ if (valueChangeListeners == null) {
+ valueChangeListeners = new LinkedList<ValueChangeListener>();
+ }
+ valueChangeListeners.add(listener);
+
+ }
+
+ @Override
+ public void removeListener(ValueChangeListener listener) {
+ if (valueChangeListeners != null) {
+ valueChangeListeners.remove(listener);
+ }
+
+ }
+
+ /**
+ * Sends a value change event to all registered listeners.
+ */
+ protected void fireValueChange() {
+ if (valueChangeListeners != null) {
+ final Object[] l = valueChangeListeners.toArray();
+ final Property.ValueChangeEvent event = new ValueChangeEvent(this);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ValueChangeListener) l[i]).valueChange(event);
+ }
+ }
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) {
+ if (valueChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections.unmodifiableCollection(valueChangeListeners);
+ }
+ } else if (Property.ReadOnlyStatusChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (readOnlyStatusChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(readOnlyStatusChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanContainer.java b/server/src/com/vaadin/data/util/BeanContainer.java
new file mode 100644
index 0000000000..bc1ee3c39e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanContainer.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+/**
+ * An in-memory container for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class. Only beans of the same type can be added to the
+ * container.
+ * </p>
+ *
+ * <p>
+ * In BeanContainer (unlike {@link BeanItemContainer}), the item IDs do not have
+ * to be the beans themselves. The container can be used either with explicit
+ * item IDs or the item IDs can be generated when adding beans.
+ * </p>
+ *
+ * <p>
+ * To use explicit item IDs, use the methods {@link #addItem(Object, Object)},
+ * {@link #addItemAfter(Object, Object, Object)} and
+ * {@link #addItemAt(int, Object, Object)}.
+ * </p>
+ *
+ * <p>
+ * If a bean id resolver is set using
+ * {@link #setBeanIdResolver(com.vaadin.data.util.AbstractBeanContainer.BeanIdResolver)}
+ * or {@link #setBeanIdProperty(Object)}, the methods {@link #addBean(Object)},
+ * {@link #addBeanAfter(Object, Object)}, {@link #addBeanAt(int, Object)} and
+ * {@link #addAll(java.util.Collection)} can be used to add items to the
+ * container. If one of these methods is called, the resolver is used to
+ * generate an identifier for the item (must not return null).
+ * </p>
+ *
+ * <p>
+ * Note that explicit item identifiers can also be used when a resolver has been
+ * set by calling the addItem*() methods - the resolver is only used when adding
+ * beans using the addBean*() or {@link #addAll(Collection)} methods.
+ * </p>
+ *
+ * <p>
+ * It is not possible to add additional properties to the container and nested
+ * bean properties are not supported.
+ * </p>
+ *
+ * @param <IDTYPE>
+ * The type of the item identifier
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @see AbstractBeanContainer
+ * @see BeanItemContainer
+ *
+ * @since 6.5
+ */
+public class BeanContainer<IDTYPE, BEANTYPE> extends
+ AbstractBeanContainer<IDTYPE, BEANTYPE> {
+
+ public BeanContainer(Class<? super BEANTYPE> type) {
+ super(type);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItem(IDTYPE itemId, BEANTYPE bean) {
+ if (itemId != null && bean != null) {
+ return super.addItem(itemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Adds the bean after the given item id.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItemAfter(IDTYPE previousItemId,
+ IDTYPE newItemId, BEANTYPE bean) {
+ if (newItemId != null && bean != null) {
+ return super.addItemAfter(previousItemId, newItemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The item id for the bean to add to the container.
+ * @param bean
+ * The bean to add to the container.
+ *
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ @Override
+ public BeanItem<BEANTYPE> addItemAt(int index, IDTYPE newItemId,
+ BEANTYPE bean) {
+ if (newItemId != null && bean != null) {
+ return super.addItemAt(index, newItemId, bean);
+ } else {
+ return null;
+ }
+ }
+
+ // automatic item id resolution
+
+ /**
+ * Sets the bean id resolver to use a property of the beans as the
+ * identifier.
+ *
+ * @param propertyId
+ * the identifier of the property to use to find item identifiers
+ */
+ public void setBeanIdProperty(Object propertyId) {
+ setBeanIdResolver(createBeanPropertyResolver(propertyId));
+ }
+
+ @Override
+ // overridden to make public
+ public void setBeanIdResolver(
+ BeanIdResolver<IDTYPE, BEANTYPE> beanIdResolver) {
+ super.setBeanIdResolver(beanIdResolver);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBean(BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBean(bean);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBeanAfter(IDTYPE previousItemId, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBeanAfter(previousItemId, bean);
+ }
+
+ @Override
+ // overridden to make public
+ public BeanItem<BEANTYPE> addBeanAt(int index, BEANTYPE bean)
+ throws IllegalStateException, IllegalArgumentException {
+ return super.addBeanAt(index, bean);
+ }
+
+ @Override
+ // overridden to make public
+ public void addAll(Collection<? extends BEANTYPE> collection)
+ throws IllegalStateException {
+ super.addAll(collection);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanItem.java b/server/src/com/vaadin/data/util/BeanItem.java
new file mode 100644
index 0000000000..94439471f5
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanItem.java
@@ -0,0 +1,269 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A wrapper class for adding the Item interface to any Java Bean.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class BeanItem<BT> extends PropertysetItem {
+
+ /**
+ * The bean which this Item is based on.
+ */
+ private final BT bean;
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all properties
+ * of a Java Bean to it. The properties are identified by their respective
+ * bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ *
+ */
+ public BeanItem(BT bean) {
+ this(bean, getPropertyDescriptors((Class<BT>) bean.getClass()));
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> using a pre-computed set
+ * of properties. The properties are identified by their respective bean
+ * names.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyDescriptors
+ * pre-computed property descriptors
+ */
+ BeanItem(BT bean,
+ Map<String, VaadinPropertyDescriptor<BT>> propertyDescriptors) {
+
+ this.bean = bean;
+
+ for (VaadinPropertyDescriptor<BT> pd : propertyDescriptors.values()) {
+ addItemProperty(pd.getName(), pd.createProperty(bean));
+ }
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all listed
+ * properties of a Java Bean to it - in specified order. The properties are
+ * identified by their respective bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyIds
+ * id of the property.
+ */
+ public BeanItem(BT bean, Collection<?> propertyIds) {
+
+ this.bean = bean;
+
+ // Create bean information
+ LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pds = getPropertyDescriptors((Class<BT>) bean
+ .getClass());
+
+ // Add all the bean properties as MethodProperties to this Item
+ for (Object id : propertyIds) {
+ VaadinPropertyDescriptor<BT> pd = pds.get(id);
+ if (pd != null) {
+ addItemProperty(pd.getName(), pd.createProperty(bean));
+ }
+ }
+
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>BeanItem</code> and adds all listed
+ * properties of a Java Bean to it - in specified order. The properties are
+ * identified by their respective bean names.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param bean
+ * the Java Bean to copy properties from.
+ * @param propertyIds
+ * ids of the properties.
+ */
+ public BeanItem(BT bean, String[] propertyIds) {
+ this(bean, Arrays.asList(propertyIds));
+ }
+
+ /**
+ * <p>
+ * Perform introspection on a Java Bean class to find its properties.
+ * </p>
+ *
+ * <p>
+ * Note : This version only supports introspectable bean properties and
+ * their getter and setter methods. Stand-alone <code>is</code> and
+ * <code>are</code> methods are not supported.
+ * </p>
+ *
+ * @param beanClass
+ * the Java Bean class to get properties for.
+ * @return an ordered map from property names to property descriptors
+ */
+ static <BT> LinkedHashMap<String, VaadinPropertyDescriptor<BT>> getPropertyDescriptors(
+ final Class<BT> beanClass) {
+ final LinkedHashMap<String, VaadinPropertyDescriptor<BT>> pdMap = new LinkedHashMap<String, VaadinPropertyDescriptor<BT>>();
+
+ // Try to introspect, if it fails, we just have an empty Item
+ try {
+ List<PropertyDescriptor> propertyDescriptors = getBeanPropertyDescriptor(beanClass);
+
+ // Add all the bean properties as MethodProperties to this Item
+ // later entries on the list overwrite earlier ones
+ for (PropertyDescriptor pd : propertyDescriptors) {
+ final Method getMethod = pd.getReadMethod();
+ if ((getMethod != null)
+ && getMethod.getDeclaringClass() != Object.class) {
+ VaadinPropertyDescriptor<BT> vaadinPropertyDescriptor = new MethodPropertyDescriptor<BT>(
+ pd.getName(), pd.getPropertyType(),
+ pd.getReadMethod(), pd.getWriteMethod());
+ pdMap.put(pd.getName(), vaadinPropertyDescriptor);
+ }
+ }
+ } catch (final java.beans.IntrospectionException ignored) {
+ }
+
+ return pdMap;
+ }
+
+ /**
+ * Returns the property descriptors of a class or an interface.
+ *
+ * For an interface, superinterfaces are also iterated as Introspector does
+ * not take them into account (Oracle Java bug 4275879), but in that case,
+ * both the setter and the getter for a property must be in the same
+ * interface and should not be overridden in subinterfaces for the discovery
+ * to work correctly.
+ *
+ * For interfaces, the iteration is depth first and the properties of
+ * superinterfaces are returned before those of their subinterfaces.
+ *
+ * @param beanClass
+ * @return
+ * @throws IntrospectionException
+ */
+ private static List<PropertyDescriptor> getBeanPropertyDescriptor(
+ final Class<?> beanClass) throws IntrospectionException {
+ // Oracle bug 4275879: Introspector does not consider superinterfaces of
+ // an interface
+ if (beanClass.isInterface()) {
+ List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>();
+
+ for (Class<?> cls : beanClass.getInterfaces()) {
+ propertyDescriptors.addAll(getBeanPropertyDescriptor(cls));
+ }
+
+ BeanInfo info = Introspector.getBeanInfo(beanClass);
+ propertyDescriptors.addAll(Arrays.asList(info
+ .getPropertyDescriptors()));
+
+ return propertyDescriptors;
+ } else {
+ BeanInfo info = Introspector.getBeanInfo(beanClass);
+ return Arrays.asList(info.getPropertyDescriptors());
+ }
+ }
+
+ /**
+ * Expands nested bean properties by replacing a top-level property with
+ * some or all of its sub-properties. The expansion is not recursive.
+ *
+ * @param propertyId
+ * property id for the property whose sub-properties are to be
+ * expanded,
+ * @param subPropertyIds
+ * sub-properties to expand, all sub-properties are expanded if
+ * not specified
+ */
+ public void expandProperty(String propertyId, String... subPropertyIds) {
+ Set<String> subPropertySet = new HashSet<String>(
+ Arrays.asList(subPropertyIds));
+
+ if (0 == subPropertyIds.length) {
+ // Enumerate all sub-properties
+ Class<?> propertyType = getItemProperty(propertyId).getType();
+ Map<String, ?> pds = getPropertyDescriptors(propertyType);
+ subPropertySet.addAll(pds.keySet());
+ }
+
+ for (String subproperty : subPropertySet) {
+ String qualifiedPropertyId = propertyId + "." + subproperty;
+ addNestedProperty(qualifiedPropertyId);
+ }
+
+ removeItemProperty(propertyId);
+ }
+
+ /**
+ * Adds a nested property to the item.
+ *
+ * @param nestedPropertyId
+ * property id to add. This property must not exist in the item
+ * already and must of of form "field1.field2" where field2 is a
+ * field in the object referenced to by field1
+ */
+ public void addNestedProperty(String nestedPropertyId) {
+ addItemProperty(nestedPropertyId, new NestedMethodProperty<Object>(
+ getBean(), nestedPropertyId));
+ }
+
+ /**
+ * Gets the underlying JavaBean object.
+ *
+ * @return the bean object.
+ */
+ public BT getBean() {
+ return bean;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/BeanItemContainer.java b/server/src/com/vaadin/data/util/BeanItemContainer.java
new file mode 100644
index 0000000000..dc4deaebdc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/BeanItemContainer.java
@@ -0,0 +1,241 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+/**
+ * An in-memory container for JavaBeans.
+ *
+ * <p>
+ * The properties of the container are determined automatically by introspecting
+ * the used JavaBean class. Only beans of the same type can be added to the
+ * container.
+ * </p>
+ *
+ * <p>
+ * BeanItemContainer uses the beans themselves as identifiers. The
+ * {@link Object#hashCode()} of a bean is used when storing and looking up beans
+ * so it must not change during the lifetime of the bean (it should not depend
+ * on any part of the bean that can be modified). Typically this restricts the
+ * implementation of {@link Object#equals(Object)} as well in order for it to
+ * fulfill the contract between {@code equals()} and {@code hashCode()}.
+ * </p>
+ *
+ * <p>
+ * To add items to the container, use the methods {@link #addBean(Object)},
+ * {@link #addBeanAfter(Object, Object)} and {@link #addBeanAt(int, Object)}.
+ * Also {@link #addItem(Object)}, {@link #addItemAfter(Object, Object)} and
+ * {@link #addItemAt(int, Object)} can be used as synonyms for them.
+ * </p>
+ *
+ * <p>
+ * It is not possible to add additional properties to the container and nested
+ * bean properties are not supported.
+ * </p>
+ *
+ * @param <BEANTYPE>
+ * The type of the Bean
+ *
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class BeanItemContainer<BEANTYPE> extends
+ AbstractBeanContainer<BEANTYPE, BEANTYPE> {
+
+ /**
+ * Bean identity resolver that returns the bean itself as its item
+ * identifier.
+ *
+ * This corresponds to the old behavior of {@link BeanItemContainer}, and
+ * requires suitable (identity-based) equals() and hashCode() methods on the
+ * beans.
+ *
+ * @param <BT>
+ *
+ * @since 6.5
+ */
+ private static class IdentityBeanIdResolver<BT> implements
+ BeanIdResolver<BT, BT> {
+
+ @Override
+ public BT getIdForBean(BT bean) {
+ return bean;
+ }
+
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} for beans of the given type.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ public BeanItemContainer(Class<? super BEANTYPE> type)
+ throws IllegalArgumentException {
+ super(type);
+ super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>());
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} and adds the given beans to it.
+ * The collection must not be empty.
+ * {@link BeanItemContainer#BeanItemContainer(Class)} can be used for
+ * creating an initially empty {@code BeanItemContainer}.
+ *
+ * Note that when using this constructor, the actual class of the first item
+ * in the collection is used to determine the bean properties supported by
+ * the container instance, and only beans of that class or its subclasses
+ * can be added to the collection. If this is problematic or empty
+ * collections need to be supported, use {@link #BeanItemContainer(Class)}
+ * and {@link #addAll(Collection)} instead.
+ *
+ * @param collection
+ * a non empty {@link Collection} of beans.
+ * @throws IllegalArgumentException
+ * If the collection is null or empty.
+ *
+ * @deprecated use {@link #BeanItemContainer(Class, Collection)} instead
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ public BeanItemContainer(Collection<? extends BEANTYPE> collection)
+ throws IllegalArgumentException {
+ // must assume the class is BT
+ // the class information is erased by the compiler
+ this((Class<BEANTYPE>) getBeanClassForCollection(collection),
+ collection);
+ }
+
+ /**
+ * Internal helper method to support the deprecated {@link Collection}
+ * container.
+ *
+ * @param <BT>
+ * @param collection
+ * @return
+ * @throws IllegalArgumentException
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ private static <BT> Class<? extends BT> getBeanClassForCollection(
+ Collection<? extends BT> collection)
+ throws IllegalArgumentException {
+ if (collection == null || collection.isEmpty()) {
+ throw new IllegalArgumentException(
+ "The collection passed to BeanItemContainer constructor must not be null or empty. Use the other BeanItemContainer constructor.");
+ }
+ return (Class<? extends BT>) collection.iterator().next().getClass();
+ }
+
+ /**
+ * Constructs a {@code BeanItemContainer} and adds the given beans to it.
+ *
+ * @param type
+ * the type of the beans that will be added to the container.
+ * @param collection
+ * a {@link Collection} of beans (can be empty or null).
+ * @throws IllegalArgumentException
+ * If {@code type} is null
+ */
+ public BeanItemContainer(Class<? super BEANTYPE> type,
+ Collection<? extends BEANTYPE> collection)
+ throws IllegalArgumentException {
+ super(type);
+ super.setBeanIdResolver(new IdentityBeanIdResolver<BEANTYPE>());
+
+ if (collection != null) {
+ addAll(collection);
+ }
+ }
+
+ /**
+ * Adds all the beans from a {@link Collection} in one go. More efficient
+ * than adding them one by one.
+ *
+ * @param collection
+ * The collection of beans to add. Must not be null.
+ */
+ @Override
+ public void addAll(Collection<? extends BEANTYPE> collection) {
+ super.addAll(collection);
+ }
+
+ /**
+ * Adds the bean after the given bean.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param previousItemId
+ * the bean (of type BT) after which to add newItemId
+ * @param newItemId
+ * the bean (of type BT) to add (not null)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(Object, Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItemAfter(Object previousItemId,
+ Object newItemId) throws IllegalArgumentException {
+ return super.addBeanAfter((BEANTYPE) previousItemId,
+ (BEANTYPE) newItemId);
+ }
+
+ /**
+ * Adds a new bean at the given index.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @param index
+ * Index at which the bean should be added.
+ * @param newItemId
+ * The bean to add to the container.
+ * @return Returns the new BeanItem or null if the operation fails.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItemAt(int index, Object newItemId)
+ throws IllegalArgumentException {
+ return super.addBeanAt(index, (BEANTYPE) newItemId);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public BeanItem<BEANTYPE> addItem(Object itemId) {
+ return super.addBean((BEANTYPE) itemId);
+ }
+
+ /**
+ * Adds the bean to the Container.
+ *
+ * The bean is used both as the item contents and as the item identifier.
+ *
+ * @see com.vaadin.data.Container#addItem(Object)
+ */
+ @Override
+ public BeanItem<BEANTYPE> addBean(BEANTYPE bean) {
+ return addItem(bean);
+ }
+
+ /**
+ * Unsupported in BeanItemContainer.
+ */
+ @Override
+ protected void setBeanIdResolver(
+ AbstractBeanContainer.BeanIdResolver<BEANTYPE, BEANTYPE> beanIdResolver)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "BeanItemContainer always uses an IdentityBeanIdResolver");
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java
new file mode 100644
index 0000000000..717ce834cf
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java
@@ -0,0 +1,792 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A wrapper class for adding external hierarchy to containers not implementing
+ * the {@link com.vaadin.data.Container.Hierarchical} interface.
+ * </p>
+ *
+ * <p>
+ * If the wrapped container is changed directly (that is, not through the
+ * wrapper), and does not implement Container.ItemSetChangeNotifier and/or
+ * Container.PropertySetChangeNotifier the hierarchy information must be updated
+ * with the {@link #updateHierarchicalWrapper()} method.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ContainerHierarchicalWrapper implements Container.Hierarchical,
+ Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier {
+
+ /** The wrapped container */
+ private final Container container;
+
+ /** Set of IDs of those contained Items that can't have children. */
+ private HashSet<Object> noChildrenAllowed = null;
+
+ /** Mapping from Item ID to parent Item ID */
+ private Hashtable<Object, Object> parent = null;
+
+ /** Mapping from Item ID to a list of child IDs */
+ private Hashtable<Object, LinkedList<Object>> children = null;
+
+ /** List that contains all root elements of the container. */
+ private LinkedHashSet<Object> roots = null;
+
+ /** Is the wrapped container hierarchical by itself ? */
+ private boolean hierarchical;
+
+ /**
+ * A comparator that sorts the listed items before other items. Otherwise,
+ * the order is undefined.
+ */
+ private static class ListedItemsFirstComparator implements
+ Comparator<Object>, Serializable {
+ private final Collection<?> itemIds;
+
+ private ListedItemsFirstComparator(Collection<?> itemIds) {
+ this.itemIds = itemIds;
+ }
+
+ @Override
+ public int compare(Object o1, Object o2) {
+ if (o1.equals(o2)) {
+ return 0;
+ }
+ for (Object id : itemIds) {
+ if (id == o1) {
+ return -1;
+ } else if (id == o2) {
+ return 1;
+ }
+ }
+ return 0;
+ }
+ };
+
+ /**
+ * Constructs a new hierarchical wrapper for an existing Container. Works
+ * even if the to-be-wrapped container already implements the
+ * <code>Container.Hierarchical</code> interface.
+ *
+ * @param toBeWrapped
+ * the container that needs to be accessed hierarchically
+ * @see #updateHierarchicalWrapper()
+ */
+ public ContainerHierarchicalWrapper(Container toBeWrapped) {
+
+ container = toBeWrapped;
+ hierarchical = container instanceof Container.Hierarchical;
+
+ // Check arguments
+ if (container == null) {
+ throw new NullPointerException("Null can not be wrapped");
+ }
+
+ // Create initial order if needed
+ if (!hierarchical) {
+ noChildrenAllowed = new HashSet<Object>();
+ parent = new Hashtable<Object, Object>();
+ children = new Hashtable<Object, LinkedList<Object>>();
+ roots = new LinkedHashSet<Object>(container.getItemIds());
+ }
+
+ updateHierarchicalWrapper();
+
+ }
+
+ /**
+ * Updates the wrapper's internal hierarchy data to include all Items in the
+ * underlying container. If the contents of the wrapped container change
+ * without the wrapper's knowledge, this method needs to be called to update
+ * the hierarchy information of the Items.
+ */
+ public void updateHierarchicalWrapper() {
+
+ if (!hierarchical) {
+
+ // Recreate hierarchy and data structures if missing
+ if (noChildrenAllowed == null || parent == null || children == null
+ || roots == null) {
+ noChildrenAllowed = new HashSet<Object>();
+ parent = new Hashtable<Object, Object>();
+ children = new Hashtable<Object, LinkedList<Object>>();
+ roots = new LinkedHashSet<Object>(container.getItemIds());
+ }
+
+ // Check that the hierarchy is up-to-date
+ else {
+
+ // ensure order of root and child lists is same as in wrapped
+ // container
+ Collection<?> itemIds = container.getItemIds();
+ Comparator<Object> basedOnOrderFromWrappedContainer = new ListedItemsFirstComparator(
+ itemIds);
+
+ // Calculate the set of all items in the hierarchy
+ final HashSet<Object> s = new HashSet<Object>();
+ s.addAll(parent.keySet());
+ s.addAll(children.keySet());
+ s.addAll(roots);
+
+ // Remove unnecessary items
+ for (final Iterator<Object> i = s.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!container.containsId(id)) {
+ removeFromHierarchyWrapper(id);
+ }
+ }
+
+ // Add all the missing items
+ final Collection<?> ids = container.getItemIds();
+ for (final Iterator<?> i = ids.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!s.contains(id)) {
+ addToHierarchyWrapper(id);
+ s.add(id);
+ }
+ }
+
+ Object[] array = roots.toArray();
+ Arrays.sort(array, basedOnOrderFromWrappedContainer);
+ roots = new LinkedHashSet<Object>();
+ for (int i = 0; i < array.length; i++) {
+ roots.add(array[i]);
+ }
+ for (Object object : children.keySet()) {
+ LinkedList<Object> object2 = children.get(object);
+ Collections.sort(object2, basedOnOrderFromWrappedContainer);
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Removes the specified Item from the wrapper's internal hierarchy
+ * structure.
+ * <p>
+ * Note : The Item is not removed from the underlying Container.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to remove from the hierarchy.
+ */
+ private void removeFromHierarchyWrapper(Object itemId) {
+
+ LinkedList<Object> oprhanedChildren = children.remove(itemId);
+ if (oprhanedChildren != null) {
+ for (Object object : oprhanedChildren) {
+ // make orphaned children root nodes
+ setParent(object, null);
+ }
+ }
+
+ roots.remove(itemId);
+ final Object p = parent.get(itemId);
+ if (p != null) {
+ final LinkedList<Object> c = children.get(p);
+ if (c != null) {
+ c.remove(itemId);
+ }
+ }
+ parent.remove(itemId);
+ noChildrenAllowed.remove(itemId);
+ }
+
+ /**
+ * Adds the specified Item specified to the internal hierarchy structure.
+ * The new item is added as a root Item. The underlying container is not
+ * modified.
+ *
+ * @param itemId
+ * the ID of the item to add to the hierarchy.
+ */
+ private void addToHierarchyWrapper(Object itemId) {
+ roots.add(itemId);
+
+ }
+
+ /*
+ * Can the specified Item have any children? Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container)
+ .areChildrenAllowed(itemId);
+ }
+
+ if (noChildrenAllowed.contains(itemId)) {
+ return false;
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the children of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).getChildren(itemId);
+ }
+
+ final Collection<?> c = children.get(itemId);
+ if (c == null) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(c);
+ }
+
+ /*
+ * Gets the ID of the parent of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).getParent(itemId);
+ }
+
+ return parent.get(itemId);
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a leaf node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).hasChildren(itemId);
+ }
+
+ LinkedList<Object> list = children.get(itemId);
+ return (list != null && !list.isEmpty());
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a root node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).isRoot(itemId);
+ }
+
+ if (parent.containsKey(itemId)) {
+ return false;
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the root elements in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).rootItemIds();
+ }
+
+ return Collections.unmodifiableCollection(roots);
+ }
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item identified
+ * with the itemId already has children and the areChildrenAllowed is false
+ * this method fails and <code>false</code> is returned; the children must
+ * be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)} or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the Item in the container whose child capability is
+ * to be set.
+ * @param childrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).setChildrenAllowed(
+ itemId, childrenAllowed);
+ }
+
+ // Check that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Update status
+ if (childrenAllowed) {
+ noChildrenAllowed.remove(itemId);
+ } else {
+ noChildrenAllowed.add(itemId);
+ }
+
+ return true;
+ }
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be able to
+ * have children. (<code>canHaveChildren(newParentId) == true</code>). It is
+ * also possible to detach a node from the hierarchy (and thus make it root)
+ * by setting the parent <code>null</code>.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to be set as the child of the Item
+ * identified with newParentId.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+
+ // If the wrapped container implements the method directly, use it
+ if (hierarchical) {
+ return ((Container.Hierarchical) container).setParent(itemId,
+ newParentId);
+ }
+
+ // Check that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Get the old parent
+ final Object oldParentId = parent.get(itemId);
+
+ // Check if no change is necessary
+ if ((newParentId == null && oldParentId == null)
+ || (newParentId != null && newParentId.equals(oldParentId))) {
+ return true;
+ }
+
+ // Making root
+ if (newParentId == null) {
+
+ // Remove from old parents children list
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(itemId);
+ }
+ }
+
+ // Add to be a root
+ roots.add(itemId);
+
+ // Update parent
+ parent.remove(itemId);
+
+ return true;
+ }
+
+ // Check that the new parent exists in container and can have
+ // children
+ if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) {
+ return false;
+ }
+
+ // Check that setting parent doesn't result to a loop
+ Object o = newParentId;
+ while (o != null && !o.equals(itemId)) {
+ o = parent.get(o);
+ }
+ if (o != null) {
+ return false;
+ }
+
+ // Update parent
+ parent.put(itemId, newParentId);
+ LinkedList<Object> pcl = children.get(newParentId);
+ if (pcl == null) {
+ pcl = new LinkedList<Object>();
+ children.put(newParentId, pcl);
+ }
+ pcl.add(itemId);
+
+ // Remove from old parent or root
+ if (oldParentId == null) {
+ roots.remove(itemId);
+ } else {
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a new Item into the Container, assigns it an automatic ID, and
+ * adds it to the hierarchy.
+ *
+ * @return the autogenerated ID of the new Item or <code>null</code> if the
+ * operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object id = container.addItem();
+ if (!hierarchical && id != null) {
+ addToHierarchyWrapper(id);
+ }
+ return id;
+ }
+
+ /**
+ * Adds a new Item by its ID to the underlying container and to the
+ * hierarchy.
+ *
+ * @param itemId
+ * the ID of the Item to be created.
+ * @return the added Item or <code>null</code> if the operation failed.
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+
+ // Null ids are not accepted
+ if (itemId == null) {
+ throw new NullPointerException("Container item id can not be null");
+ }
+
+ final Item item = container.addItem(itemId);
+ if (!hierarchical && item != null) {
+ addToHierarchyWrapper(itemId);
+ }
+ return item;
+ }
+
+ /**
+ * Removes all items from the underlying container and from the hierarcy.
+ *
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeAllItems is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+
+ final boolean success = container.removeAllItems();
+
+ if (!hierarchical && success) {
+ roots.clear();
+ parent.clear();
+ children.clear();
+ noChildrenAllowed.clear();
+ }
+ return success;
+ }
+
+ /**
+ * Removes an Item specified by the itemId from the underlying container and
+ * from the hierarchy.
+ *
+ * @param itemId
+ * the ID of the Item to be removed.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeItem is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ final boolean success = container.removeItem(itemId);
+
+ if (!hierarchical && success) {
+ removeFromHierarchyWrapper(itemId);
+ }
+
+ return success;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children.
+ *
+ * @see #removeItem(Object)
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public boolean removeItemRecursively(Object itemId) {
+ return HierarchicalContainer.removeItemRecursively(this, itemId);
+ }
+
+ /**
+ * Adds a new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * the ID of the new Property.
+ * @param type
+ * the Data type of the new Property.
+ * @param defaultValue
+ * the value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the addContainerProperty is not supported.
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ return container.addContainerProperty(propertyId, type, defaultValue);
+ }
+
+ /**
+ * Removes the specified Property from the underlying container and from the
+ * hierarchy.
+ * <p>
+ * Note : The Property will be removed from all Items in the Container.
+ * </p>
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ return container.removeContainerProperty(propertyId);
+ }
+
+ /*
+ * Does the container contain the specified Item? Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ return container.containsId(itemId);
+ }
+
+ /*
+ * Gets the specified Item from the container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return container.getItem(itemId);
+ }
+
+ /*
+ * Gets the ID's of all Items stored in the Container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return container.getItemIds();
+ }
+
+ /*
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container Don't add a JavaDoc comment here, we use the default
+ * documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ return container.getContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * Gets the ID's of all Properties stored in the Container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return container.getContainerPropertyIds();
+ }
+
+ /*
+ * Gets the data type of all Properties identified by the given Property ID.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return container.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of Items in the Container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public int size() {
+ return container.size();
+ }
+
+ /*
+ * Registers a new Item set change listener for this Container. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Item set change listener from the object. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Registers a new Property set change listener for this Container. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Property set change listener from the object. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /**
+ * This listener 'piggybacks' on the real listener in order to update the
+ * wrapper when needed. It proxies equals() and hashCode() to the real
+ * listener so that the correct listener gets removed.
+ *
+ */
+ private class PiggybackListener implements
+ Container.PropertySetChangeListener,
+ Container.ItemSetChangeListener {
+
+ Object listener;
+
+ public PiggybackListener(Object realListener) {
+ listener = realListener;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ updateHierarchicalWrapper();
+ ((Container.ItemSetChangeListener) listener)
+ .containerItemSetChange(event);
+
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ updateHierarchicalWrapper();
+ ((Container.PropertySetChangeListener) listener)
+ .containerPropertySetChange(event);
+
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == listener || (obj != null && obj.equals(listener));
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java
new file mode 100644
index 0000000000..d3d6f88d3e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java
@@ -0,0 +1,644 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A wrapper class for adding external ordering to containers not implementing
+ * the {@link com.vaadin.data.Container.Ordered} interface.
+ * </p>
+ *
+ * <p>
+ * If the wrapped container is changed directly (that is, not through the
+ * wrapper), and does not implement Container.ItemSetChangeNotifier and/or
+ * Container.PropertySetChangeNotifier the hierarchy information must be updated
+ * with the {@link #updateOrderWrapper()} method.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ContainerOrderedWrapper implements Container.Ordered,
+ Container.ItemSetChangeNotifier, Container.PropertySetChangeNotifier {
+
+ /**
+ * The wrapped container
+ */
+ private final Container container;
+
+ /**
+ * Ordering information, ie. the mapping from Item ID to the next item ID
+ */
+ private Hashtable<Object, Object> next;
+
+ /**
+ * Reverse ordering information for convenience and performance reasons.
+ */
+ private Hashtable<Object, Object> prev;
+
+ /**
+ * ID of the first Item in the container.
+ */
+ private Object first;
+
+ /**
+ * ID of the last Item in the container.
+ */
+ private Object last;
+
+ /**
+ * Is the wrapped container ordered by itself, ie. does it implement the
+ * Container.Ordered interface by itself? If it does, this class will use
+ * the methods of the underlying container directly.
+ */
+ private boolean ordered = false;
+
+ /**
+ * The last known size of the wrapped container. Used to check whether items
+ * have been added or removed to the wrapped container, when the wrapped
+ * container does not send ItemSetChangeEvents.
+ */
+ private int lastKnownSize = -1;
+
+ /**
+ * Constructs a new ordered wrapper for an existing Container. Works even if
+ * the to-be-wrapped container already implements the Container.Ordered
+ * interface.
+ *
+ * @param toBeWrapped
+ * the container whose contents need to be ordered.
+ */
+ public ContainerOrderedWrapper(Container toBeWrapped) {
+
+ container = toBeWrapped;
+ ordered = container instanceof Container.Ordered;
+
+ // Checks arguments
+ if (container == null) {
+ throw new NullPointerException("Null can not be wrapped");
+ }
+
+ // Creates initial order if needed
+ updateOrderWrapper();
+ }
+
+ /**
+ * Removes the specified Item from the wrapper's internal hierarchy
+ * structure.
+ * <p>
+ * Note : The Item is not removed from the underlying Container.
+ * </p>
+ *
+ * @param id
+ * the ID of the Item to be removed from the ordering.
+ */
+ private void removeFromOrderWrapper(Object id) {
+ if (id != null) {
+ final Object pid = prev.get(id);
+ final Object nid = next.get(id);
+ if (first.equals(id)) {
+ first = nid;
+ }
+ if (last.equals(id)) {
+ first = pid;
+ }
+ if (nid != null) {
+ prev.put(nid, pid);
+ }
+ if (pid != null) {
+ next.put(pid, nid);
+ }
+ next.remove(id);
+ prev.remove(id);
+ }
+ }
+
+ /**
+ * Registers the specified Item to the last position in the wrapper's
+ * internal ordering. The underlying container is not modified.
+ *
+ * @param id
+ * the ID of the Item to be added to the ordering.
+ */
+ private void addToOrderWrapper(Object id) {
+
+ // Adds the if to tail
+ if (last != null) {
+ next.put(last, id);
+ prev.put(id, last);
+ last = id;
+ } else {
+ first = last = id;
+ }
+ }
+
+ /**
+ * Registers the specified Item after the specified itemId in the wrapper's
+ * internal ordering. The underlying container is not modified. Given item
+ * id must be in the container, or must be null.
+ *
+ * @param id
+ * the ID of the Item to be added to the ordering.
+ * @param previousItemId
+ * the Id of the previous item.
+ */
+ private void addToOrderWrapper(Object id, Object previousItemId) {
+
+ if (last == previousItemId || last == null) {
+ addToOrderWrapper(id);
+ } else {
+ if (previousItemId == null) {
+ next.put(id, first);
+ prev.put(first, id);
+ first = id;
+ } else {
+ prev.put(id, previousItemId);
+ next.put(id, next.get(previousItemId));
+ prev.put(next.get(previousItemId), id);
+ next.put(previousItemId, id);
+ }
+ }
+ }
+
+ /**
+ * Updates the wrapper's internal ordering information to include all Items
+ * in the underlying container.
+ * <p>
+ * Note : If the contents of the wrapped container change without the
+ * wrapper's knowledge, this method needs to be called to update the
+ * ordering information of the Items.
+ * </p>
+ */
+ public void updateOrderWrapper() {
+
+ if (!ordered) {
+
+ final Collection<?> ids = container.getItemIds();
+
+ // Recreates ordering if some parts of it are missing
+ if (next == null || first == null || last == null || prev != null) {
+ first = null;
+ last = null;
+ next = new Hashtable<Object, Object>();
+ prev = new Hashtable<Object, Object>();
+ }
+
+ // Filter out all the missing items
+ final LinkedList<?> l = new LinkedList<Object>(next.keySet());
+ for (final Iterator<?> i = l.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!container.containsId(id)) {
+ removeFromOrderWrapper(id);
+ }
+ }
+
+ // Adds missing items
+ for (final Iterator<?> i = ids.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ if (!next.containsKey(id)) {
+ addToOrderWrapper(id);
+ }
+ }
+ }
+ }
+
+ /*
+ * Gets the first item stored in the ordered container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object firstItemId() {
+ if (ordered) {
+ return ((Container.Ordered) container).firstItemId();
+ }
+ return first;
+ }
+
+ /*
+ * Tests if the given item is the first item in the container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isFirstId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).isFirstId(itemId);
+ }
+ return first != null && first.equals(itemId);
+ }
+
+ /*
+ * Tests if the given item is the last item in the container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).isLastId(itemId);
+ }
+ return last != null && last.equals(itemId);
+ }
+
+ /*
+ * Gets the last item stored in the ordered container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object lastItemId() {
+ if (ordered) {
+ return ((Container.Ordered) container).lastItemId();
+ }
+ return last;
+ }
+
+ /*
+ * Gets the item that is next from the specified item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object nextItemId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).nextItemId(itemId);
+ }
+ if (itemId == null) {
+ return null;
+ }
+ return next.get(itemId);
+ }
+
+ /*
+ * Gets the item that is previous from the specified item. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object prevItemId(Object itemId) {
+ if (ordered) {
+ return ((Container.Ordered) container).prevItemId(itemId);
+ }
+ if (itemId == null) {
+ return null;
+ }
+ return prev.get(itemId);
+ }
+
+ /**
+ * Registers a new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * the ID of the new Property.
+ * @param type
+ * the Data type of the new Property.
+ * @param defaultValue
+ * the value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ return container.addContainerProperty(propertyId, type, defaultValue);
+ }
+
+ /**
+ * Creates a new Item into the Container, assigns it an automatic ID, and
+ * adds it to the ordering.
+ *
+ * @return the autogenerated ID of the new Item or <code>null</code> if the
+ * operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object id = container.addItem();
+ if (!ordered && id != null) {
+ addToOrderWrapper(id);
+ }
+ return id;
+ }
+
+ /**
+ * Registers a new Item by its ID to the underlying container and to the
+ * ordering.
+ *
+ * @param itemId
+ * the ID of the Item to be created.
+ * @return the added Item or <code>null</code> if the operation failed
+ * @throws UnsupportedOperationException
+ * if the addItem is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ final Item item = container.addItem(itemId);
+ if (!ordered && item != null) {
+ addToOrderWrapper(itemId);
+ }
+ return item;
+ }
+
+ /**
+ * Removes all items from the underlying container and from the ordering.
+ *
+ * @return <code>true</code> if the operation succeeded, otherwise
+ * <code>false</code>
+ * @throws UnsupportedOperationException
+ * if the removeAllItems is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ final boolean success = container.removeAllItems();
+ if (!ordered && success) {
+ first = last = null;
+ next.clear();
+ prev.clear();
+ }
+ return success;
+ }
+
+ /**
+ * Removes an Item specified by the itemId from the underlying container and
+ * from the ordering.
+ *
+ * @param itemId
+ * the ID of the Item to be removed.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeItem is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ final boolean success = container.removeItem(itemId);
+ if (!ordered && success) {
+ removeFromOrderWrapper(itemId);
+ }
+ return success;
+ }
+
+ /**
+ * Removes the specified Property from the underlying container and from the
+ * ordering.
+ * <p>
+ * Note : The Property will be removed from all the Items in the Container.
+ * </p>
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ return container.removeContainerProperty(propertyId);
+ }
+
+ /*
+ * Does the container contain the specified Item? Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ return container.containsId(itemId);
+ }
+
+ /*
+ * Gets the specified Item from the container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return container.getItem(itemId);
+ }
+
+ /*
+ * Gets the ID's of all Items stored in the Container Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return container.getItemIds();
+ }
+
+ /*
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container Don't add a JavaDoc comment here, we use the default
+ * documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ return container.getContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * Gets the ID's of all Properties stored in the Container Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return container.getContainerPropertyIds();
+ }
+
+ /*
+ * Gets the data type of all Properties identified by the given Property ID.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return container.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of Items in the Container. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public int size() {
+ int newSize = container.size();
+ if (lastKnownSize != -1 && newSize != lastKnownSize
+ && !(container instanceof Container.ItemSetChangeNotifier)) {
+ // Update the internal cache when the size of the container changes
+ // and the container is incapable of sending ItemSetChangeEvents
+ updateOrderWrapper();
+ }
+ lastKnownSize = newSize;
+ return newSize;
+ }
+
+ /*
+ * Registers a new Item set change listener for this Container. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Item set change listener from the object. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (container instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Registers a new Property set change listener for this Container. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .addListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * Removes a Property set change listener from the object. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ if (container instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) container)
+ .removeListener(new PiggybackListener(listener));
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+
+ // If the previous item is not in the container, fail
+ if (previousItemId != null && !containsId(previousItemId)) {
+ return null;
+ }
+
+ // Adds the item to container
+ final Item item = container.addItem(newItemId);
+
+ // Puts the new item to its correct place
+ if (!ordered && item != null) {
+ addToOrderWrapper(newItemId, previousItemId);
+ }
+
+ return item;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+
+ // If the previous item is not in the container, fail
+ if (previousItemId != null && !containsId(previousItemId)) {
+ return null;
+ }
+
+ // Adds the item to container
+ final Object id = container.addItem();
+
+ // Puts the new item to its correct place
+ if (!ordered && id != null) {
+ addToOrderWrapper(id, previousItemId);
+ }
+
+ return id;
+ }
+
+ /**
+ * This listener 'piggybacks' on the real listener in order to update the
+ * wrapper when needed. It proxies equals() and hashCode() to the real
+ * listener so that the correct listener gets removed.
+ *
+ */
+ private class PiggybackListener implements
+ Container.PropertySetChangeListener,
+ Container.ItemSetChangeListener {
+
+ Object listener;
+
+ public PiggybackListener(Object realListener) {
+ listener = realListener;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ updateOrderWrapper();
+ ((Container.ItemSetChangeListener) listener)
+ .containerItemSetChange(event);
+
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ updateOrderWrapper();
+ ((Container.PropertySetChangeListener) listener)
+ .containerPropertySetChange(event);
+
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == listener || (obj != null && obj.equals(listener));
+ }
+
+ @Override
+ public int hashCode() {
+ return listener.hashCode();
+ }
+
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/DefaultItemSorter.java b/server/src/com/vaadin/data/util/DefaultItemSorter.java
new file mode 100644
index 0000000000..81b15ebd4f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/DefaultItemSorter.java
@@ -0,0 +1,210 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Provides a default implementation of an ItemSorter. The
+ * <code>DefaultItemSorter</code> adheres to the
+ * {@link Sortable#sort(Object[], boolean[])} rules and sorts the container
+ * according to the properties given using
+ * {@link #setSortProperties(Sortable, Object[], boolean[])}.
+ * <p>
+ * A Comparator is used for comparing the individual <code>Property</code>
+ * values. The comparator can be set using the constructor. If no comparator is
+ * provided a default comparator is used.
+ *
+ */
+public class DefaultItemSorter implements ItemSorter {
+
+ private java.lang.Object[] sortPropertyIds;
+ private boolean[] sortDirections;
+ private Container container;
+ private Comparator<Object> propertyValueComparator;
+
+ /**
+ * Constructs a DefaultItemSorter using the default <code>Comparator</code>
+ * for comparing <code>Property</code>values.
+ *
+ */
+ public DefaultItemSorter() {
+ this(new DefaultPropertyValueComparator());
+ }
+
+ /**
+ * Constructs a DefaultItemSorter which uses the <code>Comparator</code>
+ * indicated by the <code>propertyValueComparator</code> parameter for
+ * comparing <code>Property</code>values.
+ *
+ * @param propertyValueComparator
+ * The comparator to use when comparing individual
+ * <code>Property</code> values
+ */
+ public DefaultItemSorter(Comparator<Object> propertyValueComparator) {
+ this.propertyValueComparator = propertyValueComparator;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.ItemSorter#compare(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public int compare(Object o1, Object o2) {
+ Item item1 = container.getItem(o1);
+ Item item2 = container.getItem(o2);
+
+ /*
+ * Items can be null if the container is filtered. Null is considered
+ * "less" than not-null.
+ */
+ if (item1 == null) {
+ if (item2 == null) {
+ return 0;
+ } else {
+ return 1;
+ }
+ } else if (item2 == null) {
+ return -1;
+ }
+
+ for (int i = 0; i < sortPropertyIds.length; i++) {
+
+ int result = compareProperty(sortPropertyIds[i], sortDirections[i],
+ item1, item2);
+
+ // If order can be decided
+ if (result != 0) {
+ return result;
+ }
+
+ }
+
+ return 0;
+ }
+
+ /**
+ * Compares the property indicated by <code>propertyId</code> in the items
+ * indicated by <code>item1</code> and <code>item2</code> for order. Returns
+ * a negative integer, zero, or a positive integer as the property value in
+ * the first item is less than, equal to, or greater than the property value
+ * in the second item. If the <code>sortDirection</code> is false the
+ * returned value is negated.
+ * <p>
+ * The comparator set for this <code>DefaultItemSorter</code> is used for
+ * comparing the two property values.
+ *
+ * @param propertyId
+ * The property id for the property that is used for comparison.
+ * @param sortDirection
+ * The direction of the sort. A false value negates the result.
+ * @param item1
+ * The first item to compare.
+ * @param item2
+ * The second item to compare.
+ * @return a negative, zero, or positive integer if the property value in
+ * the first item is less than, equal to, or greater than the
+ * property value in the second item. Negated if
+ * {@code sortDirection} is false.
+ */
+ protected int compareProperty(Object propertyId, boolean sortDirection,
+ Item item1, Item item2) {
+
+ // Get the properties to compare
+ final Property<?> property1 = item1.getItemProperty(propertyId);
+ final Property<?> property2 = item2.getItemProperty(propertyId);
+
+ // Get the values to compare
+ final Object value1 = (property1 == null) ? null : property1.getValue();
+ final Object value2 = (property2 == null) ? null : property2.getValue();
+
+ // Result of the comparison
+ int r = 0;
+ if (sortDirection) {
+ r = propertyValueComparator.compare(value1, value2);
+ } else {
+ r = propertyValueComparator.compare(value2, value1);
+ }
+
+ return r;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.ItemSorter#setSortProperties(com.vaadin.data.Container
+ * .Sortable, java.lang.Object[], boolean[])
+ */
+ @Override
+ public void setSortProperties(Container.Sortable container,
+ Object[] propertyId, boolean[] ascending) {
+ this.container = container;
+
+ // Removes any non-sortable property ids
+ final List<Object> ids = new ArrayList<Object>();
+ final List<Boolean> orders = new ArrayList<Boolean>();
+ final Collection<?> sortable = container
+ .getSortableContainerPropertyIds();
+ for (int i = 0; i < propertyId.length; i++) {
+ if (sortable.contains(propertyId[i])) {
+ ids.add(propertyId[i]);
+ orders.add(Boolean.valueOf(i < ascending.length ? ascending[i]
+ : true));
+ }
+ }
+
+ sortPropertyIds = ids.toArray();
+ sortDirections = new boolean[orders.size()];
+ for (int i = 0; i < sortDirections.length; i++) {
+ sortDirections[i] = (orders.get(i)).booleanValue();
+ }
+
+ }
+
+ /**
+ * Provides a default comparator used for comparing {@link Property} values.
+ * The <code>DefaultPropertyValueComparator</code> assumes all objects it
+ * compares can be cast to Comparable.
+ *
+ */
+ public static class DefaultPropertyValueComparator implements
+ Comparator<Object>, Serializable {
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public int compare(Object o1, Object o2) {
+ int r = 0;
+ // Normal non-null comparison
+ if (o1 != null && o2 != null) {
+ // Assume the objects can be cast to Comparable, throw
+ // ClassCastException otherwise.
+ r = ((Comparable<Object>) o1).compareTo(o2);
+ } else if (o1 == o2) {
+ // Objects are equal if both are null
+ r = 0;
+ } else {
+ if (o1 == null) {
+ r = -1; // null is less than non-null
+ } else {
+ r = 1; // non-null is greater than null
+ }
+ }
+
+ return r;
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/FilesystemContainer.java b/server/src/com/vaadin/data/util/FilesystemContainer.java
new file mode 100644
index 0000000000..cdfeb57e14
--- /dev/null
+++ b/server/src/com/vaadin/data/util/FilesystemContainer.java
@@ -0,0 +1,918 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.service.FileTypeResolver;
+import com.vaadin.terminal.Resource;
+
+/**
+ * A hierarchical container wrapper for a filesystem.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class FilesystemContainer implements Container.Hierarchical {
+
+ /**
+ * String identifier of a file's "name" property.
+ */
+ public static String PROPERTY_NAME = "Name";
+
+ /**
+ * String identifier of a file's "size" property.
+ */
+ public static String PROPERTY_SIZE = "Size";
+
+ /**
+ * String identifier of a file's "icon" property.
+ */
+ public static String PROPERTY_ICON = "Icon";
+
+ /**
+ * String identifier of a file's "last modified" property.
+ */
+ public static String PROPERTY_LASTMODIFIED = "Last Modified";
+
+ /**
+ * List of the string identifiers for the available properties.
+ */
+ public static Collection<String> FILE_PROPERTIES;
+
+ private final static Method FILEITEM_LASTMODIFIED;
+
+ private final static Method FILEITEM_NAME;
+
+ private final static Method FILEITEM_ICON;
+
+ private final static Method FILEITEM_SIZE;
+
+ static {
+
+ FILE_PROPERTIES = new ArrayList<String>();
+ FILE_PROPERTIES.add(PROPERTY_NAME);
+ FILE_PROPERTIES.add(PROPERTY_ICON);
+ FILE_PROPERTIES.add(PROPERTY_SIZE);
+ FILE_PROPERTIES.add(PROPERTY_LASTMODIFIED);
+ FILE_PROPERTIES = Collections.unmodifiableCollection(FILE_PROPERTIES);
+ try {
+ FILEITEM_LASTMODIFIED = FileItem.class.getMethod("lastModified",
+ new Class[] {});
+ FILEITEM_NAME = FileItem.class.getMethod("getName", new Class[] {});
+ FILEITEM_ICON = FileItem.class.getMethod("getIcon", new Class[] {});
+ FILEITEM_SIZE = FileItem.class.getMethod("getSize", new Class[] {});
+ } catch (final NoSuchMethodException e) {
+ throw new RuntimeException(
+ "Internal error finding methods in FilesystemContainer");
+ }
+ }
+
+ private File[] roots = new File[] {};
+
+ private FilenameFilter filter = null;
+
+ private boolean recursive = true;
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem. The files are included recursively.
+ *
+ * @param root
+ * the root file for the new file-system container. Null values
+ * are ignored.
+ */
+ public FilesystemContainer(File root) {
+ if (root != null) {
+ roots = new File[] { root };
+ }
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem. The files are included recursively.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, boolean recursive) {
+ this(root);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified file
+ * as the root of the filesystem.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param extension
+ * the Filename extension (w/o separator) to limit the files in
+ * container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, String extension, boolean recursive) {
+ this(root);
+ this.setFilter(extension);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Constructs a new <code>FileSystemContainer</code> with the specified root
+ * and recursivity status.
+ *
+ * @param root
+ * the root file for the new file-system container.
+ * @param filter
+ * the Filename filter to limit the files in container.
+ * @param recursive
+ * should the container recursively contain subdirectories.
+ */
+ public FilesystemContainer(File root, FilenameFilter filter,
+ boolean recursive) {
+ this(root);
+ this.setFilter(filter);
+ setRecursive(recursive);
+ }
+
+ /**
+ * Adds new root file directory. Adds a file to be included as root file
+ * directory in the <code>FilesystemContainer</code>.
+ *
+ * @param root
+ * the File to be added as root directory. Null values are
+ * ignored.
+ */
+ public void addRoot(File root) {
+ if (root != null) {
+ final File[] newRoots = new File[roots.length + 1];
+ for (int i = 0; i < roots.length; i++) {
+ newRoots[i] = roots[i];
+ }
+ newRoots[roots.length] = root;
+ roots = newRoots;
+ }
+ }
+
+ /**
+ * Tests if the specified Item in the container may have children. Since a
+ * <code>FileSystemContainer</code> contains files and directories, this
+ * method returns <code>true</code> for directory Items only.
+ *
+ * @param itemId
+ * the id of the item.
+ * @return <code>true</code> if the specified Item is a directory,
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return itemId instanceof File && ((File) itemId).canRead()
+ && ((File) itemId).isDirectory();
+ }
+
+ /*
+ * Gets the ID's of all Items who are children of the specified Item. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Collection<File> getChildren(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+ File[] f;
+ if (filter != null) {
+ f = ((File) itemId).listFiles(filter);
+ } else {
+ f = ((File) itemId).listFiles();
+ }
+
+ if (f == null) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+
+ return Collections.unmodifiableCollection(l);
+ }
+
+ /*
+ * Gets the parent item of the specified Item. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+ return ((File) itemId).getParentFile();
+ }
+
+ /*
+ * Tests if the specified Item has any children. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ String[] l;
+ if (filter != null) {
+ l = ((File) itemId).list(filter);
+ } else {
+ l = ((File) itemId).list();
+ }
+ return (l != null) && (l.length > 0);
+ }
+
+ /*
+ * Tests if the specified Item is the root of the filesystem. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ for (int i = 0; i < roots.length; i++) {
+ if (roots[i].equals(itemId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /*
+ * Gets the ID's of all root Items in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<File> rootItemIds() {
+
+ File[] f;
+
+ // in single root case we use children
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return Collections.unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+
+ return Collections.unmodifiableCollection(l);
+ }
+
+ /**
+ * Returns <code>false</code> when conversion from files to directories is
+ * not supported.
+ *
+ * @param itemId
+ * the ID of the item.
+ * @param areChildrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operaton is successful otherwise
+ * <code>false</code>.
+ * @throws UnsupportedOperationException
+ * if the setChildrenAllowed is not supported.
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException(
+ "Conversion file to/from directory is not supported");
+ }
+
+ /**
+ * Returns <code>false</code> when moving files around in the filesystem is
+ * not supported.
+ *
+ * @param itemId
+ * the ID of the item.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation is successful otherwise
+ * <code>false</code>.
+ * @throws UnsupportedOperationException
+ * if the setParent is not supported.
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException("File moving is not supported");
+ }
+
+ /*
+ * Tests if the filesystem contains the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return false;
+ }
+ boolean val = false;
+
+ // Try to match all roots
+ for (int i = 0; i < roots.length; i++) {
+ try {
+ val |= ((File) itemId).getCanonicalPath().startsWith(
+ roots[i].getCanonicalPath());
+ } catch (final IOException e) {
+ // Exception ignored
+ }
+
+ }
+ if (val && filter != null) {
+ val &= filter.accept(((File) itemId).getParentFile(),
+ ((File) itemId).getName());
+ }
+ return val;
+ }
+
+ /*
+ * Gets the specified Item from the filesystem. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+ return new FileItem((File) itemId);
+ }
+
+ /**
+ * Internal recursive method to add the files under the specified directory
+ * to the collection.
+ *
+ * @param col
+ * the collection where the found items are added
+ * @param f
+ * the root file where to start adding files
+ */
+ private void addItemIds(Collection<File> col, File f) {
+ File[] l;
+ if (filter != null) {
+ l = f.listFiles(filter);
+ } else {
+ l = f.listFiles();
+ }
+ if (l == null) {
+ // File.listFiles returns null if File does not exist or if there
+ // was an IO error (permission denied)
+ return;
+ }
+ final List<File> ll = Arrays.asList(l);
+ Collections.sort(ll);
+
+ for (final Iterator<File> i = ll.iterator(); i.hasNext();) {
+ final File lf = i.next();
+ col.add(lf);
+ if (lf.isDirectory()) {
+ addItemIds(col, lf);
+ }
+ }
+ }
+
+ /*
+ * Gets the IDs of Items in the filesystem. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Collection<File> getItemIds() {
+
+ if (recursive) {
+ final Collection<File> col = new ArrayList<File>();
+ for (int i = 0; i < roots.length; i++) {
+ addItemIds(col, roots[i]);
+ }
+ return Collections.unmodifiableCollection(col);
+ } else {
+ File[] f;
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return Collections
+ .unmodifiableCollection(new LinkedList<File>());
+ }
+
+ final List<File> l = Arrays.asList(f);
+ Collections.sort(l);
+ return Collections.unmodifiableCollection(l);
+ }
+
+ }
+
+ /**
+ * Gets the specified property of the specified file Item. The available
+ * file properties are "Name", "Size" and "Last Modified". If propertyId is
+ * not one of those, <code>null</code> is returned.
+ *
+ * @param itemId
+ * the ID of the file whose property is requested.
+ * @param propertyId
+ * the property's ID.
+ * @return the requested property's value, or <code>null</code>
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+
+ if (!(itemId instanceof File)) {
+ return null;
+ }
+
+ if (propertyId.equals(PROPERTY_NAME)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_NAME, null);
+ }
+
+ if (propertyId.equals(PROPERTY_ICON)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_ICON, null);
+ }
+
+ if (propertyId.equals(PROPERTY_SIZE)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_SIZE, null);
+ }
+
+ if (propertyId.equals(PROPERTY_LASTMODIFIED)) {
+ return new MethodProperty<Object>(getType(propertyId),
+ new FileItem((File) itemId), FILEITEM_LASTMODIFIED, null);
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the collection of available file properties.
+ *
+ * @return Unmodifiable collection containing all available file properties.
+ */
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return FILE_PROPERTIES;
+ }
+
+ /**
+ * Gets the specified property's data type. "Name" is a <code>String</code>,
+ * "Size" is a <code>Long</code>, "Last Modified" is a <code>Date</code>. If
+ * propertyId is not one of those, <code>null</code> is returned.
+ *
+ * @param propertyId
+ * the ID of the property whose type is requested.
+ * @return data type of the requested property, or <code>null</code>
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+
+ if (propertyId.equals(PROPERTY_NAME)) {
+ return String.class;
+ }
+ if (propertyId.equals(PROPERTY_ICON)) {
+ return Resource.class;
+ }
+ if (propertyId.equals(PROPERTY_SIZE)) {
+ return Long.class;
+ }
+ if (propertyId.equals(PROPERTY_LASTMODIFIED)) {
+ return Date.class;
+ }
+ return null;
+ }
+
+ /**
+ * Internal method to recursively calculate the number of files under a root
+ * directory.
+ *
+ * @param f
+ * the root to start counting from.
+ */
+ private int getFileCounts(File f) {
+ File[] l;
+ if (filter != null) {
+ l = f.listFiles(filter);
+ } else {
+ l = f.listFiles();
+ }
+
+ if (l == null) {
+ return 0;
+ }
+ int ret = l.length;
+ for (int i = 0; i < l.length; i++) {
+ if (l[i].isDirectory()) {
+ ret += getFileCounts(l[i]);
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Gets the number of Items in the container. In effect, this is the
+ * combined amount of files and directories.
+ *
+ * @return Number of Items in the container.
+ */
+ @Override
+ public int size() {
+
+ if (recursive) {
+ int counts = 0;
+ for (int i = 0; i < roots.length; i++) {
+ counts += getFileCounts(roots[i]);
+ }
+ return counts;
+ } else {
+ File[] f;
+ if (roots.length == 1) {
+ if (filter != null) {
+ f = roots[0].listFiles(filter);
+ } else {
+ f = roots[0].listFiles();
+ }
+ } else {
+ f = roots;
+ }
+
+ if (f == null) {
+ return 0;
+ }
+ return f.length;
+ }
+ }
+
+ /**
+ * A Item wrapper for files in a filesystem.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class FileItem implements Item {
+
+ /**
+ * The wrapped file.
+ */
+ private final File file;
+
+ /**
+ * Constructs a FileItem from a existing file.
+ */
+ private FileItem(File file) {
+ this.file = file;
+ }
+
+ /*
+ * Gets the specified property of this file. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return getContainerProperty(file, id);
+ }
+
+ /*
+ * Gets the IDs of all properties available for this item Don't add a
+ * JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Collection<String> getItemPropertyIds() {
+ return getContainerPropertyIds();
+ }
+
+ /**
+ * Calculates a integer hash-code for the Property that's unique inside
+ * the Item containing the Property. Two different Properties inside the
+ * same Item contained in the same list always have different
+ * hash-codes, though Properties in different Items may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return file.hashCode() ^ FilesystemContainer.this.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two
+ * Properties got from an Item with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object.
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof FileItem)) {
+ return false;
+ }
+ final FileItem fi = (FileItem) obj;
+ return fi.getHost() == getHost() && fi.file.equals(file);
+ }
+
+ /**
+ * Gets the host of this file.
+ */
+ private FilesystemContainer getHost() {
+ return FilesystemContainer.this;
+ }
+
+ /**
+ * Gets the last modified date of this file.
+ *
+ * @return Date
+ */
+ public Date lastModified() {
+ return new Date(file.lastModified());
+ }
+
+ /**
+ * Gets the name of this file.
+ *
+ * @return file name of this file.
+ */
+ public String getName() {
+ return file.getName();
+ }
+
+ /**
+ * Gets the icon of this file.
+ *
+ * @return the icon of this file.
+ */
+ public Resource getIcon() {
+ return FileTypeResolver.getIcon(file);
+ }
+
+ /**
+ * Gets the size of this file.
+ *
+ * @return size
+ */
+ public long getSize() {
+ if (file.isDirectory()) {
+ return 0;
+ }
+ return file.length();
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ if ("".equals(file.getName())) {
+ return file.getAbsolutePath();
+ }
+ return file.getName();
+ }
+
+ /**
+ * Filesystem container does not support adding new properties.
+ *
+ * @see com.vaadin.data.Item#addItemProperty(Object, Property)
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Filesystem container "
+ + "does not support adding new properties");
+ }
+
+ /**
+ * Filesystem container does not support removing properties.
+ *
+ * @see com.vaadin.data.Item#removeItemProperty(Object)
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Filesystem container does not support property removal");
+ }
+
+ }
+
+ /**
+ * Generic file extension filter for displaying only files having certain
+ * extension.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class FileExtensionFilter implements FilenameFilter, Serializable {
+
+ private final String filter;
+
+ /**
+ * Constructs a new FileExtensionFilter using given extension.
+ *
+ * @param fileExtension
+ * the File extension without the separator (dot).
+ */
+ public FileExtensionFilter(String fileExtension) {
+ filter = "." + fileExtension;
+ }
+
+ /**
+ * Allows only files with the extension and directories.
+ *
+ * @see java.io.FilenameFilter#accept(File, String)
+ */
+ @Override
+ public boolean accept(File dir, String name) {
+ if (name.endsWith(filter)) {
+ return true;
+ }
+ return new File(dir, name).isDirectory();
+ }
+
+ }
+
+ /**
+ * Returns the file filter used to limit the files in this container.
+ *
+ * @return Used filter instance or null if no filter is assigned.
+ */
+ public FilenameFilter getFilter() {
+ return filter;
+ }
+
+ /**
+ * Sets the file filter used to limit the files in this container.
+ *
+ * @param filter
+ * The filter to set. <code>null</code> disables filtering.
+ */
+ public void setFilter(FilenameFilter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Sets the file filter used to limit the files in this container.
+ *
+ * @param extension
+ * the Filename extension (w/o separator) to limit the files in
+ * container.
+ */
+ public void setFilter(String extension) {
+ filter = new FileExtensionFilter(extension);
+ }
+
+ /**
+ * Is this container recursive filesystem.
+ *
+ * @return <code>true</code> if container is recursive, <code>false</code>
+ * otherwise.
+ */
+ public boolean isRecursive() {
+ return recursive;
+ }
+
+ /**
+ * Sets the container recursive property. Set this to false to limit the
+ * files directly under the root file.
+ * <p>
+ * Note : This is meaningful only if the root really is a directory.
+ * </p>
+ *
+ * @param recursive
+ * the New value for recursive property.
+ */
+ public void setRecursive(boolean recursive) {
+ this.recursive = recursive;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object )
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "File system container does not support this operation");
+ }
+}
diff --git a/server/src/com/vaadin/data/util/HierarchicalContainer.java b/server/src/com/vaadin/data/util/HierarchicalContainer.java
new file mode 100644
index 0000000000..06ab77c0e7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/HierarchicalContainer.java
@@ -0,0 +1,814 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+
+/**
+ * A specialized Container whose contents can be accessed like it was a
+ * tree-like structure.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class HierarchicalContainer extends IndexedContainer implements
+ Container.Hierarchical {
+
+ /**
+ * Set of IDs of those contained Items that can't have children.
+ */
+ private final HashSet<Object> noChildrenAllowed = new HashSet<Object>();
+
+ /**
+ * Mapping from Item ID to parent Item ID.
+ */
+ private final HashMap<Object, Object> parent = new HashMap<Object, Object>();
+
+ /**
+ * Mapping from Item ID to parent Item ID for items included in the filtered
+ * container.
+ */
+ private HashMap<Object, Object> filteredParent = null;
+
+ /**
+ * Mapping from Item ID to a list of child IDs.
+ */
+ private final HashMap<Object, LinkedList<Object>> children = new HashMap<Object, LinkedList<Object>>();
+
+ /**
+ * Mapping from Item ID to a list of child IDs when filtered
+ */
+ private HashMap<Object, LinkedList<Object>> filteredChildren = null;
+
+ /**
+ * List that contains all root elements of the container.
+ */
+ private final LinkedList<Object> roots = new LinkedList<Object>();
+
+ /**
+ * List that contains all filtered root elements of the container.
+ */
+ private LinkedList<Object> filteredRoots = null;
+
+ /**
+ * Determines how filtering of the container is done.
+ */
+ private boolean includeParentsWhenFiltering = true;
+
+ private boolean contentChangedEventsDisabled = false;
+
+ private boolean contentsChangedEventPending;
+
+ /*
+ * Can the specified Item have any children? Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ if (noChildrenAllowed.contains(itemId)) {
+ return false;
+ }
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the children of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ LinkedList<Object> c;
+
+ if (filteredChildren != null) {
+ c = filteredChildren.get(itemId);
+ } else {
+ c = children.get(itemId);
+ }
+
+ if (c == null) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(c);
+ }
+
+ /*
+ * Gets the ID of the parent of the specified Item. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Object getParent(Object itemId) {
+ if (filteredParent != null) {
+ return filteredParent.get(itemId);
+ }
+ return parent.get(itemId);
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a leaf node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+ if (filteredChildren != null) {
+ return filteredChildren.containsKey(itemId);
+ } else {
+ return children.containsKey(itemId);
+ }
+ }
+
+ /*
+ * Is the Item corresponding to the given ID a root node? Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+ // If the container is filtered the itemId must be among filteredRoots
+ // to be a root.
+ if (filteredRoots != null) {
+ if (!filteredRoots.contains(itemId)) {
+ return false;
+ }
+ } else {
+ // Container is not filtered
+ if (parent.containsKey(itemId)) {
+ return false;
+ }
+ }
+
+ return containsId(itemId);
+ }
+
+ /*
+ * Gets the IDs of the root elements in the container. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+ if (filteredRoots != null) {
+ return Collections.unmodifiableCollection(filteredRoots);
+ } else {
+ return Collections.unmodifiableCollection(roots);
+ }
+ }
+
+ /**
+ * <p>
+ * Sets the given Item's capability to have children. If the Item identified
+ * with the itemId already has children and the areChildrenAllowed is false
+ * this method fails and <code>false</code> is returned; the children must
+ * be first explicitly removed with
+ * {@link #setParent(Object itemId, Object newParentId)} or
+ * {@link com.vaadin.data.Container#removeItem(Object itemId)}.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the Item in the container whose child capability is
+ * to be set.
+ * @param childrenAllowed
+ * the boolean value specifying if the Item can have children or
+ * not.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean childrenAllowed) {
+
+ // Checks that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Updates status
+ if (childrenAllowed) {
+ noChildrenAllowed.remove(itemId);
+ } else {
+ noChildrenAllowed.add(itemId);
+ }
+
+ return true;
+ }
+
+ /**
+ * <p>
+ * Sets the parent of an Item. The new parent item must exist and be able to
+ * have children. (<code>canHaveChildren(newParentId) == true</code>). It is
+ * also possible to detach a node from the hierarchy (and thus make it root)
+ * by setting the parent <code>null</code>.
+ * </p>
+ *
+ * @param itemId
+ * the ID of the item to be set as the child of the Item
+ * identified with newParentId.
+ * @param newParentId
+ * the ID of the Item that's to be the new parent of the Item
+ * identified with itemId.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+
+ // Checks that the item is in the container
+ if (!containsId(itemId)) {
+ return false;
+ }
+
+ // Gets the old parent
+ final Object oldParentId = parent.get(itemId);
+
+ // Checks if no change is necessary
+ if ((newParentId == null && oldParentId == null)
+ || ((newParentId != null) && newParentId.equals(oldParentId))) {
+ return true;
+ }
+
+ // Making root?
+ if (newParentId == null) {
+ // The itemId should become a root so we need to
+ // - Remove it from the old parent's children list
+ // - Add it as a root
+ // - Remove it from the item -> parent list (parent is null for
+ // roots)
+
+ // Removes from old parents children list
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+
+ }
+
+ // Add to be a root
+ roots.add(itemId);
+
+ // Updates parent
+ parent.remove(itemId);
+
+ if (hasFilters()) {
+ // Refilter the container if setParent is called when filters
+ // are applied. Changing parent can change what is included in
+ // the filtered version (if includeParentsWhenFiltering==true).
+ doFilterContainer(hasFilters());
+ }
+
+ fireItemSetChange();
+
+ return true;
+ }
+
+ // We get here when the item should not become a root and we need to
+ // - Verify the new parent exists and can have children
+ // - Check that the new parent is not a child of the selected itemId
+ // - Updated the item -> parent mapping to point to the new parent
+ // - Remove the item from the roots list if it was a root
+ // - Remove the item from the old parent's children list if it was not a
+ // root
+
+ // Checks that the new parent exists in container and can have
+ // children
+ if (!containsId(newParentId) || noChildrenAllowed.contains(newParentId)) {
+ return false;
+ }
+
+ // Checks that setting parent doesn't result to a loop
+ Object o = newParentId;
+ while (o != null && !o.equals(itemId)) {
+ o = parent.get(o);
+ }
+ if (o != null) {
+ return false;
+ }
+
+ // Updates parent
+ parent.put(itemId, newParentId);
+ LinkedList<Object> pcl = children.get(newParentId);
+ if (pcl == null) {
+ // Create an empty list for holding children if one were not
+ // previously created
+ pcl = new LinkedList<Object>();
+ children.put(newParentId, pcl);
+ }
+ pcl.add(itemId);
+
+ // Removes from old parent or root
+ if (oldParentId == null) {
+ roots.remove(itemId);
+ } else {
+ final LinkedList<Object> l = children.get(oldParentId);
+ if (l != null) {
+ l.remove(itemId);
+ if (l.isEmpty()) {
+ children.remove(oldParentId);
+ }
+ }
+ }
+
+ if (hasFilters()) {
+ // Refilter the container if setParent is called when filters
+ // are applied. Changing parent can change what is included in
+ // the filtered version (if includeParentsWhenFiltering==true).
+ doFilterContainer(hasFilters());
+ }
+
+ fireItemSetChange();
+
+ return true;
+ }
+
+ private boolean hasFilters() {
+ return (filteredRoots != null);
+ }
+
+ /**
+ * Moves a node (an Item) in the container immediately after a sibling node.
+ * The two nodes must have the same parent in the container.
+ *
+ * @param itemId
+ * the identifier of the moved node (Item)
+ * @param siblingId
+ * the identifier of the reference node (Item), after which the
+ * other node will be located
+ */
+ public void moveAfterSibling(Object itemId, Object siblingId) {
+ Object parent2 = getParent(itemId);
+ LinkedList<Object> childrenList;
+ if (parent2 == null) {
+ childrenList = roots;
+ } else {
+ childrenList = children.get(parent2);
+ }
+ if (siblingId == null) {
+ childrenList.remove(itemId);
+ childrenList.addFirst(itemId);
+
+ } else {
+ int oldIndex = childrenList.indexOf(itemId);
+ int indexOfSibling = childrenList.indexOf(siblingId);
+ if (indexOfSibling != -1 && oldIndex != -1) {
+ int newIndex;
+ if (oldIndex > indexOfSibling) {
+ newIndex = indexOfSibling + 1;
+ } else {
+ newIndex = indexOfSibling;
+ }
+ childrenList.remove(oldIndex);
+ childrenList.add(newIndex, itemId);
+ } else {
+ throw new IllegalArgumentException(
+ "Given identifiers no not have the same parent.");
+ }
+ }
+ fireItemSetChange();
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#addItem()
+ */
+ @Override
+ public Object addItem() {
+ disableContentsChangeEvents();
+ final Object itemId = super.addItem();
+ if (itemId == null) {
+ return null;
+ }
+
+ if (!roots.contains(itemId)) {
+ roots.add(itemId);
+ if (filteredRoots != null) {
+ if (passesFilters(itemId)) {
+ filteredRoots.add(itemId);
+ }
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return itemId;
+ }
+
+ @Override
+ protected void fireItemSetChange(
+ com.vaadin.data.Container.ItemSetChangeEvent event) {
+ if (contentsChangeEventsOn()) {
+ super.fireItemSetChange(event);
+ } else {
+ contentsChangedEventPending = true;
+ }
+ }
+
+ private boolean contentsChangeEventsOn() {
+ return !contentChangedEventsDisabled;
+ }
+
+ private void disableContentsChangeEvents() {
+ contentChangedEventsDisabled = true;
+ }
+
+ private void enableAndFireContentsChangeEvents() {
+ contentChangedEventsDisabled = false;
+ if (contentsChangedEventPending) {
+ fireItemSetChange();
+ }
+ contentsChangedEventPending = false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) {
+ disableContentsChangeEvents();
+ final Item item = super.addItem(itemId);
+ if (item == null) {
+ return null;
+ }
+
+ roots.add(itemId);
+
+ if (filteredRoots != null) {
+ if (passesFilters(itemId)) {
+ filteredRoots.add(itemId);
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return item;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ disableContentsChangeEvents();
+ final boolean success = super.removeAllItems();
+
+ if (success) {
+ roots.clear();
+ parent.clear();
+ children.clear();
+ noChildrenAllowed.clear();
+ if (filteredRoots != null) {
+ filteredRoots = null;
+ }
+ if (filteredChildren != null) {
+ filteredChildren = null;
+ }
+ if (filteredParent != null) {
+ filteredParent = null;
+ }
+ }
+ enableAndFireContentsChangeEvents();
+ return success;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#removeItem(java.lang.Object )
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ disableContentsChangeEvents();
+ final boolean success = super.removeItem(itemId);
+
+ if (success) {
+ // Remove from roots if this was a root
+ if (roots.remove(itemId)) {
+
+ // If filtering is enabled we might need to remove it from the
+ // filtered list also
+ if (filteredRoots != null) {
+ filteredRoots.remove(itemId);
+ }
+ }
+
+ // Clear the children list. Old children will now become root nodes
+ LinkedList<Object> childNodeIds = children.remove(itemId);
+ if (childNodeIds != null) {
+ if (filteredChildren != null) {
+ filteredChildren.remove(itemId);
+ }
+ for (Object childId : childNodeIds) {
+ setParent(childId, null);
+ }
+ }
+
+ // Parent of the item that we are removing will contain the item id
+ // in its children list
+ final Object parentItemId = parent.get(itemId);
+ if (parentItemId != null) {
+ final LinkedList<Object> c = children.get(parentItemId);
+ if (c != null) {
+ c.remove(itemId);
+
+ if (c.isEmpty()) {
+ children.remove(parentItemId);
+ }
+
+ // Found in the children list so might also be in the
+ // filteredChildren list
+ if (filteredChildren != null) {
+ LinkedList<Object> f = filteredChildren
+ .get(parentItemId);
+ if (f != null) {
+ f.remove(itemId);
+ if (f.isEmpty()) {
+ filteredChildren.remove(parentItemId);
+ }
+ }
+ }
+ }
+ }
+ parent.remove(itemId);
+ if (filteredParent != null) {
+ // Item id no longer has a parent as the item id is not in the
+ // container.
+ filteredParent.remove(itemId);
+ }
+ noChildrenAllowed.remove(itemId);
+ }
+
+ enableAndFireContentsChangeEvents();
+
+ return success;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children.
+ *
+ * @see #removeItem(Object)
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public boolean removeItemRecursively(Object itemId) {
+ disableContentsChangeEvents();
+ boolean removeItemRecursively = removeItemRecursively(this, itemId);
+ enableAndFireContentsChangeEvents();
+ return removeItemRecursively;
+ }
+
+ /**
+ * Removes the Item identified by given itemId and all its children from the
+ * given Container.
+ *
+ * @param container
+ * the container where the item is to be removed
+ * @param itemId
+ * the identifier of the Item to be removed
+ * @return true if the operation succeeded
+ */
+ public static boolean removeItemRecursively(
+ Container.Hierarchical container, Object itemId) {
+ boolean success = true;
+ Collection<?> children2 = container.getChildren(itemId);
+ if (children2 != null) {
+ Object[] array = children2.toArray();
+ for (int i = 0; i < array.length; i++) {
+ boolean removeItemRecursively = removeItemRecursively(
+ container, array[i]);
+ if (!removeItemRecursively) {
+ success = false;
+ }
+ }
+ }
+ // remove the root of subtree if children where succesfully removed
+ if (success) {
+ success = container.removeItem(itemId);
+ }
+ return success;
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#doSort()
+ */
+ @Override
+ protected void doSort() {
+ super.doSort();
+
+ Collections.sort(roots, getItemSorter());
+ for (LinkedList<Object> childList : children.values()) {
+ Collections.sort(childList, getItemSorter());
+ }
+ }
+
+ /**
+ * Used to control how filtering works. @see
+ * {@link #setIncludeParentsWhenFiltering(boolean)} for more information.
+ *
+ * @return true if all parents for items that match the filter are included
+ * when filtering, false if only the matching items are included
+ */
+ public boolean isIncludeParentsWhenFiltering() {
+ return includeParentsWhenFiltering;
+ }
+
+ /**
+ * Controls how the filtering of the container works. Set this to true to
+ * make filtering include parents for all matched items in addition to the
+ * items themselves. Setting this to false causes the filtering to only
+ * include the matching items and make items with excluded parents into root
+ * items.
+ *
+ * @param includeParentsWhenFiltering
+ * true to include all parents for items that match the filter,
+ * false to only include the matching items
+ */
+ public void setIncludeParentsWhenFiltering(
+ boolean includeParentsWhenFiltering) {
+ this.includeParentsWhenFiltering = includeParentsWhenFiltering;
+ if (filteredRoots != null) {
+ // Currently filtered so needs to be re-filtered
+ doFilterContainer(true);
+ }
+ }
+
+ /*
+ * Overridden to provide filtering for root & children items.
+ *
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.IndexedContainer#updateContainerFiltering()
+ */
+ @Override
+ protected boolean doFilterContainer(boolean hasFilters) {
+ if (!hasFilters) {
+ // All filters removed
+ filteredRoots = null;
+ filteredChildren = null;
+ filteredParent = null;
+
+ return super.doFilterContainer(hasFilters);
+ }
+
+ // Reset data structures
+ filteredRoots = new LinkedList<Object>();
+ filteredChildren = new HashMap<Object, LinkedList<Object>>();
+ filteredParent = new HashMap<Object, Object>();
+
+ if (includeParentsWhenFiltering) {
+ // Filter so that parents for items that match the filter are also
+ // included
+ HashSet<Object> includedItems = new HashSet<Object>();
+ for (Object rootId : roots) {
+ if (filterIncludingParents(rootId, includedItems)) {
+ filteredRoots.add(rootId);
+ addFilteredChildrenRecursively(rootId, includedItems);
+ }
+ }
+ // includedItemIds now contains all the item ids that should be
+ // included. Filter IndexedContainer based on this
+ filterOverride = includedItems;
+ super.doFilterContainer(hasFilters);
+ filterOverride = null;
+
+ return true;
+ } else {
+ // Filter by including all items that pass the filter and make items
+ // with no parent new root items
+
+ // Filter IndexedContainer first so getItemIds return the items that
+ // match
+ super.doFilterContainer(hasFilters);
+
+ LinkedHashSet<Object> filteredItemIds = new LinkedHashSet<Object>(
+ getItemIds());
+
+ for (Object itemId : filteredItemIds) {
+ Object itemParent = parent.get(itemId);
+ if (itemParent == null || !filteredItemIds.contains(itemParent)) {
+ // Parent is not included or this was a root, in both cases
+ // this should be a filtered root
+ filteredRoots.add(itemId);
+ } else {
+ // Parent is included. Add this to the children list (create
+ // it first if necessary)
+ addFilteredChild(itemParent, itemId);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /**
+ * Adds the given childItemId as a filteredChildren for the parentItemId and
+ * sets it filteredParent.
+ *
+ * @param parentItemId
+ * @param childItemId
+ */
+ private void addFilteredChild(Object parentItemId, Object childItemId) {
+ LinkedList<Object> parentToChildrenList = filteredChildren
+ .get(parentItemId);
+ if (parentToChildrenList == null) {
+ parentToChildrenList = new LinkedList<Object>();
+ filteredChildren.put(parentItemId, parentToChildrenList);
+ }
+ filteredParent.put(childItemId, parentItemId);
+ parentToChildrenList.add(childItemId);
+
+ }
+
+ /**
+ * Recursively adds all items in the includedItems list to the
+ * filteredChildren map in the same order as they are in the children map.
+ * Starts from parentItemId and recurses down as long as child items that
+ * should be included are found.
+ *
+ * @param parentItemId
+ * The item id to start recurse from. Not added to a
+ * filteredChildren list
+ * @param includedItems
+ * Set containing the item ids for the items that should be
+ * included in the filteredChildren map
+ */
+ private void addFilteredChildrenRecursively(Object parentItemId,
+ HashSet<Object> includedItems) {
+ LinkedList<Object> childList = children.get(parentItemId);
+ if (childList == null) {
+ return;
+ }
+
+ for (Object childItemId : childList) {
+ if (includedItems.contains(childItemId)) {
+ addFilteredChild(parentItemId, childItemId);
+ addFilteredChildrenRecursively(childItemId, includedItems);
+ }
+ }
+ }
+
+ /**
+ * Scans the itemId and all its children for which items should be included
+ * when filtering. All items which passes the filters are included.
+ * Additionally all items that have a child node that should be included are
+ * also themselves included.
+ *
+ * @param itemId
+ * @param includedItems
+ * @return true if the itemId should be included in the filtered container.
+ */
+ private boolean filterIncludingParents(Object itemId,
+ HashSet<Object> includedItems) {
+ boolean toBeIncluded = passesFilters(itemId);
+
+ LinkedList<Object> childList = children.get(itemId);
+ if (childList != null) {
+ for (Object childItemId : children.get(itemId)) {
+ toBeIncluded |= filterIncludingParents(childItemId,
+ includedItems);
+ }
+ }
+
+ if (toBeIncluded) {
+ includedItems.add(itemId);
+ }
+ return toBeIncluded;
+ }
+
+ private Set<Object> filterOverride = null;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.IndexedContainer#passesFilters(java.lang.Object)
+ */
+ @Override
+ protected boolean passesFilters(Object itemId) {
+ if (filterOverride != null) {
+ return filterOverride.contains(itemId);
+ } else {
+ return super.passesFilters(itemId);
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java
new file mode 100644
index 0000000000..172dc0dd4f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java
@@ -0,0 +1,70 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container.Hierarchical;
+
+/**
+ * A wrapper class for adding external ordering to containers not implementing
+ * the {@link com.vaadin.data.Container.Ordered} interface while retaining
+ * {@link Hierarchical} features.
+ *
+ * @see ContainerOrderedWrapper
+ */
+@SuppressWarnings({ "serial" })
+public class HierarchicalContainerOrderedWrapper extends
+ ContainerOrderedWrapper implements Hierarchical {
+
+ private Hierarchical hierarchical;
+
+ public HierarchicalContainerOrderedWrapper(Hierarchical toBeWrapped) {
+ super(toBeWrapped);
+ hierarchical = toBeWrapped;
+ }
+
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return hierarchical.areChildrenAllowed(itemId);
+ }
+
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return hierarchical.getChildren(itemId);
+ }
+
+ @Override
+ public Object getParent(Object itemId) {
+ return hierarchical.getParent(itemId);
+ }
+
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return hierarchical.hasChildren(itemId);
+ }
+
+ @Override
+ public boolean isRoot(Object itemId) {
+ return hierarchical.isRoot(itemId);
+ }
+
+ @Override
+ public Collection<?> rootItemIds() {
+ return hierarchical.rootItemIds();
+ }
+
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+ return hierarchical.setChildrenAllowed(itemId, areChildrenAllowed);
+ }
+
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+ return hierarchical.setParent(itemId, newParentId);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java
new file mode 100644
index 0000000000..b95b2c4de8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/IndexedContainer.java
@@ -0,0 +1,1109 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+/**
+ * An implementation of the <code>{@link Container.Indexed}</code> interface
+ * with all important features.</p>
+ *
+ * Features:
+ * <ul>
+ * <li> {@link Container.Indexed}
+ * <li> {@link Container.Ordered}
+ * <li> {@link Container.Sortable}
+ * <li> {@link Container.Filterable}
+ * <li> {@link Cloneable} (deprecated, might be removed in the future)
+ * <li>Sends all needed events on content changes.
+ * </ul>
+ *
+ * @see com.vaadin.data.Container
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+
+@SuppressWarnings("serial")
+// item type is really IndexedContainerItem, but using Item not to show it in
+// public API
+public class IndexedContainer extends
+ AbstractInMemoryContainer<Object, Object, Item> implements
+ Container.PropertySetChangeNotifier, Property.ValueChangeNotifier,
+ Container.Sortable, Cloneable, Container.Filterable,
+ Container.SimpleFilterable {
+
+ /* Internal structure */
+
+ /**
+ * Linked list of ordered Property IDs.
+ */
+ private ArrayList<Object> propertyIds = new ArrayList<Object>();
+
+ /**
+ * Property ID to type mapping.
+ */
+ private Hashtable<Object, Class<?>> types = new Hashtable<Object, Class<?>>();
+
+ /**
+ * Hash of Items, where each Item is implemented as a mapping from Property
+ * ID to Property value.
+ */
+ private Hashtable<Object, Map<Object, Object>> items = new Hashtable<Object, Map<Object, Object>>();
+
+ /**
+ * Set of properties that are read-only.
+ */
+ private HashSet<Property<?>> readOnlyProperties = new HashSet<Property<?>>();
+
+ /**
+ * List of all Property value change event listeners listening all the
+ * properties.
+ */
+ private LinkedList<Property.ValueChangeListener> propertyValueChangeListeners = null;
+
+ /**
+ * Data structure containing all listeners interested in changes to single
+ * Properties. The data structure is a hashtable mapping Property IDs to a
+ * hashtable that maps Item IDs to a linked list of listeners listening
+ * Property identified by given Property ID and Item ID.
+ */
+ private Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>> singlePropertyValueChangeListeners = null;
+
+ private HashMap<Object, Object> defaultPropertyValues;
+
+ private int nextGeneratedItemId = 1;
+
+ /* Container constructors */
+
+ public IndexedContainer() {
+ super();
+ }
+
+ public IndexedContainer(Collection<?> itemIds) {
+ this();
+ if (items != null) {
+ for (final Iterator<?> i = itemIds.iterator(); i.hasNext();) {
+ Object itemId = i.next();
+ internalAddItemAtEnd(itemId, new IndexedContainerItem(itemId),
+ false);
+ }
+ filterAll();
+ }
+ }
+
+ /* Container methods */
+
+ @Override
+ protected Item getUnfilteredItem(Object itemId) {
+ if (itemId != null && items.containsKey(itemId)) {
+ return new IndexedContainerItem(itemId);
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /**
+ * Gets the type of a Property stored in the list.
+ *
+ * @param id
+ * the ID of the Property.
+ * @return Type of the requested Property
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return types.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ if (!containsId(itemId)) {
+ return null;
+ }
+
+ return new IndexedContainerProperty(itemId, propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) {
+
+ // Fails, if nulls are given
+ if (propertyId == null || type == null) {
+ return false;
+ }
+
+ // Fails if the Property is already present
+ if (propertyIds.contains(propertyId)) {
+ return false;
+ }
+
+ // Adds the Property to Property list and types
+ propertyIds.add(propertyId);
+ types.put(propertyId, type);
+
+ // If default value is given, set it
+ if (defaultValue != null) {
+ // for existing rows
+ for (final Iterator<?> i = getAllItemIds().iterator(); i.hasNext();) {
+ getItem(i.next()).getItemProperty(propertyId).setValue(
+ defaultValue);
+ }
+ // store for next rows
+ if (defaultPropertyValues == null) {
+ defaultPropertyValues = new HashMap<Object, Object>();
+ }
+ defaultPropertyValues.put(propertyId, defaultValue);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() {
+ int origSize = size();
+
+ internalRemoveAllItems();
+
+ items.clear();
+
+ // fire event only if the visible view changed, regardless of whether
+ // filtered out items were removed or not
+ if (origSize != 0) {
+ // Sends a change event
+ fireItemSetChange();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ // Adds the Item into container
+ addItem(id);
+
+ return id;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) {
+ Item item = internalAddItemAtEnd(itemId, new IndexedContainerItem(
+ itemId), false);
+ if (!isFiltered()) {
+ // always the last item
+ fireItemAdded(size() - 1, itemId, item);
+ } else if (passesFilters(itemId) && !containsId(itemId)) {
+ getFilteredItemIds().add(itemId);
+ // always the last item
+ fireItemAdded(size() - 1, itemId, item);
+ }
+ return item;
+ }
+
+ /**
+ * Helper method to add default values for items if available
+ *
+ * @param t
+ * data table of added item
+ */
+ private void addDefaultValues(Hashtable<Object, Object> t) {
+ if (defaultPropertyValues != null) {
+ for (Object key : defaultPropertyValues.keySet()) {
+ t.put(key, defaultPropertyValues.get(key));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId) {
+ if (itemId == null || items.remove(itemId) == null) {
+ return false;
+ }
+ int origSize = size();
+ int position = indexOfId(itemId);
+ if (internalRemoveItem(itemId)) {
+ // fire event only if the visible view changed, regardless of
+ // whether filtered out items were removed or not
+ if (size() != origSize) {
+ fireItemRemoved(position, itemId);
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object )
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId) {
+
+ // Fails if the Property is not present
+ if (!propertyIds.contains(propertyId)) {
+ return false;
+ }
+
+ // Removes the Property to Property list and types
+ propertyIds.remove(propertyId);
+ types.remove(propertyId);
+ if (defaultPropertyValues != null) {
+ defaultPropertyValues.remove(propertyId);
+ }
+
+ // If remove the Property from all Items
+ for (final Iterator<Object> i = getAllItemIds().iterator(); i.hasNext();) {
+ items.get(i.next()).remove(propertyId);
+ }
+
+ // Sends a change event
+ fireContainerPropertySetChange();
+
+ return true;
+ }
+
+ /* Container.Ordered methods */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId) {
+ return internalAddItemAfter(previousItemId, newItemId,
+ new IndexedContainerItem(newItemId), true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId) {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ if (addItemAfter(previousItemId, id) != null) {
+ return id;
+ } else {
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
+ */
+ @Override
+ public Item addItemAt(int index, Object newItemId) {
+ return internalAddItemAt(index, newItemId, new IndexedContainerItem(
+ newItemId), true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+ */
+ @Override
+ public Object addItemAt(int index) {
+
+ // Creates a new id
+ final Object id = generateId();
+
+ // Adds the Item into container
+ addItemAt(index, id);
+
+ return id;
+ }
+
+ /**
+ * Generates an unique identifier for use as an item id. Guarantees that the
+ * generated id is not currently used as an id.
+ *
+ * @return
+ */
+ private Serializable generateId() {
+ Serializable id;
+ do {
+ id = Integer.valueOf(nextGeneratedItemId++);
+ } while (items.containsKey(id));
+
+ return id;
+ }
+
+ @Override
+ protected void registerNewItem(int index, Object newItemId, Item item) {
+ Hashtable<Object, Object> t = new Hashtable<Object, Object>();
+ items.put(newItemId, t);
+ addDefaultValues(t);
+ }
+
+ /* Event notifiers */
+
+ /**
+ * An <code>event</code> object specifying the list whose Item set has
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class ItemSetChangeEvent extends BaseItemSetChangeEvent {
+
+ private final int addedItemIndex;
+
+ private ItemSetChangeEvent(IndexedContainer source, int addedItemIndex) {
+ super(source);
+ this.addedItemIndex = addedItemIndex;
+ }
+
+ /**
+ * Iff one item is added, gives its index.
+ *
+ * @return -1 if either multiple items are changed or some other change
+ * than add is done.
+ */
+ public int getAddedItemIndex() {
+ return addedItemIndex;
+ }
+
+ }
+
+ /**
+ * An <code>event</code> object specifying the Property in a list whose
+ * value has changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private static class PropertyValueChangeEvent extends EventObject implements
+ Property.ValueChangeEvent, Serializable {
+
+ private PropertyValueChangeEvent(Property source) {
+ super(source);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeEvent#getProperty()
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+
+ }
+
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ super.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ super.removeListener(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com.
+ * vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ if (propertyValueChangeListeners == null) {
+ propertyValueChangeListeners = new LinkedList<Property.ValueChangeListener>();
+ }
+ propertyValueChangeListeners.add(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com
+ * .vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ if (propertyValueChangeListeners != null) {
+ propertyValueChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a Property value change event to all interested listeners.
+ *
+ * @param source
+ * the IndexedContainerProperty object.
+ */
+ private void firePropertyValueChange(IndexedContainerProperty source) {
+
+ // Sends event to listeners listening all value changes
+ if (propertyValueChangeListeners != null) {
+ final Object[] l = propertyValueChangeListeners.toArray();
+ final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent(
+ source);
+ for (int i = 0; i < l.length; i++) {
+ ((Property.ValueChangeListener) l[i]).valueChange(event);
+ }
+ }
+
+ // Sends event to single property value change listeners
+ if (singlePropertyValueChangeListeners != null) {
+ final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(source.propertyId);
+ if (propertySetToListenerListMap != null) {
+ final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(source.itemId);
+ if (listenerList != null) {
+ final Property.ValueChangeEvent event = new IndexedContainer.PropertyValueChangeEvent(
+ source);
+ Object[] listeners = listenerList.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Property.ValueChangeListener) listeners[i])
+ .valueChange(event);
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Property.ValueChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertyValueChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertyValueChangeListeners);
+ }
+ }
+ return super.getListeners(eventType);
+ }
+
+ @Override
+ protected void fireItemAdded(int position, Object itemId, Item item) {
+ if (position >= 0) {
+ fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this,
+ position));
+ }
+ }
+
+ @Override
+ protected void fireItemSetChange() {
+ fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, -1));
+ }
+
+ /**
+ * Adds new single Property change listener.
+ *
+ * @param propertyId
+ * the ID of the Property to add.
+ * @param itemId
+ * the ID of the Item .
+ * @param listener
+ * the listener to be added.
+ */
+ private void addSinglePropertyChangeListener(Object propertyId,
+ Object itemId, Property.ValueChangeListener listener) {
+ if (listener != null) {
+ if (singlePropertyValueChangeListeners == null) {
+ singlePropertyValueChangeListeners = new Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>();
+ }
+ Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(propertyId);
+ if (propertySetToListenerListMap == null) {
+ propertySetToListenerListMap = new Hashtable<Object, List<Property.ValueChangeListener>>();
+ singlePropertyValueChangeListeners.put(propertyId,
+ propertySetToListenerListMap);
+ }
+ List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(itemId);
+ if (listenerList == null) {
+ listenerList = new LinkedList<Property.ValueChangeListener>();
+ propertySetToListenerListMap.put(itemId, listenerList);
+ }
+ listenerList.add(listener);
+ }
+ }
+
+ /**
+ * Removes a previously registered single Property change listener.
+ *
+ * @param propertyId
+ * the ID of the Property to remove.
+ * @param itemId
+ * the ID of the Item.
+ * @param listener
+ * the listener to be removed.
+ */
+ private void removeSinglePropertyChangeListener(Object propertyId,
+ Object itemId, Property.ValueChangeListener listener) {
+ if (listener != null && singlePropertyValueChangeListeners != null) {
+ final Map<Object, List<Property.ValueChangeListener>> propertySetToListenerListMap = singlePropertyValueChangeListeners
+ .get(propertyId);
+ if (propertySetToListenerListMap != null) {
+ final List<Property.ValueChangeListener> listenerList = propertySetToListenerListMap
+ .get(itemId);
+ if (listenerList != null) {
+ listenerList.remove(listener);
+ if (listenerList.isEmpty()) {
+ propertySetToListenerListMap.remove(itemId);
+ }
+ }
+ if (propertySetToListenerListMap.isEmpty()) {
+ singlePropertyValueChangeListeners.remove(propertyId);
+ }
+ }
+ if (singlePropertyValueChangeListeners.isEmpty()) {
+ singlePropertyValueChangeListeners = null;
+ }
+ }
+ }
+
+ /* Internal Item and Property implementations */
+
+ /*
+ * A class implementing the com.vaadin.data.Item interface to be contained
+ * in the list.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @version @VERSION@
+ *
+ * @since 3.0
+ */
+ class IndexedContainerItem implements Item {
+
+ /**
+ * Item ID in the host container for this Item.
+ */
+ private final Object itemId;
+
+ /**
+ * Constructs a new ListItem instance and connects it to a host
+ * container.
+ *
+ * @param itemId
+ * the Item ID of the new Item.
+ */
+ private IndexedContainerItem(Object itemId) {
+
+ // Gets the item contents from the host
+ if (itemId == null) {
+ throw new NullPointerException();
+ }
+ this.itemId = itemId;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Item#getItemProperty(java.lang.Object)
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return new IndexedContainerProperty(itemId, id);
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /**
+ * Gets the <code>String</code> representation of the contents of the
+ * Item. The format of the string is a space separated catenation of the
+ * <code>String</code> representations of the values of the Properties
+ * contained by the Item.
+ *
+ * @return <code>String</code> representation of the Item contents
+ */
+ @Override
+ public String toString() {
+ String retValue = "";
+
+ for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) {
+ final Object propertyId = i.next();
+ retValue += getItemProperty(propertyId).getValue();
+ if (i.hasNext()) {
+ retValue += " ";
+ }
+ }
+
+ return retValue;
+ }
+
+ /**
+ * Calculates a integer hash-code for the Item that's unique inside the
+ * list. Two Items inside the same list have always different
+ * hash-codes, though Items in different lists may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return itemId.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two Items
+ * got from a list container with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null
+ || !obj.getClass().equals(IndexedContainerItem.class)) {
+ return false;
+ }
+ final IndexedContainerItem li = (IndexedContainerItem) obj;
+ return getHost() == li.getHost() && itemId.equals(li.itemId);
+ }
+
+ private IndexedContainer getHost() {
+ return IndexedContainer.this;
+ }
+
+ /**
+ * IndexedContainerItem does not support adding new properties. Add
+ * properties at container level. See
+ * {@link IndexedContainer#addContainerProperty(Object, Class, Object)}
+ *
+ * @see com.vaadin.data.Item#addProperty(Object, Property)
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Indexed container item "
+ + "does not support adding new properties");
+ }
+
+ /**
+ * Indexed container does not support removing properties. Remove
+ * properties at container level. See
+ * {@link IndexedContainer#removeContainerProperty(Object)}
+ *
+ * @see com.vaadin.data.Item#removeProperty(Object)
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException(
+ "Indexed container item does not support property removal");
+ }
+
+ }
+
+ /**
+ * A class implementing the {@link Property} interface to be contained in
+ * the {@link IndexedContainerItem} contained in the
+ * {@link IndexedContainer}.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private class IndexedContainerProperty implements Property<Object>,
+ Property.ValueChangeNotifier {
+
+ /**
+ * ID of the Item, where this property resides.
+ */
+ private final Object itemId;
+
+ /**
+ * Id of the Property.
+ */
+ private final Object propertyId;
+
+ /**
+ * Constructs a new {@link IndexedContainerProperty} object.
+ *
+ * @param itemId
+ * the ID of the Item to connect the new Property to.
+ * @param propertyId
+ * the Property ID of the new Property.
+ * @param host
+ * the list that contains the Item to contain the new
+ * Property.
+ */
+ private IndexedContainerProperty(Object itemId, Object propertyId) {
+ if (itemId == null || propertyId == null) {
+ // Null ids are not accepted
+ throw new NullPointerException(
+ "Container item or property ids can not be null");
+ }
+ this.propertyId = propertyId;
+ this.itemId = itemId;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getType()
+ */
+ @Override
+ public Class<?> getType() {
+ return types.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getValue()
+ */
+ @Override
+ public Object getValue() {
+ return items.get(itemId).get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#isReadOnly()
+ */
+ @Override
+ public boolean isReadOnly() {
+ return readOnlyProperties.contains(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setReadOnly(boolean)
+ */
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (newStatus) {
+ readOnlyProperties.add(this);
+ } else {
+ readOnlyProperties.remove(this);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+ // Gets the Property set
+ final Map<Object, Object> propertySet = items.get(itemId);
+
+ // Support null values on all types
+ if (newValue == null) {
+ propertySet.remove(propertyId);
+ } else if (getType().isAssignableFrom(newValue.getClass())) {
+ propertySet.put(propertyId, newValue);
+ } else {
+ throw new IllegalArgumentException(
+ "Value is of invalid type, got "
+ + newValue.getClass().getName() + " but "
+ + getType().getName() + " was expected");
+ }
+
+ // update the container filtering if this property is being filtered
+ if (isPropertyFiltered(propertyId)) {
+ filterAll();
+ }
+
+ firePropertyValueChange(this);
+ }
+
+ /**
+ * 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.
+ *
+ * @return <code>String</code> representation of the value stored in the
+ * Property
+ * @deprecated use {@link #getValue()} instead and possibly toString on
+ * that
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use Property.getValue() instead of IndexedContainerProperty.toString()");
+ }
+
+ /**
+ * Calculates a integer hash-code for the Property that's unique inside
+ * the Item containing the Property. Two different Properties inside the
+ * same Item contained in the same list always have different
+ * hash-codes, though Properties in different Items may have identical
+ * hash-codes.
+ *
+ * @return A locally unique hash-code as integer
+ */
+ @Override
+ public int hashCode() {
+ return itemId.hashCode() ^ propertyId.hashCode();
+ }
+
+ /**
+ * Tests if the given object is the same as the this object. Two
+ * Properties got from an Item with the same ID are equal.
+ *
+ * @param obj
+ * an object to compare with this object
+ * @return <code>true</code> if the given object is the same as this
+ * object, <code>false</code> if not
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null
+ || !obj.getClass().equals(IndexedContainerProperty.class)) {
+ return false;
+ }
+ final IndexedContainerProperty lp = (IndexedContainerProperty) obj;
+ return lp.getHost() == getHost()
+ && lp.propertyId.equals(propertyId)
+ && lp.itemId.equals(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(
+ * com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ addSinglePropertyChangeListener(propertyId, itemId, listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener
+ * (com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ removeSinglePropertyChangeListener(propertyId, itemId, listener);
+ }
+
+ private IndexedContainer getHost() {
+ return IndexedContainer.this;
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sortContainer(propertyId, ascending);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds
+ * ()
+ */
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getSortablePropertyIds();
+ }
+
+ @Override
+ public ItemSorter getItemSorter() {
+ return super.getItemSorter();
+ }
+
+ @Override
+ public void setItemSorter(ItemSorter itemSorter) {
+ super.setItemSorter(itemSorter);
+ }
+
+ /**
+ * Supports cloning of the IndexedContainer cleanly.
+ *
+ * @throws CloneNotSupportedException
+ * if an object cannot be cloned. .
+ *
+ * @deprecated cloning support might be removed from IndexedContainer in the
+ * future
+ */
+ @Deprecated
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+
+ // Creates the clone
+ final IndexedContainer nc = new IndexedContainer();
+
+ // Clone the shallow properties
+ nc.setAllItemIds(getAllItemIds() != null ? (ListSet<Object>) ((ListSet<Object>) getAllItemIds())
+ .clone() : null);
+ nc.setItemSetChangeListeners(getItemSetChangeListeners() != null ? new LinkedList<Container.ItemSetChangeListener>(
+ getItemSetChangeListeners()) : null);
+ nc.propertyIds = propertyIds != null ? (ArrayList<Object>) propertyIds
+ .clone() : null;
+ nc.setPropertySetChangeListeners(getPropertySetChangeListeners() != null ? new LinkedList<Container.PropertySetChangeListener>(
+ getPropertySetChangeListeners()) : null);
+ nc.propertyValueChangeListeners = propertyValueChangeListeners != null ? (LinkedList<Property.ValueChangeListener>) propertyValueChangeListeners
+ .clone() : null;
+ nc.readOnlyProperties = readOnlyProperties != null ? (HashSet<Property<?>>) readOnlyProperties
+ .clone() : null;
+ nc.singlePropertyValueChangeListeners = singlePropertyValueChangeListeners != null ? (Hashtable<Object, Map<Object, List<Property.ValueChangeListener>>>) singlePropertyValueChangeListeners
+ .clone() : null;
+
+ nc.types = types != null ? (Hashtable<Object, Class<?>>) types.clone()
+ : null;
+
+ nc.setFilters((HashSet<Filter>) ((HashSet<Filter>) getFilters())
+ .clone());
+
+ nc.setFilteredItemIds(getFilteredItemIds() == null ? null
+ : (ListSet<Object>) ((ListSet<Object>) getFilteredItemIds())
+ .clone());
+
+ // Clone property-values
+ if (items == null) {
+ nc.items = null;
+ } else {
+ nc.items = new Hashtable<Object, Map<Object, Object>>();
+ for (final Iterator<?> i = items.keySet().iterator(); i.hasNext();) {
+ final Object id = i.next();
+ final Hashtable<Object, Object> it = (Hashtable<Object, Object>) items
+ .get(id);
+ nc.items.put(id, (Map<Object, Object>) it.clone());
+ }
+ }
+
+ return nc;
+ }
+
+ @Override
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ try {
+ addFilter(new SimpleStringFilter(propertyId, filterString,
+ ignoreCase, onlyMatchPrefix));
+ } catch (UnsupportedFilterException e) {
+ // the filter instance created here is always valid for in-memory
+ // containers
+ }
+ }
+
+ @Override
+ public void removeAllContainerFilters() {
+ removeAllFilters();
+ }
+
+ @Override
+ public void removeContainerFilters(Object propertyId) {
+ removeFilters(propertyId);
+ }
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ addFilter(filter);
+ }
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ removeFilter(filter);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ItemSorter.java b/server/src/com/vaadin/data/util/ItemSorter.java
new file mode 100644
index 0000000000..4399dbe292
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ItemSorter.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+import java.util.Comparator;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Sortable;
+
+/**
+ * An item comparator which is compatible with the {@link Sortable} interface.
+ * The <code>ItemSorter</code> interface can be used in <code>Sortable</code>
+ * implementations to provide a custom sorting method.
+ */
+public interface ItemSorter extends Comparator<Object>, Cloneable, Serializable {
+
+ /**
+ * Sets the parameters for an upcoming sort operation. The parameters
+ * determine what container to sort and how the <code>ItemSorter</code>
+ * sorts the container.
+ *
+ * @param container
+ * The container that will be sorted. The container must contain
+ * the propertyIds given in the <code>propertyId</code>
+ * parameter.
+ * @param propertyId
+ * The property ids used for sorting. The property ids must exist
+ * in the container and should only be used if they are also
+ * sortable, i.e include in the collection returned by
+ * <code>container.getSortableContainerPropertyIds()</code>. See
+ * {@link Sortable#sort(Object[], boolean[])} for more
+ * information.
+ * @param ascending
+ * Sorting order flags for each property id. See
+ * {@link Sortable#sort(Object[], boolean[])} for more
+ * information.
+ */
+ void setSortProperties(Container.Sortable container, Object[] propertyId,
+ boolean[] ascending);
+
+ /**
+ * Compares its two arguments for order. Returns a negative integer, zero,
+ * or a positive integer as the first argument is less than, equal to, or
+ * greater than the second.
+ * <p>
+ * The parameters for the <code>ItemSorter</code> <code>compare()</code>
+ * method must always be item ids which exist in the container set using
+ * {@link #setSortProperties(Sortable, Object[], boolean[])}.
+ *
+ * @see Comparator#compare(Object, Object)
+ */
+ @Override
+ int compare(Object itemId1, Object itemId2);
+
+}
diff --git a/server/src/com/vaadin/data/util/ListSet.java b/server/src/com/vaadin/data/util/ListSet.java
new file mode 100644
index 0000000000..b71cc46898
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ListSet.java
@@ -0,0 +1,264 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * ListSet is an internal Vaadin class which implements a combination of a List
+ * and a Set. The main purpose of this class is to provide a list with a fast
+ * {@link #contains(Object)} method. Each inserted object must by unique (as
+ * specified by {@link #equals(Object)}). The {@link #set(int, Object)} method
+ * allows duplicates because of the way {@link Collections#sort(java.util.List)}
+ * works.
+ *
+ * This class is subject to change and should not be used outside Vaadin core.
+ */
+public class ListSet<E> extends ArrayList<E> {
+ private HashSet<E> itemSet = null;
+
+ /**
+ * Contains a map from an element to the number of duplicates it has. Used
+ * to temporarily allow duplicates in the list.
+ */
+ private HashMap<E, Integer> duplicates = new HashMap<E, Integer>();
+
+ public ListSet() {
+ super();
+ itemSet = new HashSet<E>();
+ }
+
+ public ListSet(Collection<? extends E> c) {
+ super(c);
+ itemSet = new HashSet<E>(c.size());
+ itemSet.addAll(c);
+ }
+
+ public ListSet(int initialCapacity) {
+ super(initialCapacity);
+ itemSet = new HashSet<E>(initialCapacity);
+ }
+
+ // Delegate contains operations to the set
+ @Override
+ public boolean contains(Object o) {
+ return itemSet.contains(o);
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return itemSet.containsAll(c);
+ }
+
+ // Methods for updating the set when the list is updated.
+ @Override
+ public boolean add(E e) {
+ if (contains(e)) {
+ // Duplicates are not allowed
+ return false;
+ }
+
+ if (super.add(e)) {
+ itemSet.add(e);
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ /**
+ * Works as java.util.ArrayList#add(int, java.lang.Object) but returns
+ * immediately if the element is already in the ListSet.
+ */
+ @Override
+ public void add(int index, E element) {
+ if (contains(element)) {
+ // Duplicates are not allowed
+ return;
+ }
+
+ super.add(index, element);
+ itemSet.add(element);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends E> c) {
+ boolean modified = false;
+ Iterator<? extends E> i = c.iterator();
+ while (i.hasNext()) {
+ E e = i.next();
+ if (contains(e)) {
+ continue;
+ }
+
+ if (add(e)) {
+ itemSet.add(e);
+ modified = true;
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends E> c) {
+ ensureCapacity(size() + c.size());
+
+ boolean modified = false;
+ Iterator<? extends E> i = c.iterator();
+ while (i.hasNext()) {
+ E e = i.next();
+ if (contains(e)) {
+ continue;
+ }
+
+ add(index++, e);
+ itemSet.add(e);
+ modified = true;
+ }
+
+ return modified;
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+ itemSet.clear();
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ if (!contains(o)) {
+ return -1;
+ }
+
+ return super.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ if (!contains(o)) {
+ return -1;
+ }
+
+ return super.lastIndexOf(o);
+ }
+
+ @Override
+ public E remove(int index) {
+ E e = super.remove(index);
+
+ if (e != null) {
+ itemSet.remove(e);
+ }
+
+ return e;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if (super.remove(o)) {
+ itemSet.remove(o);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected void removeRange(int fromIndex, int toIndex) {
+ HashSet<E> toRemove = new HashSet<E>();
+ for (int idx = fromIndex; idx < toIndex; idx++) {
+ toRemove.add(get(idx));
+ }
+ super.removeRange(fromIndex, toIndex);
+ itemSet.removeAll(toRemove);
+ }
+
+ @Override
+ public E set(int index, E element) {
+ if (contains(element)) {
+ // Element already exist in the list
+ if (get(index) == element) {
+ // At the same position, nothing to be done
+ return element;
+ } else {
+ // Adding at another position. We assume this is a sort
+ // operation and temporarily allow it.
+
+ // We could just remove (null) the old element and keep the list
+ // unique. This would require finding the index of the old
+ // element (indexOf(element)) which is not a fast operation in a
+ // list. So we instead allow duplicates temporarily.
+ addDuplicate(element);
+ }
+ }
+
+ E old = super.set(index, element);
+ removeFromSet(old);
+ itemSet.add(element);
+
+ return old;
+ }
+
+ /**
+ * Removes "e" from the set if it no longer exists in the list.
+ *
+ * @param e
+ */
+ private void removeFromSet(E e) {
+ Integer dupl = duplicates.get(e);
+ if (dupl != null) {
+ // A duplicate was present so we only decrement the duplicate count
+ // and continue
+ if (dupl == 1) {
+ // This is what always should happen. A sort sets the items one
+ // by one, temporarily breaking the uniqueness requirement.
+ duplicates.remove(e);
+ } else {
+ duplicates.put(e, dupl - 1);
+ }
+ } else {
+ // The "old" value is no longer in the list.
+ itemSet.remove(e);
+ }
+
+ }
+
+ /**
+ * Marks the "element" can be found more than once from the list. Allowed in
+ * {@link #set(int, Object)} to make sorting work.
+ *
+ * @param element
+ */
+ private void addDuplicate(E element) {
+ Integer nr = duplicates.get(element);
+ if (nr == null) {
+ nr = 1;
+ } else {
+ nr++;
+ }
+
+ /*
+ * Store the number of duplicates of this element so we know later on if
+ * we should remove an element from the set or if it was a duplicate (in
+ * removeFromSet)
+ */
+ duplicates.put(element, nr);
+
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object clone() {
+ ListSet<E> v = (ListSet<E>) super.clone();
+ v.itemSet = new HashSet<E>(itemSet);
+ return v;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/MethodProperty.java b/server/src/com/vaadin/data/util/MethodProperty.java
new file mode 100644
index 0000000000..0c64d90481
--- /dev/null
+++ b/server/src/com/vaadin/data/util/MethodProperty.java
@@ -0,0 +1,784 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Property;
+import com.vaadin.util.SerializerHelper;
+
+/**
+ * <p>
+ * Proxy class for creating Properties from pairs of getter and setter methods
+ * of a Bean property. An instance of this class can be thought as having been
+ * attached to a field of an object. Accessing the object through the Property
+ * interface directly manipulates the underlying field.
+ * </p>
+ *
+ * <p>
+ * It's assumed that the return value returned by the getter method is
+ * assignable to the type of the property, and the setter method parameter is
+ * assignable to that value.
+ * </p>
+ *
+ * <p>
+ * A valid getter method must always be available, but instance of this class
+ * can be constructed with a <code>null</code> setter method in which case the
+ * resulting MethodProperty is read-only.
+ * </p>
+ *
+ * <p>
+ * MethodProperty implements Property.ValueChangeNotifier, but does not
+ * automatically know whether or not the getter method will actually return a
+ * new value - value change listeners are always notified when setValue is
+ * called, without verifying what the getter returns.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class MethodProperty<T> extends AbstractProperty<T> {
+
+ /**
+ * The object that includes the property the MethodProperty is bound to.
+ */
+ private transient Object instance;
+
+ /**
+ * Argument arrays for the getter and setter methods.
+ */
+ private transient Object[] setArgs, getArgs;
+
+ /**
+ * The getter and setter methods.
+ */
+ private transient Method setMethod, getMethod;
+
+ /**
+ * Index of the new value in the argument list for the setter method. If the
+ * setter method requires several parameters, this index tells which one is
+ * the actual value to change.
+ */
+ private int setArgumentIndex;
+
+ /**
+ * Type of the property.
+ */
+ private transient Class<? extends T> type;
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ SerializerHelper.writeClass(out, type);
+ out.writeObject(instance);
+ out.writeObject(setArgs);
+ out.writeObject(getArgs);
+ if (setMethod != null) {
+ out.writeObject(setMethod.getName());
+ SerializerHelper
+ .writeClassArray(out, setMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ if (getMethod != null) {
+ out.writeObject(getMethod.getName());
+ SerializerHelper
+ .writeClassArray(out, getMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ };
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ try {
+ @SuppressWarnings("unchecked")
+ // business assumption; type parameters not checked at runtime
+ Class<T> class1 = (Class<T>) SerializerHelper.readClass(in);
+ type = class1;
+ instance = in.readObject();
+ setArgs = (Object[]) in.readObject();
+ getArgs = (Object[]) in.readObject();
+ String name = (String) in.readObject();
+ Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ setMethod = instance.getClass().getMethod(name, paramTypes);
+ } else {
+ setMethod = null;
+ }
+
+ name = (String) in.readObject();
+ paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ getMethod = instance.getClass().getMethod(name, paramTypes);
+ } else {
+ getMethod = null;
+ }
+ } catch (SecurityException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ } catch (NoSuchMethodException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ }
+ };
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from a named bean
+ * property. This constructor takes an object and the name of a bean
+ * property and initializes itself with the accessor methods for the
+ * property.
+ * </p>
+ * <p>
+ * The getter method of a <code>MethodProperty</code> instantiated with this
+ * constructor will be called with no arguments, and the setter method with
+ * only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is unavailable, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * <p>
+ * Method names are constructed from the bean property by adding
+ * get/is/are/set prefix and capitalising the first character in the name of
+ * the given bean property.
+ * </p>
+ *
+ * @param instance
+ * the object that includes the property.
+ * @param beanPropertyName
+ * the name of the property to bind to.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodProperty(Object instance, String beanPropertyName) {
+
+ final Class<?> beanClass = instance.getClass();
+
+ // Assure that the first letter is upper cased (it is a common
+ // mistake to write firstName, not FirstName).
+ if (Character.isLowerCase(beanPropertyName.charAt(0))) {
+ final char[] buf = beanPropertyName.toCharArray();
+ buf[0] = Character.toUpperCase(buf[0]);
+ beanPropertyName = new String(buf);
+ }
+
+ // Find the get method
+ getMethod = null;
+ try {
+ getMethod = initGetterMethod(beanPropertyName, beanClass);
+ } catch (final java.lang.NoSuchMethodException ignored) {
+ throw new MethodException(this, "Bean property " + beanPropertyName
+ + " can not be found");
+ }
+
+ // In case the get method is found, resolve the type
+ Class<?> returnType = getMethod.getReturnType();
+
+ // Finds the set method
+ setMethod = null;
+ try {
+ setMethod = beanClass.getMethod("set" + beanPropertyName,
+ new Class[] { returnType });
+ } catch (final java.lang.NoSuchMethodException skipped) {
+ }
+
+ // Gets the return type from get method
+ if (returnType.isPrimitive()) {
+ type = (Class<T>) convertPrimitiveType(returnType);
+ if (type.isPrimitive()) {
+ throw new MethodException(this, "Bean property "
+ + beanPropertyName
+ + " getter return type must not be void");
+ }
+ } else {
+ type = (Class<T>) returnType;
+ }
+
+ setArguments(new Object[] {}, new Object[] { null }, 0);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethodName
+ * the name of the getter method.
+ * @param setMethodName
+ * the name of the setter method.
+ *
+ */
+ public MethodProperty(Class<? extends T> type, Object instance,
+ String getMethodName, String setMethodName) {
+ this(type, instance, getMethodName, setMethodName, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> with the getter and
+ * setter methods. The getter method of a <code>MethodProperty</code>
+ * instantiated with this constructor will be called with no arguments, and
+ * the setter method with only the new value as the sole argument.
+ * </p>
+ *
+ * <p>
+ * If the setter method is <code>null</code>, the resulting
+ * <code>MethodProperty</code> will be read-only, otherwise it will be
+ * read-write.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethod
+ * the getter method.
+ * @param setMethod
+ * the setter method.
+ */
+ public MethodProperty(Class<? extends T> type, Object instance,
+ Method getMethod, Method setMethod) {
+ this(type, instance, getMethod, setMethod, new Object[] {},
+ new Object[] { null }, 0);
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from named getter
+ * and setter methods and argument lists. The getter method of a
+ * <code>MethodProperty</code> instantiated with this constructor will be
+ * called with the getArgs as arguments. The setArgs will be used as the
+ * arguments for the setter method, though the argument indexed by the
+ * setArgumentIndex will be replaced with the argument passed to the
+ * {@link #setValue(Object newValue)} method.
+ * </p>
+ *
+ * <p>
+ * For example, if the <code>setArgs</code> contains <code>A</code>,
+ * <code>B</code> and <code>C</code>, and <code>setArgumentIndex =
+ * 1</code>, the call <code>methodProperty.setValue(X)</code> would result
+ * in the setter method to be called with the parameter set of
+ * <code>{A, X, C}</code>
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethodName
+ * the name of the getter method.
+ * @param setMethodName
+ * the name of the setter method.
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ public MethodProperty(Class<? extends T> type, Object instance,
+ String getMethodName, String setMethodName, Object[] getArgs,
+ Object[] setArgs, int setArgumentIndex) {
+
+ // Check the setargs and setargs index
+ if (setMethodName != null && setArgs == null) {
+ throw new IndexOutOfBoundsException("The setArgs can not be null");
+ }
+ if (setMethodName != null
+ && (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length)) {
+ throw new IndexOutOfBoundsException(
+ "The setArgumentIndex must be >= 0 and < setArgs.length");
+ }
+
+ // Set type
+ this.type = type;
+
+ // Find set and get -methods
+ final Method[] m = instance.getClass().getMethods();
+
+ // Finds get method
+ boolean found = false;
+ for (int i = 0; i < m.length; i++) {
+
+ // Tests the name of the get Method
+ if (!m[i].getName().equals(getMethodName)) {
+
+ // name does not match, try next method
+ continue;
+ }
+
+ // Tests return type
+ if (!type.equals(m[i].getReturnType())) {
+ continue;
+ }
+
+ // Tests the parameter types
+ final Class<?>[] c = m[i].getParameterTypes();
+ if (c.length != getArgs.length) {
+
+ // not the right amount of parameters, try next method
+ continue;
+ }
+ int j = 0;
+ while (j < c.length) {
+ if (getArgs[j] != null
+ && !c[j].isAssignableFrom(getArgs[j].getClass())) {
+
+ // parameter type does not match, try next method
+ break;
+ }
+ j++;
+ }
+ if (j == c.length) {
+
+ // all paramteters matched
+ if (found == true) {
+ throw new MethodException(this,
+ "Could not uniquely identify " + getMethodName
+ + "-method");
+ } else {
+ found = true;
+ getMethod = m[i];
+ }
+ }
+ }
+ if (found != true) {
+ throw new MethodException(this, "Could not find " + getMethodName
+ + "-method");
+ }
+
+ // Finds set method
+ if (setMethodName != null) {
+
+ // Finds setMethod
+ found = false;
+ for (int i = 0; i < m.length; i++) {
+
+ // Checks name
+ if (!m[i].getName().equals(setMethodName)) {
+
+ // name does not match, try next method
+ continue;
+ }
+
+ // Checks parameter compatibility
+ final Class<?>[] c = m[i].getParameterTypes();
+ if (c.length != setArgs.length) {
+
+ // not the right amount of parameters, try next method
+ continue;
+ }
+ int j = 0;
+ while (j < c.length) {
+ if (setArgs[j] != null
+ && !c[j].isAssignableFrom(setArgs[j].getClass())) {
+
+ // parameter type does not match, try next method
+ break;
+ } else if (j == setArgumentIndex && !c[j].equals(type)) {
+
+ // Property type is not the same as setArg type
+ break;
+ }
+ j++;
+ }
+ if (j == c.length) {
+
+ // all parameters match
+ if (found == true) {
+ throw new MethodException(this,
+ "Could not identify unique " + setMethodName
+ + "-method");
+ } else {
+ found = true;
+ setMethod = m[i];
+ }
+ }
+ }
+ if (found != true) {
+ throw new MethodException(this, "Could not identify "
+ + setMethodName + "-method");
+ }
+ }
+
+ // Gets the return type from get method
+ this.type = (Class<T>) convertPrimitiveType(type);
+
+ setArguments(getArgs, setArgs, setArgumentIndex);
+ this.instance = instance;
+ }
+
+ /**
+ * <p>
+ * Creates a new instance of <code>MethodProperty</code> from the getter and
+ * setter methods, and argument lists.
+ * </p>
+ * <p>
+ * This constructor behaves exactly like
+ * {@link #MethodProperty(Class type, Object instance, String getMethodName, String setMethodName, Object [] getArgs, Object [] setArgs, int setArgumentIndex)}
+ * except that instead of names of the getter and setter methods this
+ * constructor is given the actual methods themselves.
+ * </p>
+ *
+ * @param type
+ * the type of the property.
+ * @param instance
+ * the object that includes the property.
+ * @param getMethod
+ * the getter method.
+ * @param setMethod
+ * the setter method.
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ @SuppressWarnings("unchecked")
+ // cannot use "Class<? extends T>" because of automatic primitive type
+ // conversions
+ public MethodProperty(Class<?> type, Object instance, Method getMethod,
+ Method setMethod, Object[] getArgs, Object[] setArgs,
+ int setArgumentIndex) {
+
+ if (getMethod == null) {
+ throw new MethodException(this,
+ "Property GET-method cannot not be null: " + type);
+ }
+
+ if (setMethod != null) {
+ if (setArgs == null) {
+ throw new IndexOutOfBoundsException(
+ "The setArgs can not be null");
+ }
+ if (setArgumentIndex < 0 || setArgumentIndex >= setArgs.length) {
+ throw new IndexOutOfBoundsException(
+ "The setArgumentIndex must be >= 0 and < setArgs.length");
+ }
+ }
+
+ // Gets the return type from get method
+ Class<? extends T> convertedType = (Class<? extends T>) convertPrimitiveType(type);
+
+ this.getMethod = getMethod;
+ this.setMethod = setMethod;
+ setArguments(getArgs, setArgs, setArgumentIndex);
+ this.instance = instance;
+ this.type = convertedType;
+ }
+
+ /**
+ * Find a getter method for a property (getXyz(), isXyz() or areXyz()).
+ *
+ * @param propertyName
+ * name of the property
+ * @param beanClass
+ * class in which to look for the getter methods
+ * @return Method
+ * @throws NoSuchMethodException
+ * if no getter found
+ */
+ static Method initGetterMethod(String propertyName, final Class<?> beanClass)
+ throws NoSuchMethodException {
+ propertyName = propertyName.substring(0, 1).toUpperCase()
+ + propertyName.substring(1);
+
+ Method getMethod = null;
+ try {
+ getMethod = beanClass.getMethod("get" + propertyName,
+ new Class[] {});
+ } catch (final java.lang.NoSuchMethodException ignored) {
+ try {
+ getMethod = beanClass.getMethod("is" + propertyName,
+ new Class[] {});
+ } catch (final java.lang.NoSuchMethodException ignoredAsWell) {
+ getMethod = beanClass.getMethod("are" + propertyName,
+ new Class[] {});
+ }
+ }
+ return getMethod;
+ }
+
+ static Class<?> convertPrimitiveType(Class<?> type) {
+ // Gets the return type from get method
+ if (type.isPrimitive()) {
+ if (type.equals(Boolean.TYPE)) {
+ type = Boolean.class;
+ } else if (type.equals(Integer.TYPE)) {
+ type = Integer.class;
+ } else if (type.equals(Float.TYPE)) {
+ type = Float.class;
+ } else if (type.equals(Double.TYPE)) {
+ type = Double.class;
+ } else if (type.equals(Byte.TYPE)) {
+ type = Byte.class;
+ } else if (type.equals(Character.TYPE)) {
+ type = Character.class;
+ } else if (type.equals(Short.TYPE)) {
+ type = Short.class;
+ } else if (type.equals(Long.TYPE)) {
+ type = Long.class;
+ }
+ }
+ return type;
+ }
+
+ /**
+ * Returns the type of the Property. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ @Override
+ public final Class<? extends T> getType() {
+ return type;
+ }
+
+ /**
+ * Tests if the object is in read-only mode. In read-only mode calls to
+ * <code>setValue</code> will throw <code>ReadOnlyException</code> and will
+ * not modify the value of the Property.
+ *
+ * @return <code>true</code> if the object is in read-only mode,
+ * <code>false</code> if it's not
+ */
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || (setMethod == null);
+ }
+
+ /**
+ * Gets the value stored in the Property. The value is resolved by calling
+ * the specified getter method with the argument specified at instantiation.
+ *
+ * @return the value of the Property
+ */
+ @Override
+ public T getValue() {
+ try {
+ return (T) getMethod.invoke(instance, getArgs);
+ } catch (final Throwable e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * <p>
+ * Sets the setter method and getter method argument lists.
+ * </p>
+ *
+ * @param getArgs
+ * the fixed argument list to be passed to the getter method.
+ * @param setArgs
+ * the fixed argument list to be passed to the setter method.
+ * @param setArgumentIndex
+ * the index of the argument in <code>setArgs</code> to be
+ * replaced with <code>newValue</code> when
+ * {@link #setValue(Object newValue)} is called.
+ */
+ public void setArguments(Object[] getArgs, Object[] setArgs,
+ int setArgumentIndex) {
+ this.getArgs = new Object[getArgs.length];
+ for (int i = 0; i < getArgs.length; i++) {
+ this.getArgs[i] = getArgs[i];
+ }
+ this.setArgs = new Object[setArgs.length];
+ for (int i = 0; i < setArgs.length; i++) {
+ this.setArgs[i] = setArgs[i];
+ }
+ this.setArgumentIndex = setArgumentIndex;
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * Note that since Vaadin 7, no conversions are performed and the value must
+ * be of the correct type.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode.
+ * @see #invokeSetMethod(Object)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Invalid value type for ObjectProperty.");
+ }
+
+ invokeSetMethod((T) newValue);
+ fireValueChange();
+ }
+
+ /**
+ * Internal method to actually call the setter method of the wrapped
+ * property.
+ *
+ * @param value
+ */
+ protected void invokeSetMethod(T value) {
+
+ try {
+ // Construct a temporary argument array only if needed
+ if (setArgs.length == 1) {
+ setMethod.invoke(instance, new Object[] { value });
+ } else {
+
+ // Sets the value to argument array
+ final Object[] args = new Object[setArgs.length];
+ for (int i = 0; i < setArgs.length; i++) {
+ args[i] = (i == setArgumentIndex) ? value : setArgs[i];
+ }
+ setMethod.invoke(instance, args);
+ }
+ } catch (final InvocationTargetException e) {
+ final Throwable targetException = e.getTargetException();
+ throw new MethodException(this, targetException);
+ } catch (final Exception e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * <code>Exception</code> object that signals that there were problems
+ * calling or finding the specified getter or setter methods of the
+ * property.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("rawtypes")
+ // Exceptions cannot be parameterized, ever.
+ public static class MethodException extends RuntimeException {
+
+ /**
+ * The method property from which the exception originates from
+ */
+ private final Property property;
+
+ /**
+ * Cause of the method exception
+ */
+ private Throwable cause;
+
+ /**
+ * Constructs a new <code>MethodException</code> with the specified
+ * detail message.
+ *
+ * @param property
+ * the property.
+ * @param msg
+ * the detail message.
+ */
+ public MethodException(Property property, String msg) {
+ super(msg);
+ this.property = property;
+ }
+
+ /**
+ * Constructs a new <code>MethodException</code> from another exception.
+ *
+ * @param property
+ * the property.
+ * @param cause
+ * the cause of the exception.
+ */
+ public MethodException(Property property, Throwable cause) {
+ this.property = property;
+ this.cause = cause;
+ }
+
+ /**
+ * @see java.lang.Throwable#getCause()
+ */
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+
+ /**
+ * Gets the method property this exception originates from.
+ *
+ * @return MethodProperty or null if not a valid MethodProperty
+ */
+ public MethodProperty getMethodProperty() {
+ return (property instanceof MethodProperty) ? (MethodProperty) property
+ : null;
+ }
+
+ /**
+ * Gets the method property this exception originates from.
+ *
+ * @return Property from which the exception originates
+ */
+ public Property getProperty() {
+ return property;
+ }
+ }
+
+ /**
+ * Sends a value change event to all registered listeners.
+ *
+ * Public for backwards compatibility, visibility may be reduced in future
+ * versions.
+ */
+ @Override
+ public void fireValueChange() {
+ super.fireValueChange();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(MethodProperty.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java
new file mode 100644
index 0000000000..a2a76ec6cf
--- /dev/null
+++ b/server/src/com/vaadin/data/util/MethodPropertyDescriptor.java
@@ -0,0 +1,134 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Property;
+import com.vaadin.util.SerializerHelper;
+
+/**
+ * Property descriptor that is able to create simple {@link MethodProperty}
+ * instances for a bean, using given accessors.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public class MethodPropertyDescriptor<BT> implements
+ VaadinPropertyDescriptor<BT> {
+
+ private final String name;
+ private Class<?> propertyType;
+ private transient Method readMethod;
+ private transient Method writeMethod;
+
+ /**
+ * Creates a property descriptor that can create MethodProperty instances to
+ * access the underlying bean property.
+ *
+ * @param name
+ * of the property
+ * @param propertyType
+ * type (class) of the property
+ * @param readMethod
+ * getter {@link Method} for the property
+ * @param writeMethod
+ * setter {@link Method} for the property or null if read-only
+ * property
+ */
+ public MethodPropertyDescriptor(String name, Class<?> propertyType,
+ Method readMethod, Method writeMethod) {
+ this.name = name;
+ this.propertyType = propertyType;
+ this.readMethod = readMethod;
+ this.writeMethod = writeMethod;
+ }
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ SerializerHelper.writeClass(out, propertyType);
+
+ if (writeMethod != null) {
+ out.writeObject(writeMethod.getName());
+ SerializerHelper.writeClass(out, writeMethod.getDeclaringClass());
+ SerializerHelper.writeClassArray(out,
+ writeMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+
+ if (readMethod != null) {
+ out.writeObject(readMethod.getName());
+ SerializerHelper.writeClass(out, readMethod.getDeclaringClass());
+ SerializerHelper.writeClassArray(out,
+ readMethod.getParameterTypes());
+ } else {
+ out.writeObject(null);
+ out.writeObject(null);
+ out.writeObject(null);
+ }
+ }
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ try {
+ @SuppressWarnings("unchecked")
+ // business assumption; type parameters not checked at runtime
+ Class<BT> class1 = (Class<BT>) SerializerHelper.readClass(in);
+ propertyType = class1;
+
+ String name = (String) in.readObject();
+ Class<?> writeMethodClass = SerializerHelper.readClass(in);
+ Class<?>[] paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ writeMethod = writeMethodClass.getMethod(name, paramTypes);
+ } else {
+ writeMethod = null;
+ }
+
+ name = (String) in.readObject();
+ Class<?> readMethodClass = SerializerHelper.readClass(in);
+ paramTypes = SerializerHelper.readClassArray(in);
+ if (name != null) {
+ readMethod = readMethodClass.getMethod(name, paramTypes);
+ } else {
+ readMethod = null;
+ }
+ } catch (SecurityException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ } catch (NoSuchMethodException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ }
+ };
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class<?> getPropertyType() {
+ return propertyType;
+ }
+
+ @Override
+ public Property<?> createProperty(Object bean) {
+ return new MethodProperty<Object>(propertyType, bean, readMethod,
+ writeMethod);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(MethodPropertyDescriptor.class.getName());
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/NestedMethodProperty.java b/server/src/com/vaadin/data/util/NestedMethodProperty.java
new file mode 100644
index 0000000000..9bff38456d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/NestedMethodProperty.java
@@ -0,0 +1,257 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.MethodProperty.MethodException;
+
+/**
+ * Nested accessor based property for a bean.
+ *
+ * The property is specified in the dotted notation, e.g. "address.street", and
+ * can contain multiple levels of nesting.
+ *
+ * When accessing the property value, all intermediate getters must return
+ * non-null values.
+ *
+ * @see MethodProperty
+ *
+ * @since 6.6
+ */
+public class NestedMethodProperty<T> extends AbstractProperty<T> {
+
+ // needed for de-serialization
+ private String propertyName;
+
+ // chain of getter methods
+ private transient List<Method> getMethods;
+ /**
+ * The setter method.
+ */
+ private transient Method setMethod;
+
+ /**
+ * Bean instance used as a starting point for accessing the property value.
+ */
+ private Object instance;
+
+ private Class<? extends T> type;
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ // getMethods and setMethod are reconstructed on read based on
+ // propertyName
+ }
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+
+ initialize(instance.getClass(), propertyName);
+ }
+
+ /**
+ * Constructs a nested method property for a given object instance. The
+ * property name is a dot separated string pointing to a nested property,
+ * e.g. "manager.address.street".
+ *
+ * @param instance
+ * top-level bean to which the property applies
+ * @param propertyName
+ * dot separated nested property name
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ public NestedMethodProperty(Object instance, String propertyName) {
+ this.instance = instance;
+ initialize(instance.getClass(), propertyName);
+ }
+
+ /**
+ * For internal use to deduce property type etc. without a bean instance.
+ * Calling {@link #setValue(Object)} or {@link #getValue()} on properties
+ * constructed this way is not supported.
+ *
+ * @param instanceClass
+ * class of the top-level bean
+ * @param propertyName
+ */
+ NestedMethodProperty(Class<?> instanceClass, String propertyName) {
+ instance = null;
+ initialize(instanceClass, propertyName);
+ }
+
+ /**
+ * Initializes most of the internal fields based on the top-level bean
+ * instance and property name (dot-separated string).
+ *
+ * @param beanClass
+ * class of the top-level bean to which the property applies
+ * @param propertyName
+ * dot separated nested property name
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ private void initialize(Class<?> beanClass, String propertyName)
+ throws IllegalArgumentException {
+
+ List<Method> getMethods = new ArrayList<Method>();
+
+ String lastSimplePropertyName = propertyName;
+ Class<?> lastClass = beanClass;
+
+ // first top-level property, then go deeper in a loop
+ Class<?> propertyClass = beanClass;
+ String[] simplePropertyNames = propertyName.split("\\.");
+ if (propertyName.endsWith(".") || 0 == simplePropertyNames.length) {
+ throw new IllegalArgumentException("Invalid property name '"
+ + propertyName + "'");
+ }
+ for (int i = 0; i < simplePropertyNames.length; i++) {
+ String simplePropertyName = simplePropertyNames[i].trim();
+ if (simplePropertyName.length() > 0) {
+ lastSimplePropertyName = simplePropertyName;
+ lastClass = propertyClass;
+ try {
+ Method getter = MethodProperty.initGetterMethod(
+ simplePropertyName, propertyClass);
+ propertyClass = getter.getReturnType();
+ getMethods.add(getter);
+ } catch (final java.lang.NoSuchMethodException e) {
+ throw new IllegalArgumentException("Bean property '"
+ + simplePropertyName + "' not found", e);
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "Empty or invalid bean property identifier in '"
+ + propertyName + "'");
+ }
+ }
+
+ // In case the get method is found, resolve the type
+ Method lastGetMethod = getMethods.get(getMethods.size() - 1);
+ Class<?> type = lastGetMethod.getReturnType();
+
+ // Finds the set method
+ Method setMethod = null;
+ try {
+ // Assure that the first letter is upper cased (it is a common
+ // mistake to write firstName, not FirstName).
+ if (Character.isLowerCase(lastSimplePropertyName.charAt(0))) {
+ final char[] buf = lastSimplePropertyName.toCharArray();
+ buf[0] = Character.toUpperCase(buf[0]);
+ lastSimplePropertyName = new String(buf);
+ }
+
+ setMethod = lastClass.getMethod("set" + lastSimplePropertyName,
+ new Class[] { type });
+ } catch (final NoSuchMethodException skipped) {
+ }
+
+ this.type = (Class<? extends T>) MethodProperty
+ .convertPrimitiveType(type);
+ this.propertyName = propertyName;
+ this.getMethods = getMethods;
+ this.setMethod = setMethod;
+ }
+
+ @Override
+ public Class<? extends T> getType() {
+ return type;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || (null == setMethod);
+ }
+
+ /**
+ * Gets the value stored in the Property. The value is resolved by calling
+ * the specified getter method with the argument specified at instantiation.
+ *
+ * @return the value of the Property
+ */
+ @Override
+ public T getValue() {
+ try {
+ Object object = instance;
+ for (Method m : getMethods) {
+ object = m.invoke(object);
+ }
+ return (T) object;
+ } catch (final Throwable e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * Sets the value of the property. The new value must be assignable to the
+ * type of this property.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode.
+ * @see #invokeSetMethod(Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Invalid value type for NestedMethodProperty.");
+ }
+
+ invokeSetMethod((T) newValue);
+ fireValueChange();
+ }
+
+ /**
+ * Internal method to actually call the setter method of the wrapped
+ * property.
+ *
+ * @param value
+ */
+ protected void invokeSetMethod(T value) {
+ try {
+ Object object = instance;
+ for (int i = 0; i < getMethods.size() - 1; i++) {
+ object = getMethods.get(i).invoke(object);
+ }
+ setMethod.invoke(object, new Object[] { value });
+ } catch (final InvocationTargetException e) {
+ throw new MethodException(this, e.getTargetException());
+ } catch (final Exception e) {
+ throw new MethodException(this, e);
+ }
+ }
+
+ /**
+ * Returns an unmodifiable list of getter methods to call in sequence to get
+ * the property value.
+ *
+ * This API may change in future versions.
+ *
+ * @return unmodifiable list of getter methods corresponding to each segment
+ * of the property name
+ */
+ protected List<Method> getGetMethods() {
+ return Collections.unmodifiableList(getMethods);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java
new file mode 100644
index 0000000000..b67b425d1d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/NestedPropertyDescriptor.java
@@ -0,0 +1,60 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that is able to create nested property instances for a
+ * bean.
+ *
+ * The property is specified in the dotted notation, e.g. "address.street", and
+ * can contain multiple levels of nesting.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public class NestedPropertyDescriptor<BT> implements
+ VaadinPropertyDescriptor<BT> {
+
+ private final String name;
+ private final Class<?> propertyType;
+
+ /**
+ * Creates a property descriptor that can create MethodProperty instances to
+ * access the underlying bean property.
+ *
+ * @param name
+ * of the property in a dotted path format, e.g. "address.street"
+ * @param beanType
+ * type (class) of the top-level bean
+ * @throws IllegalArgumentException
+ * if the property name is invalid
+ */
+ public NestedPropertyDescriptor(String name, Class<BT> beanType)
+ throws IllegalArgumentException {
+ this.name = name;
+ NestedMethodProperty<?> property = new NestedMethodProperty<Object>(
+ beanType, name);
+ this.propertyType = property.getType();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public Class<?> getPropertyType() {
+ return propertyType;
+ }
+
+ @Override
+ public Property<?> createProperty(BT bean) {
+ return new NestedMethodProperty<Object>(bean, name);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/ObjectProperty.java b/server/src/com/vaadin/data/util/ObjectProperty.java
new file mode 100644
index 0000000000..cb85b44c2a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/ObjectProperty.java
@@ -0,0 +1,141 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+
+/**
+ * A simple data object containing one typed value. This class is a
+ * straightforward implementation of the the {@link com.vaadin.data.Property}
+ * interface.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ObjectProperty<T> extends AbstractProperty<T> {
+
+ /**
+ * The value contained by the Property.
+ */
+ private T value;
+
+ /**
+ * Data type of the Property's value.
+ */
+ private final Class<T> type;
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value. The type
+ * of the property is automatically initialized to be the type of the given
+ * value.
+ *
+ * @param value
+ * the Initial value of the Property.
+ */
+ @SuppressWarnings("unchecked")
+ // the cast is safe, because an object of type T has class Class<T>
+ public ObjectProperty(T value) {
+ this(value, (Class<T>) value.getClass());
+ }
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value and type.
+ *
+ * Since Vaadin 7, only values of the correct type are accepted, and no
+ * automatic conversions are performed.
+ *
+ * @param value
+ * the Initial value of the Property.
+ * @param type
+ * the type of the value. The value must be assignable to given
+ * type.
+ */
+ public ObjectProperty(T value, Class<T> type) {
+
+ // Set the values
+ this.type = type;
+ setValue(value);
+ }
+
+ /**
+ * Creates a new instance of ObjectProperty with the given value, type and
+ * read-only mode status.
+ *
+ * Since Vaadin 7, only the correct type of values is accepted, see
+ * {@link #ObjectProperty(Object, Class)}.
+ *
+ * @param value
+ * the Initial value of the property.
+ * @param type
+ * the type of the value. <code>value</code> must be assignable
+ * to this type.
+ * @param readOnly
+ * Sets the read-only mode.
+ */
+ public ObjectProperty(T value, Class<T> type, boolean readOnly) {
+ this(value, type);
+ setReadOnly(readOnly);
+ }
+
+ /**
+ * Returns the type of the ObjectProperty. The methods <code>getValue</code>
+ * and <code>setValue</code> must be compatible with this type: one must be
+ * able to safely cast the value returned from <code>getValue</code> to the
+ * given type and pass any variable assignable to this type as an argument
+ * to <code>setValue</code>.
+ *
+ * @return type of the Property
+ */
+ @Override
+ public final Class<T> getType() {
+ return type;
+ }
+
+ /**
+ * Gets the value stored in the Property.
+ *
+ * @return the value stored in the Property
+ */
+ @Override
+ public T getValue() {
+ return value;
+ }
+
+ /**
+ * Sets the value of the property.
+ *
+ * Note that since Vaadin 7, no conversions are performed and the value must
+ * be of the correct type.
+ *
+ * @param newValue
+ * the New value of the property.
+ * @throws <code>Property.ReadOnlyException</code> if the object is in
+ * read-only mode
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+
+ // Checks the mode
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Checks the type of the value
+ if (newValue != null && !type.isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException("Invalid value type "
+ + newValue.getClass().getName()
+ + " for ObjectProperty of type " + type.getName() + ".");
+ }
+
+ // the cast is safe after an isAssignableFrom check
+ this.value = (T) newValue;
+
+ fireValueChange();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/PropertyFormatter.java b/server/src/com/vaadin/data/util/PropertyFormatter.java
new file mode 100644
index 0000000000..3d65726309
--- /dev/null
+++ b/server/src/com/vaadin/data/util/PropertyFormatter.java
@@ -0,0 +1,245 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.converter.Converter;
+
+/**
+ * Formatting proxy for a {@link Property}.
+ *
+ * <p>
+ * This class can be used to implement formatting for any type of Property
+ * datasources. The idea is to connect this as proxy between UI component and
+ * the original datasource.
+ * </p>
+ *
+ * <p>
+ * For example <code>
+ * <pre>textfield.setPropertyDataSource(new PropertyFormatter(property) {
+ public String format(Object value) {
+ return ((Double) value).toString() + "000000000";
+ }
+
+ public Object parse(String formattedValue) throws Exception {
+ return Double.parseDouble(formattedValue);
+ }
+
+ });</pre></code> adds formatter for Double-typed property that extends
+ * standard "1.0" notation with more zeroes.
+ * </p>
+ *
+ * @param T
+ * type of the underlying property (a PropertyFormatter is always a
+ * Property&lt;String&gt;)
+ *
+ * @deprecated Since 7.0 replaced by {@link Converter}
+ * @author Vaadin Ltd.
+ * @since 5.3.0
+ */
+@SuppressWarnings("serial")
+@Deprecated
+public abstract class PropertyFormatter<T> extends AbstractProperty<String>
+ implements Property.Viewer, Property.ValueChangeListener,
+ Property.ReadOnlyStatusChangeListener {
+
+ /** Datasource that stores the actual value. */
+ Property<T> dataSource;
+
+ /**
+ * Construct a new {@code PropertyFormatter} that is not connected to any
+ * data source. Call {@link #setPropertyDataSource(Property)} later on to
+ * attach it to a property.
+ *
+ */
+ protected PropertyFormatter() {
+ }
+
+ /**
+ * Construct a new formatter that is connected to given data source. Calls
+ * {@link #format(Object)} which can be a problem if the formatter has not
+ * yet been initialized.
+ *
+ * @param propertyDataSource
+ * to connect this property to.
+ */
+ public PropertyFormatter(Property<T> propertyDataSource) {
+
+ setPropertyDataSource(propertyDataSource);
+ }
+
+ /**
+ * Gets the current data source of the formatter, if any.
+ *
+ * @return the current data source as a Property, or <code>null</code> if
+ * none defined.
+ */
+ @Override
+ public Property<T> getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the specified Property as the data source for the formatter.
+ *
+ *
+ * <p>
+ * Remember that new data sources getValue() must return objects that are
+ * compatible with parse() and format() methods.
+ * </p>
+ *
+ * @param newDataSource
+ * the new data source Property.
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+
+ boolean readOnly = false;
+ String prevValue = null;
+
+ if (dataSource != null) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeListener) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ readOnly = isReadOnly();
+ prevValue = getValue();
+ }
+
+ dataSource = newDataSource;
+
+ if (dataSource != null) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeListener) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .addListener(this);
+ }
+ }
+
+ if (isReadOnly() != readOnly) {
+ fireReadOnlyStatusChange();
+ }
+ String newVal = getValue();
+ if ((prevValue == null && newVal != null)
+ || (prevValue != null && !prevValue.equals(newVal))) {
+ fireValueChange();
+ }
+ }
+
+ /* Documented in the interface */
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Get the formatted value.
+ *
+ * @return If the datasource returns null, this is null. Otherwise this is
+ * String given by format().
+ */
+ @Override
+ public String getValue() {
+ T value = dataSource == null ? null : dataSource.getValue();
+ if (value == null) {
+ return null;
+ }
+ return format(value);
+ }
+
+ /** Reflects the read-only status of the datasource. */
+ @Override
+ public boolean isReadOnly() {
+ return dataSource == null ? false : dataSource.isReadOnly();
+ }
+
+ /**
+ * This method must be implemented to format the values received from
+ * DataSource.
+ *
+ * @param value
+ * Value object got from the datasource. This is guaranteed to be
+ * non-null and of the type compatible with getType() of the
+ * datasource.
+ * @return
+ */
+ abstract public String format(T value);
+
+ /**
+ * Parse string and convert it to format compatible with datasource.
+ *
+ * The method is required to assure that parse(format(x)) equals x.
+ *
+ * @param formattedValue
+ * This is guaranteed to be non-null string.
+ * @return Non-null value compatible with datasource.
+ * @throws Exception
+ * Any type of exception can be thrown to indicate that the
+ * conversion was not succesful.
+ */
+ abstract public T parse(String formattedValue) throws Exception;
+
+ /**
+ * Sets the Property's read-only mode to the specified status.
+ *
+ * @param newStatus
+ * the new read-only status of the Property.
+ */
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (dataSource != null) {
+ dataSource.setReadOnly(newStatus);
+ }
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (dataSource == null) {
+ return;
+ }
+ if (newValue == null) {
+ if (dataSource.getValue() != null) {
+ dataSource.setValue(null);
+ fireValueChange();
+ }
+ } else {
+ try {
+ dataSource.setValue(parse(newValue.toString()));
+ if (!newValue.equals(getValue())) {
+ fireValueChange();
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Could not parse value", e);
+ }
+ }
+ }
+
+ /**
+ * Listens for changes in the datasource.
+ *
+ * This should not be called directly.
+ */
+ @Override
+ public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
+ fireValueChange();
+ }
+
+ /**
+ * Listens for changes in the datasource.
+ *
+ * This should not be called directly.
+ */
+ @Override
+ public void readOnlyStatusChange(
+ com.vaadin.data.Property.ReadOnlyStatusChangeEvent event) {
+ fireReadOnlyStatusChange();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/PropertysetItem.java b/server/src/com/vaadin/data/util/PropertysetItem.java
new file mode 100644
index 0000000000..22f2da75b2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/PropertysetItem.java
@@ -0,0 +1,340 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Class for handling a set of identified Properties. The elements contained in
+ * a </code>MapItem</code> can be referenced using locally unique identifiers.
+ * The class supports listeners who are interested in changes to the Property
+ * set managed by the class.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class PropertysetItem implements Item, Item.PropertySetChangeNotifier,
+ Cloneable {
+
+ /* Private representation of the item */
+
+ /**
+ * Mapping from property id to property.
+ */
+ private HashMap<Object, Property<?>> map = new HashMap<Object, Property<?>>();
+
+ /**
+ * List of all property ids to maintain the order.
+ */
+ private LinkedList<Object> list = new LinkedList<Object>();
+
+ /**
+ * List of property set modification listeners.
+ */
+ private LinkedList<Item.PropertySetChangeListener> propertySetChangeListeners = null;
+
+ /* Item methods */
+
+ /**
+ * Gets the Property corresponding to the given Property ID stored in the
+ * Item. If the Item does not contain the Property, <code>null</code> is
+ * returned.
+ *
+ * @param id
+ * the identifier of the Property to get.
+ * @return the Property with the given ID or <code>null</code>
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ return map.get(id);
+ }
+
+ /**
+ * Gets the collection of IDs of all Properties stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties stored
+ * the Item
+ */
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableCollection(list);
+ }
+
+ /* Item.Managed methods */
+
+ /**
+ * Removes the Property identified by ID from the Item. This functionality
+ * is optional. If the method is not implemented, the method always returns
+ * <code>false</code>.
+ *
+ * @param id
+ * the ID of the Property to be removed.
+ * @return <code>true</code> if the operation succeeded <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean removeItemProperty(Object id) {
+
+ // Cant remove missing properties
+ if (map.remove(id) == null) {
+ return false;
+ }
+ list.remove(id);
+
+ // Send change events
+ fireItemPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Tries to add a new Property into the Item.
+ *
+ * @param id
+ * the ID of the new Property.
+ * @param property
+ * the Property to be added and associated with the id.
+ * @return <code>true</code> if the operation succeeded, <code>false</code>
+ * if not
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property) {
+
+ // Null ids are not accepted
+ if (id == null) {
+ throw new NullPointerException("Item property id can not be null");
+ }
+
+ // Cant add a property twice
+ if (map.containsKey(id)) {
+ return false;
+ }
+
+ // Put the property to map
+ map.put(id, property);
+ list.add(id);
+
+ // Send event
+ fireItemPropertySetChange();
+
+ return true;
+ }
+
+ /**
+ * Gets the <code>String</code> representation of the contents of the Item.
+ * The format of the string is a space separated catenation of the
+ * <code>String</code> representations of the Properties contained by the
+ * Item.
+ *
+ * @return <code>String</code> representation of the Item contents
+ */
+ @Override
+ public String toString() {
+ String retValue = "";
+
+ for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) {
+ final Object propertyId = i.next();
+ retValue += getItemProperty(propertyId).getValue();
+ if (i.hasNext()) {
+ retValue += " ";
+ }
+ }
+
+ return retValue;
+ }
+
+ /* Notifiers */
+
+ /**
+ * An <code>event</code> object specifying an Item whose Property set has
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ private static class PropertySetChangeEvent extends EventObject implements
+ Item.PropertySetChangeEvent {
+
+ private PropertySetChangeEvent(Item source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Item whose Property set has changed.
+ *
+ * @return source object of the event as an <code>Item</code>
+ */
+ @Override
+ public Item getItem() {
+ return (Item) getSource();
+ }
+ }
+
+ /**
+ * Registers a new property set change listener for this Item.
+ *
+ * @param listener
+ * the new Listener to be registered.
+ */
+ @Override
+ public void addListener(Item.PropertySetChangeListener listener) {
+ if (propertySetChangeListeners == null) {
+ propertySetChangeListeners = new LinkedList<PropertySetChangeListener>();
+ }
+ propertySetChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously registered property set change listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ @Override
+ public void removeListener(Item.PropertySetChangeListener listener) {
+ if (propertySetChangeListeners != null) {
+ propertySetChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Sends a Property set change event to all interested listeners.
+ */
+ private void fireItemPropertySetChange() {
+ if (propertySetChangeListeners != null) {
+ final Object[] l = propertySetChangeListeners.toArray();
+ final Item.PropertySetChangeEvent event = new PropertysetItem.PropertySetChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Item.PropertySetChangeListener) l[i])
+ .itemPropertySetChange(event);
+ }
+ }
+ }
+
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Item.PropertySetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (propertySetChangeListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetChangeListeners);
+ }
+ }
+
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * Creates and returns a copy of this object.
+ * <p>
+ * The method <code>clone</code> performs a shallow copy of the
+ * <code>PropertysetItem</code>.
+ * </p>
+ * <p>
+ * Note : All arrays are considered to implement the interface Cloneable.
+ * Otherwise, this method creates a new instance of the class of this object
+ * and initializes all its fields with exactly the contents of the
+ * corresponding fields of this object, as if by assignment, the contents of
+ * the fields are not themselves cloned. Thus, this method performs a
+ * "shallow copy" of this object, not a "deep copy" operation.
+ * </p>
+ *
+ * @throws CloneNotSupportedException
+ * if the object's class does not support the Cloneable
+ * interface.
+ *
+ * @see java.lang.Object#clone()
+ */
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+
+ final PropertysetItem npsi = new PropertysetItem();
+
+ npsi.list = list != null ? (LinkedList<Object>) list.clone() : null;
+ npsi.propertySetChangeListeners = propertySetChangeListeners != null ? (LinkedList<PropertySetChangeListener>) propertySetChangeListeners
+ .clone() : null;
+ npsi.map = (HashMap<Object, Property<?>>) map.clone();
+
+ return npsi;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+
+ if (obj == null || !(obj instanceof PropertysetItem)) {
+ return false;
+ }
+
+ final PropertysetItem other = (PropertysetItem) obj;
+
+ if (other.list != list) {
+ if (other.list == null) {
+ return false;
+ }
+ if (!other.list.equals(list)) {
+ return false;
+ }
+ }
+ if (other.map != map) {
+ if (other.map == null) {
+ return false;
+ }
+ if (!other.map.equals(map)) {
+ return false;
+ }
+ }
+ if (other.propertySetChangeListeners != propertySetChangeListeners) {
+ boolean thisEmpty = (propertySetChangeListeners == null || propertySetChangeListeners
+ .isEmpty());
+ boolean otherEmpty = (other.propertySetChangeListeners == null || other.propertySetChangeListeners
+ .isEmpty());
+ if (thisEmpty && otherEmpty) {
+ return true;
+ }
+ if (otherEmpty) {
+ return false;
+ }
+ if (!other.propertySetChangeListeners
+ .equals(propertySetChangeListeners)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+
+ return (list == null ? 0 : list.hashCode())
+ ^ (map == null ? 0 : map.hashCode())
+ ^ ((propertySetChangeListeners == null || propertySetChangeListeners
+ .isEmpty()) ? 0 : propertySetChangeListeners.hashCode());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/QueryContainer.java b/server/src/com/vaadin/data/util/QueryContainer.java
new file mode 100644
index 0000000000..dc7c883a7e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/QueryContainer.java
@@ -0,0 +1,675 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * The <code>QueryContainer</code> is the specialized form of Container which is
+ * Ordered and Indexed. This is used to represent the contents of relational
+ * database tables accessed through the JDBC Connection in the Vaadin Table.
+ * This creates Items based on the queryStatement provided to the container.
+ * </p>
+ *
+ * <p>
+ * The <code>QueryContainer</code> can be visualized as a representation of a
+ * relational database table.Each Item in the container represents the row
+ * fetched by the query.All cells in a column have same data type and the data
+ * type information is retrieved from the metadata of the resultset.
+ * </p>
+ *
+ * <p>
+ * Note : If data in the tables gets modified, Container will not get reflected
+ * with the updates, we have to explicity invoke QueryContainer.refresh method.
+ * {@link com.vaadin.data.util.QueryContainer#refresh() refresh()}
+ * </p>
+ *
+ * @see com.vaadin.data.Container
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @since 4.0
+ *
+ * @deprecated will be removed in the future, use the SQLContainer add-on
+ */
+
+@Deprecated
+@SuppressWarnings("serial")
+public class QueryContainer implements Container, Container.Ordered,
+ Container.Indexed {
+
+ // default ResultSet type
+ public static final int DEFAULT_RESULTSET_TYPE = ResultSet.TYPE_SCROLL_INSENSITIVE;
+
+ // default ResultSet concurrency
+ public static final int DEFAULT_RESULTSET_CONCURRENCY = ResultSet.CONCUR_READ_ONLY;
+
+ private int resultSetType = DEFAULT_RESULTSET_TYPE;
+
+ private int resultSetConcurrency = DEFAULT_RESULTSET_CONCURRENCY;
+
+ private final String queryStatement;
+
+ private final Connection connection;
+
+ private ResultSet result;
+
+ private Collection<String> propertyIds;
+
+ private final HashMap<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
+
+ private int size = -1;
+
+ private Statement statement;
+
+ /**
+ * Constructs new <code>QueryContainer</code> with the specified
+ * <code>queryStatement</code>.
+ *
+ * @param queryStatement
+ * Database query
+ * @param connection
+ * Connection object
+ * @param resultSetType
+ * @param resultSetConcurrency
+ * @throws SQLException
+ * when database operation fails
+ */
+ public QueryContainer(String queryStatement, Connection connection,
+ int resultSetType, int resultSetConcurrency) throws SQLException {
+ this.queryStatement = queryStatement;
+ this.connection = connection;
+ this.resultSetType = resultSetType;
+ this.resultSetConcurrency = resultSetConcurrency;
+ init();
+ }
+
+ /**
+ * Constructs new <code>QueryContainer</code> with the specified
+ * queryStatement using the default resultset type and default resultset
+ * concurrency.
+ *
+ * @param queryStatement
+ * Database query
+ * @param connection
+ * Connection object
+ * @see QueryContainer#DEFAULT_RESULTSET_TYPE
+ * @see QueryContainer#DEFAULT_RESULTSET_CONCURRENCY
+ * @throws SQLException
+ * when database operation fails
+ */
+ public QueryContainer(String queryStatement, Connection connection)
+ throws SQLException {
+ this(queryStatement, connection, DEFAULT_RESULTSET_TYPE,
+ DEFAULT_RESULTSET_CONCURRENCY);
+ }
+
+ /**
+ * Fills the Container with the items and properties. Invoked by the
+ * constructor.
+ *
+ * @throws SQLException
+ * when parameter initialization fails.
+ * @see QueryContainer#QueryContainer(String, Connection, int, int).
+ */
+ private void init() throws SQLException {
+ refresh();
+ ResultSetMetaData metadata;
+ metadata = result.getMetaData();
+ final int count = metadata.getColumnCount();
+ final ArrayList<String> list = new ArrayList<String>(count);
+ for (int i = 1; i <= count; i++) {
+ final String columnName = metadata.getColumnName(i);
+ list.add(columnName);
+ final Property<?> p = getContainerProperty(new Integer(1),
+ columnName);
+ propertyTypes.put(columnName,
+ p == null ? Object.class : p.getType());
+ }
+ propertyIds = Collections.unmodifiableCollection(list);
+ }
+
+ /**
+ * <p>
+ * Restores items in the container. This method will update the latest data
+ * to the container.
+ * </p>
+ * Note: This method should be used to update the container with the latest
+ * items.
+ *
+ * @throws SQLException
+ * when database operation fails
+ *
+ */
+
+ public void refresh() throws SQLException {
+ close();
+ statement = connection.createStatement(resultSetType,
+ resultSetConcurrency);
+ result = statement.executeQuery(queryStatement);
+ result.last();
+ size = result.getRow();
+ }
+
+ /**
+ * Releases and nullifies the <code>statement</code>.
+ *
+ * @throws SQLException
+ * when database operation fails
+ */
+
+ public void close() throws SQLException {
+ if (statement != null) {
+ statement.close();
+ }
+ statement = null;
+ }
+
+ /**
+ * Gets the Item with the given Item ID from the Container.
+ *
+ * @param id
+ * ID of the Item to retrieve
+ * @return Item Id.
+ */
+
+ @Override
+ public Item getItem(Object id) {
+ return new Row(id);
+ }
+
+ /**
+ * Gets the collection of propertyId from the Container.
+ *
+ * @return Collection of Property ID.
+ */
+
+ @Override
+ public Collection<String> getContainerPropertyIds() {
+ return propertyIds;
+ }
+
+ /**
+ * Gets an collection of all the item IDs in the container.
+ *
+ * @return collection of Item IDs
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ final Collection<Integer> c = new ArrayList<Integer>(size);
+ for (int i = 1; i <= size; i++) {
+ c.add(new Integer(i));
+ }
+ return c;
+ }
+
+ /**
+ * Gets the property identified by the given itemId and propertyId from the
+ * container. If the container does not contain the property
+ * <code>null</code> is returned.
+ *
+ * @param itemId
+ * ID of the Item which contains the Property
+ * @param propertyId
+ * ID of the Property to retrieve
+ *
+ * @return Property with the given ID if exists; <code>null</code>
+ * otherwise.
+ */
+
+ @Override
+ public synchronized Property<?> getContainerProperty(Object itemId,
+ Object propertyId) {
+ if (!(itemId instanceof Integer && propertyId instanceof String)) {
+ return null;
+ }
+ Object value;
+ try {
+ result.absolute(((Integer) itemId).intValue());
+ value = result.getObject((String) propertyId);
+ } catch (final Exception e) {
+ return null;
+ }
+
+ // Handle also null values from the database
+ return new ObjectProperty<Object>(value != null ? value
+ : new String(""));
+ }
+
+ /**
+ * Gets the data type of all properties identified by the given type ID.
+ *
+ * @param id
+ * ID identifying the Properties
+ *
+ * @return data type of the Properties
+ */
+
+ @Override
+ public Class<?> getType(Object id) {
+ return propertyTypes.get(id);
+ }
+
+ /**
+ * Gets the number of items in the container.
+ *
+ * @return the number of items in the container.
+ */
+ @Override
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Tests if the list contains the specified Item.
+ *
+ * @param id
+ * ID the of Item to be tested.
+ * @return <code>true</code> if given id is in the container;
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean containsId(Object id) {
+ if (!(id instanceof Integer)) {
+ return false;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i < 1) {
+ return false;
+ }
+ if (i > size) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Creates new Item with the given ID into the Container.
+ *
+ * @param itemId
+ * ID of the Item to be created.
+ *
+ * @return Created new Item, or <code>null</code> if it fails.
+ *
+ * @throws UnsupportedOperationException
+ * if the addItem method is not supported.
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Creates a new Item into the Container, and assign it an ID.
+ *
+ * @return ID of the newly created Item, or <code>null</code> if it fails.
+ * @throws UnsupportedOperationException
+ * if the addItem method is not supported.
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes the Item identified by ItemId from the Container.
+ *
+ * @param itemId
+ * ID of the Item to remove.
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeItem method is not supported.
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new Property to all Items in the Container.
+ *
+ * @param propertyId
+ * ID of the Property
+ * @param type
+ * Data type of the new Property
+ * @param defaultValue
+ * The value all created Properties are initialized to.
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the addContainerProperty method is not supported.
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ *
+ * @param propertyId
+ * ID of the Property to remove
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeContainerProperty method is not supported.
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * @return <code>true</code> if the operation succeeded; <code>false</code>
+ * otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeAllItems method is not supported.
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @param previousItemId
+ * Id of the previous item in ordered container.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return Returns new item or <code>null</code> if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAfter method is not supported.
+ */
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @param previousItemId
+ * Id of the previous item in ordered container.
+ * @return Returns item id created new item or <code>null</code> if the
+ * operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAfter method is not supported.
+ */
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns id of first item in the Container.
+ *
+ * @return ID of the first Item in the list.
+ */
+ @Override
+ public Object firstItemId() {
+ if (size < 1) {
+ return null;
+ }
+ return new Integer(1);
+ }
+
+ /**
+ * Returns <code>true</code> if given id is first id at first index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ */
+ @Override
+ public boolean isFirstId(Object id) {
+ return size > 0 && (id instanceof Integer)
+ && ((Integer) id).intValue() == 1;
+ }
+
+ /**
+ * Returns <code>true</code> if given id is last id at last index.
+ *
+ * @param id
+ * ID of an Item in the Container
+ *
+ */
+ @Override
+ public boolean isLastId(Object id) {
+ return size > 0 && (id instanceof Integer)
+ && ((Integer) id).intValue() == size;
+ }
+
+ /**
+ * Returns id of last item in the Container.
+ *
+ * @return ID of the last Item.
+ */
+ @Override
+ public Object lastItemId() {
+ if (size < 1) {
+ return null;
+ }
+ return new Integer(size);
+ }
+
+ /**
+ * Returns id of next item in container at next index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ * @return ID of the next Item or null.
+ */
+ @Override
+ public Object nextItemId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return null;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i >= size) {
+ return null;
+ }
+ return new Integer(i + 1);
+ }
+
+ /**
+ * Returns id of previous item in container at previous index.
+ *
+ * @param id
+ * ID of an Item in the Container.
+ * @return ID of the previous Item or null.
+ */
+ @Override
+ public Object prevItemId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return null;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i <= 1) {
+ return null;
+ }
+ return new Integer(i - 1);
+ }
+
+ /**
+ * The <code>Row</code> class implements methods of Item.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @since 4.0
+ */
+ class Row implements Item {
+
+ Object id;
+
+ private Row(Object rowId) {
+ id = rowId;
+ }
+
+ /**
+ * Adds the item property.
+ *
+ * @param id
+ * ID of the new Property.
+ * @param property
+ * Property to be added and associated with ID.
+ * @return <code>true</code> if the operation succeeded;
+ * <code>false</code> otherwise.
+ * @throws UnsupportedOperationException
+ * if the addItemProperty method is not supported.
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the property corresponding to the given property ID stored in
+ * the Item.
+ *
+ * @param propertyId
+ * identifier of the Property to get
+ * @return the Property with the given ID or <code>null</code>
+ */
+ @Override
+ public Property<?> getItemProperty(Object propertyId) {
+ return getContainerProperty(id, propertyId);
+ }
+
+ /**
+ * Gets the collection of property IDs stored in the Item.
+ *
+ * @return unmodifiable collection containing IDs of the Properties
+ * stored the Item.
+ */
+ @Override
+ public Collection<String> getItemPropertyIds() {
+ return propertyIds;
+ }
+
+ /**
+ * Removes given item property.
+ *
+ * @param id
+ * ID of the Property to be removed.
+ * @return <code>true</code> if the item property is removed;
+ * <code>false</code> otherwise.
+ * @throws UnsupportedOperationException
+ * if the removeItemProperty is not supported.
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * Closes the statement.
+ *
+ * @see #close()
+ */
+ @Override
+ public void finalize() {
+ try {
+ close();
+ } catch (final SQLException ignored) {
+
+ }
+ }
+
+ /**
+ * Adds the given item at the position of given index.
+ *
+ * @param index
+ * Index to add the new item.
+ * @param newItemId
+ * Id of the new item to be added.
+ * @return new item or <code>null</code> if the operation fails.
+ * @throws UnsupportedOperationException
+ * if the addItemAt is not supported.
+ */
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds item at the position of provided index in the container.
+ *
+ * @param index
+ * Index to add the new item.
+ * @return item id created new item or <code>null</code> if the operation
+ * fails.
+ *
+ * @throws UnsupportedOperationException
+ * if the addItemAt is not supported.
+ */
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Gets the Index id in the container.
+ *
+ * @param index
+ * Index Id.
+ * @return ID in the given index.
+ */
+ @Override
+ public Object getIdByIndex(int index) {
+ if (size < 1 || index < 0 || index >= size) {
+ return null;
+ }
+ return new Integer(index + 1);
+ }
+
+ /**
+ * Gets the index of the Item corresponding to id in the container.
+ *
+ * @param id
+ * ID of an Item in the Container
+ * @return index of the Item, or -1 if the Container does not include the
+ * Item
+ */
+
+ @Override
+ public int indexOfId(Object id) {
+ if (size < 1 || !(id instanceof Integer)) {
+ return -1;
+ }
+ final int i = ((Integer) id).intValue();
+ if (i >= size || i < 1) {
+ return -1;
+ }
+ return i - 1;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/TextFileProperty.java b/server/src/com/vaadin/data/util/TextFileProperty.java
new file mode 100644
index 0000000000..598b721a9c
--- /dev/null
+++ b/server/src/com/vaadin/data/util/TextFileProperty.java
@@ -0,0 +1,144 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Property implementation for wrapping a text file.
+ *
+ * Supports reading and writing of a File from/to String.
+ *
+ * {@link ValueChangeListener}s are supported, but only fire when
+ * setValue(Object) is explicitly called. {@link ReadOnlyStatusChangeListener}s
+ * are supported but only fire when setReadOnly(boolean) is explicitly called.
+ *
+ */
+@SuppressWarnings("serial")
+public class TextFileProperty extends AbstractProperty<String> {
+
+ private File file;
+ private Charset charset = null;
+
+ /**
+ * Wrap given file with property interface.
+ *
+ * Setting the file to null works, but getValue() will return null.
+ *
+ * @param file
+ * File to be wrapped.
+ */
+ public TextFileProperty(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Wrap the given file with the property interface and specify character
+ * set.
+ *
+ * Setting the file to null works, but getValue() will return null.
+ *
+ * @param file
+ * File to be wrapped.
+ * @param charset
+ * Charset to be used for reading and writing the file.
+ */
+ public TextFileProperty(File file, Charset charset) {
+ this.file = file;
+ this.charset = charset;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getType()
+ */
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#getValue()
+ */
+ @Override
+ public String getValue() {
+ if (file == null) {
+ return null;
+ }
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ InputStreamReader isr = charset == null ? new InputStreamReader(fis)
+ : new InputStreamReader(fis, charset);
+ BufferedReader r = new BufferedReader(isr);
+ StringBuilder b = new StringBuilder();
+ char buf[] = new char[8 * 1024];
+ int len;
+ while ((len = r.read(buf)) != -1) {
+ b.append(buf, 0, len);
+ }
+ r.close();
+ isr.close();
+ fis.close();
+ return b.toString();
+ } catch (FileNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#isReadOnly()
+ */
+ @Override
+ public boolean isReadOnly() {
+ return file == null || super.isReadOnly() || !file.canWrite();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Property#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (isReadOnly()) {
+ throw new ReadOnlyException();
+ }
+ if (file == null) {
+ return;
+ }
+
+ try {
+ FileOutputStream fos = new FileOutputStream(file);
+ OutputStreamWriter osw = charset == null ? new OutputStreamWriter(
+ fos) : new OutputStreamWriter(fos, charset);
+ BufferedWriter w = new BufferedWriter(osw);
+ w.append(newValue.toString());
+ w.flush();
+ w.close();
+ osw.close();
+ fos.close();
+ fireValueChange();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java
new file mode 100644
index 0000000000..d042bfaac2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java
@@ -0,0 +1,114 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeNotifier;
+
+/**
+ * Wrapper class that helps implement two-phase commit for a non-transactional
+ * property.
+ *
+ * When accessing the property through the wrapper, getting and setting the
+ * property value take place immediately. However, the wrapper keeps track of
+ * the old value of the property so that it can be set for the property in case
+ * of a roll-back. This can result in the underlying property value changing
+ * multiple times (first based on modifications made by the application, then
+ * back upon roll-back).
+ *
+ * Value change events on the {@link TransactionalPropertyWrapper} are only
+ * fired at the end of a successful transaction, whereas listeners attached to
+ * the underlying property may receive multiple value change events.
+ *
+ * @see com.vaadin.data.Property.Transactional
+ *
+ * @author Vaadin Ltd
+ * @version @version@
+ * @since 7.0
+ *
+ * @param <T>
+ */
+public class TransactionalPropertyWrapper<T> extends AbstractProperty<T>
+ implements ValueChangeNotifier, Property.Transactional<T> {
+
+ private Property<T> wrappedProperty;
+ private boolean inTransaction = false;
+ private boolean valueChangePending;
+ private T valueBeforeTransaction;
+
+ public TransactionalPropertyWrapper(Property<T> wrappedProperty) {
+ this.wrappedProperty = wrappedProperty;
+ if (wrappedProperty instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) wrappedProperty)
+ .addListener(new ValueChangeListener() {
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ fireValueChange();
+ }
+ });
+ }
+ }
+
+ @Override
+ public Class getType() {
+ return wrappedProperty.getType();
+ }
+
+ @Override
+ public T getValue() {
+ return wrappedProperty.getValue();
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ // Causes a value change to be sent to this listener which in turn fires
+ // a new value change event for this property
+ wrappedProperty.setValue(newValue);
+ }
+
+ @Override
+ public void startTransaction() {
+ inTransaction = true;
+ valueBeforeTransaction = getValue();
+ }
+
+ @Override
+ public void commit() {
+ endTransaction();
+ }
+
+ @Override
+ public void rollback() {
+ try {
+ wrappedProperty.setValue(valueBeforeTransaction);
+ } finally {
+ valueChangePending = false;
+ endTransaction();
+ }
+ }
+
+ protected void endTransaction() {
+ inTransaction = false;
+ valueBeforeTransaction = null;
+ if (valueChangePending) {
+ fireValueChange();
+ }
+ }
+
+ @Override
+ protected void fireValueChange() {
+ if (inTransaction) {
+ valueChangePending = true;
+ } else {
+ super.fireValueChange();
+ }
+ }
+
+ public Property<T> getWrappedProperty() {
+ return wrappedProperty;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java
new file mode 100644
index 0000000000..ee1e525540
--- /dev/null
+++ b/server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java
@@ -0,0 +1,43 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Property;
+
+/**
+ * Property descriptor that can create a property instance for a bean.
+ *
+ * Used by {@link BeanItem} and {@link AbstractBeanContainer} to keep track of
+ * the set of properties of items.
+ *
+ * @param <BT>
+ * bean type
+ *
+ * @since 6.6
+ */
+public interface VaadinPropertyDescriptor<BT> extends Serializable {
+ /**
+ * Returns the name of the property.
+ *
+ * @return
+ */
+ public String getName();
+
+ /**
+ * Returns the type of the property.
+ *
+ * @return Class<?>
+ */
+ public Class<?> getPropertyType();
+
+ /**
+ * Creates a new {@link Property} instance for this property for a bean.
+ *
+ * @param bean
+ * @return
+ */
+ public Property<?> createProperty(BT bean);
+}
diff --git a/server/src/com/vaadin/data/util/converter/Converter.java b/server/src/com/vaadin/data/util/converter/Converter.java
new file mode 100644
index 0000000000..b8c15e8cdc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/Converter.java
@@ -0,0 +1,159 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+/**
+ * Interface that implements conversion between a model and a presentation type.
+ * <p>
+ * Typically {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should be symmetric so that chaining
+ * these together returns the original result for all input but this is not a
+ * requirement.
+ * </p>
+ * <p>
+ * Converters must not have any side effects (never update UI from inside a
+ * converter).
+ * </p>
+ * <p>
+ * All Converters must be stateless and thread safe.
+ * </p>
+ * <p>
+ * If conversion of a value fails, a {@link ConversionException} is thrown.
+ * </p>
+ *
+ * @param <MODEL>
+ * The model type. Must be compatible with what
+ * {@link #getModelType()} returns.
+ * @param <PRESENTATION>
+ * The presentation type. Must be compatible with what
+ * {@link #getPresentationType()} returns.
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public interface Converter<PRESENTATION, MODEL> extends Serializable {
+
+ /**
+ * Converts the given value from target type to source type.
+ * <p>
+ * A converter can optionally use locale to do the conversion.
+ * </p>
+ * A converter should in most cases be symmetric so chaining
+ * {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should return the original value.
+ *
+ * @param value
+ * The value to convert, compatible with the target type. Can be
+ * null
+ * @param locale
+ * The locale to use for conversion. Can be null.
+ * @return The converted value compatible with the source type
+ * @throws ConversionException
+ * If the value could not be converted
+ */
+ public MODEL convertToModel(PRESENTATION value, Locale locale)
+ throws ConversionException;
+
+ /**
+ * Converts the given value from source type to target type.
+ * <p>
+ * A converter can optionally use locale to do the conversion.
+ * </p>
+ * A converter should in most cases be symmetric so chaining
+ * {@link #convertToPresentation(Object, Locale)} and
+ * {@link #convertToModel(Object, Locale)} should return the original value.
+ *
+ * @param value
+ * The value to convert, compatible with the target type. Can be
+ * null
+ * @param locale
+ * The locale to use for conversion. Can be null.
+ * @return The converted value compatible with the source type
+ * @throws ConversionException
+ * If the value could not be converted
+ */
+ public PRESENTATION convertToPresentation(MODEL value, Locale locale)
+ throws ConversionException;
+
+ /**
+ * The source type of the converter.
+ *
+ * Values of this type can be passed to
+ * {@link #convertToPresentation(Object, Locale)}.
+ *
+ * @return The source type
+ */
+ public Class<MODEL> getModelType();
+
+ /**
+ * The target type of the converter.
+ *
+ * Values of this type can be passed to
+ * {@link #convertToModel(Object, Locale)}.
+ *
+ * @return The target type
+ */
+ public Class<PRESENTATION> getPresentationType();
+
+ /**
+ * An exception that signals that the value passed to
+ * {@link Converter#convertToPresentation(Object, Locale)} or
+ * {@link Converter#convertToModel(Object, Locale)} could not be converted.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+ public static class ConversionException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>ConversionException</code> without a detail
+ * message.
+ */
+ public ConversionException() {
+ }
+
+ /**
+ * Constructs a new <code>ConversionException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public ConversionException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new {@code ConversionException} with the specified
+ * cause.
+ *
+ * @param cause
+ * The cause of the the exception
+ */
+ public ConversionException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new <code>ConversionException</code> with the specified
+ * detail message and cause.
+ *
+ * @param message
+ * the detail message
+ * @param cause
+ * The cause of the the exception
+ */
+ public ConversionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ConverterFactory.java b/server/src/com/vaadin/data/util/converter/ConverterFactory.java
new file mode 100644
index 0000000000..ed4ab41ac0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ConverterFactory.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+
+/**
+ * Factory interface for providing Converters based on a presentation type and a
+ * model type.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ *
+ */
+public interface ConverterFactory extends Serializable {
+ public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType);
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ConverterUtil.java b/server/src/com/vaadin/data/util/converter/ConverterUtil.java
new file mode 100644
index 0000000000..7011496ed7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ConverterUtil.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.converter;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+import com.vaadin.Application;
+
+public class ConverterUtil implements Serializable {
+
+ /**
+ * Finds a converter that can convert from the given presentation type to
+ * the given model type and back. Uses the given application to find a
+ * {@link ConverterFactory} or, if application is null, uses the
+ * {@link Application#getCurrent()}.
+ *
+ * @param <PRESENTATIONTYPE>
+ * The presentation type
+ * @param <MODELTYPE>
+ * The model type
+ * @param presentationType
+ * The presentation type
+ * @param modelType
+ * The model type
+ * @param application
+ * The application to use to find a ConverterFactory or null to
+ * use the current application
+ * @return A Converter capable of converting between the given types or null
+ * if no converter was found
+ */
+ public static <PRESENTATIONTYPE, MODELTYPE> Converter<PRESENTATIONTYPE, MODELTYPE> getConverter(
+ Class<PRESENTATIONTYPE> presentationType,
+ Class<MODELTYPE> modelType, Application application) {
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter = null;
+ if (application == null) {
+ application = Application.getCurrent();
+ }
+
+ if (application != null) {
+ ConverterFactory factory = application.getConverterFactory();
+ converter = factory.createConverter(presentationType, modelType);
+ }
+ return converter;
+
+ }
+
+ /**
+ * Convert the given value from the data source type to the UI type.
+ *
+ * @param modelValue
+ * The model value to convert
+ * @param presentationType
+ * The type of the presentation value
+ * @param converter
+ * The converter to (try to) use
+ * @param locale
+ * The locale to use for conversion
+ * @param <PRESENTATIONTYPE>
+ * Presentation type
+ *
+ * @return The converted value, compatible with the presentation type, or
+ * the original value if its type is compatible and no converter is
+ * set.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the model type.
+ */
+ @SuppressWarnings("unchecked")
+ public static <PRESENTATIONTYPE, MODELTYPE> PRESENTATIONTYPE convertFromModel(
+ MODELTYPE modelValue,
+ Class<? extends PRESENTATIONTYPE> presentationType,
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale)
+ throws Converter.ConversionException {
+ if (converter != null) {
+ return converter.convertToPresentation(modelValue, locale);
+ }
+ if (modelValue == null) {
+ return null;
+ }
+
+ if (presentationType.isAssignableFrom(modelValue.getClass())) {
+ return (PRESENTATIONTYPE) modelValue;
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType
+ + ". No converter is set and the types are not compatible.");
+ }
+ }
+
+ /**
+ * @param <MODELTYPE>
+ * @param <PRESENTATIONTYPE>
+ * @param presentationValue
+ * @param modelType
+ * @param converter
+ * @param locale
+ * @return
+ * @throws Converter.ConversionException
+ */
+ public static <MODELTYPE, PRESENTATIONTYPE> MODELTYPE convertToModel(
+ PRESENTATIONTYPE presentationValue, Class<MODELTYPE> modelType,
+ Converter<PRESENTATIONTYPE, MODELTYPE> converter, Locale locale)
+ throws Converter.ConversionException {
+ if (converter != null) {
+ /*
+ * If there is a converter, always use it. It must convert or throw
+ * an exception.
+ */
+ return converter.convertToModel(presentationValue, locale);
+ }
+
+ if (presentationValue == null) {
+ // Null should always be passed through the converter but if there
+ // is no converter we can safely return null
+ return null;
+ }
+
+ if (modelType == null) {
+ // No model type, return original value
+ return (MODELTYPE) presentationValue;
+ } else if (modelType.isAssignableFrom(presentationValue.getClass())) {
+ // presentation type directly compatible with model type
+ return modelType.cast(presentationValue);
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + presentationValue.getClass().getName()
+ + " to model type "
+ + modelType
+ + ". No converter is set and the types are not compatible.");
+ }
+
+ }
+
+ /**
+ * Checks if the given converter can handle conversion between the given
+ * presentation and model type
+ *
+ * @param converter
+ * The converter to check
+ * @param presentationType
+ * The presentation type
+ * @param modelType
+ * The model type
+ * @return true if the converter supports conversion between the given
+ * presentation and model type, false otherwise
+ */
+ public static boolean canConverterHandle(Converter<?, ?> converter,
+ Class<?> presentationType, Class<?> modelType) {
+ if (converter == null) {
+ return false;
+ }
+
+ if (!modelType.isAssignableFrom(converter.getModelType())) {
+ return false;
+ }
+ if (!presentationType.isAssignableFrom(converter.getPresentationType())) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/converter/DateToLongConverter.java b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java
new file mode 100644
index 0000000000..aeba38aa1f
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/DateToLongConverter.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Long} to {@link Date} and back.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DateToLongConverter implements Converter<Date, Long> {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Long convertToModel(Date value, Locale locale) {
+ if (value == null) {
+ return null;
+ }
+
+ return value.getTime();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public Date convertToPresentation(Long value, Locale locale) {
+ if (value == null) {
+ return null;
+ }
+
+ return new Date(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Long> getModelType() {
+ return Long.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<Date> getPresentationType() {
+ return Date.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java
new file mode 100644
index 0000000000..afb95d81ed
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java
@@ -0,0 +1,101 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Date;
+import java.util.logging.Logger;
+
+import com.vaadin.Application;
+
+/**
+ * Default implementation of {@link ConverterFactory}. Provides converters for
+ * standard types like {@link String}, {@link Double} and {@link Date}. </p>
+ * <p>
+ * Custom converters can be provided by extending this class and using
+ * {@link Application#setConverterFactory(ConverterFactory)}.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DefaultConverterFactory implements ConverterFactory {
+
+ private final static Logger log = Logger
+ .getLogger(DefaultConverterFactory.class.getName());
+
+ @Override
+ public <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> createConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType) {
+ Converter<PRESENTATION, MODEL> converter = findConverter(
+ presentationType, modelType);
+ if (converter != null) {
+ log.finest(getClass().getName() + " created a "
+ + converter.getClass());
+ return converter;
+ }
+
+ // Try to find a reverse converter
+ Converter<MODEL, PRESENTATION> reverseConverter = findConverter(
+ modelType, presentationType);
+ if (reverseConverter != null) {
+ log.finest(getClass().getName() + " created a reverse "
+ + reverseConverter.getClass());
+ return new ReverseConverter<PRESENTATION, MODEL>(reverseConverter);
+ }
+
+ log.finest(getClass().getName() + " could not find a converter for "
+ + presentationType.getName() + " to " + modelType.getName()
+ + " conversion");
+ return null;
+
+ }
+
+ protected <PRESENTATION, MODEL> Converter<PRESENTATION, MODEL> findConverter(
+ Class<PRESENTATION> presentationType, Class<MODEL> modelType) {
+ if (presentationType == String.class) {
+ // TextField converters and more
+ Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createStringConverter(modelType);
+ if (converter != null) {
+ return converter;
+ }
+ } else if (presentationType == Date.class) {
+ // DateField converters and more
+ Converter<PRESENTATION, MODEL> converter = (Converter<PRESENTATION, MODEL>) createDateConverter(modelType);
+ if (converter != null) {
+ return converter;
+ }
+ }
+
+ return null;
+
+ }
+
+ protected Converter<Date, ?> createDateConverter(Class<?> sourceType) {
+ if (Long.class.isAssignableFrom(sourceType)) {
+ return new DateToLongConverter();
+ } else {
+ return null;
+ }
+ }
+
+ protected Converter<String, ?> createStringConverter(Class<?> sourceType) {
+ if (Double.class.isAssignableFrom(sourceType)) {
+ return new StringToDoubleConverter();
+ } else if (Integer.class.isAssignableFrom(sourceType)) {
+ return new StringToIntegerConverter();
+ } else if (Boolean.class.isAssignableFrom(sourceType)) {
+ return new StringToBooleanConverter();
+ } else if (Number.class.isAssignableFrom(sourceType)) {
+ return new StringToNumberConverter();
+ } else if (Date.class.isAssignableFrom(sourceType)) {
+ return new StringToDateConverter();
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/ReverseConverter.java b/server/src/com/vaadin/data/util/converter/ReverseConverter.java
new file mode 100644
index 0000000000..fa1bb5daf1
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/ReverseConverter.java
@@ -0,0 +1,84 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Locale;
+
+/**
+ * A converter that wraps another {@link Converter} and reverses source and
+ * target types.
+ *
+ * @param <MODEL>
+ * The source type
+ * @param <PRESENTATION>
+ * The target type
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class ReverseConverter<PRESENTATION, MODEL> implements
+ Converter<PRESENTATION, MODEL> {
+
+ private Converter<MODEL, PRESENTATION> realConverter;
+
+ /**
+ * Creates a converter from source to target based on a converter that
+ * converts from target to source.
+ *
+ * @param converter
+ * The converter to use in a reverse fashion
+ */
+ public ReverseConverter(Converter<MODEL, PRESENTATION> converter) {
+ this.realConverter = converter;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#convertToModel(java
+ * .lang.Object, java.util.Locale)
+ */
+ @Override
+ public MODEL convertToModel(PRESENTATION value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ return realConverter.convertToPresentation(value, locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public PRESENTATION convertToPresentation(MODEL value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ return realConverter.convertToModel(value, locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getSourceType()
+ */
+ @Override
+ public Class<MODEL> getModelType() {
+ return realConverter.getPresentationType();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getTargetType()
+ */
+ @Override
+ public Class<PRESENTATION> getPresentationType() {
+ return realConverter.getModelType();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java
new file mode 100644
index 0000000000..999f575dc4
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java
@@ -0,0 +1,108 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Boolean} and back.
+ * The String representation is given by Boolean.toString().
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToBooleanConverter implements Converter<String, Boolean> {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Boolean convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ if (getTrueString().equals(value)) {
+ return true;
+ } else if (getFalseString().equals(value)) {
+ return false;
+ } else {
+ throw new ConversionException("Cannot convert " + value + " to "
+ + getModelType().getName());
+ }
+ }
+
+ /**
+ * Gets the string representation for true. Default is "true".
+ *
+ * @return the string representation for true
+ */
+ protected String getTrueString() {
+ return Boolean.TRUE.toString();
+ }
+
+ /**
+ * Gets the string representation for false. Default is "false".
+ *
+ * @return the string representation for false
+ */
+ protected String getFalseString() {
+ return Boolean.FALSE.toString();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Boolean value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+ if (value) {
+ return getTrueString();
+ } else {
+ return getFalseString();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Boolean> getModelType() {
+ return Boolean.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToDateConverter.java b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java
new file mode 100644
index 0000000000..487b02b2aa
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToDateConverter.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.DateFormat;
+import java.text.ParsePosition;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Date} to {@link String} and back. Uses
+ * the given locale and {@link DateFormat} for formatting and parsing.
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToDateConverter implements Converter<String, Date> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Date, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A DateFormat instance
+ */
+ protected DateFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ DateFormat f = DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
+ DateFormat.MEDIUM, locale);
+ f.setLenient(false);
+ return f;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Date convertToModel(String value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ ParsePosition parsePosition = new ParsePosition(0);
+ Date parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ return parsedValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Date value, Locale locale)
+ throws com.vaadin.data.util.converter.Converter.ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Date> getModelType() {
+ return Date.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java
new file mode 100644
index 0000000000..251f91855b
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java
@@ -0,0 +1,107 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Double} and back.
+ * Uses the given locale and a {@link NumberFormat} instance for formatting and
+ * parsing.
+ * <p>
+ * Leading and trailing white spaces are ignored when converting from a String.
+ * </p>
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToDoubleConverter implements Converter<String, Double> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Double, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ return NumberFormat.getNumberInstance(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Double convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+ return parsedValue.doubleValue();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Double value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Double> getModelType() {
+ return Double.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java
new file mode 100644
index 0000000000..950f01c6ab
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java
@@ -0,0 +1,88 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link String} to {@link Integer} and back.
+ * Uses the given locale and a {@link NumberFormat} instance for formatting and
+ * parsing.
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToIntegerConverter implements Converter<String, Integer> {
+
+ /**
+ * Returns the format used by
+ * {@link #convertToPresentation(Integer, Locale)} and
+ * {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ return NumberFormat.getIntegerInstance(locale);
+ }
+
+ @Override
+ public Integer convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ // Parse and detect errors. If the full string was not used, it is
+ // an error.
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ if (parsedValue == null) {
+ // Convert "" to null
+ return null;
+ }
+ return parsedValue.intValue();
+ }
+
+ @Override
+ public String convertToPresentation(Integer value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ @Override
+ public Class<Integer> getModelType() {
+ return Integer.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java
new file mode 100644
index 0000000000..42699a326a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/converter/StringToNumberConverter.java
@@ -0,0 +1,111 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.util.converter;
+
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/**
+ * A converter that converts from {@link Number} to {@link String} and back.
+ * Uses the given locale and {@link NumberFormat} for formatting and parsing.
+ * <p>
+ * Override and overwrite {@link #getFormat(Locale)} to use a different format.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class StringToNumberConverter implements Converter<String, Number> {
+
+ /**
+ * Returns the format used by {@link #convertToPresentation(Number, Locale)}
+ * and {@link #convertToModel(String, Locale)}.
+ *
+ * @param locale
+ * The locale to use
+ * @return A NumberFormat instance
+ */
+ protected NumberFormat getFormat(Locale locale) {
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+
+ return NumberFormat.getNumberInstance(locale);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object,
+ * java.util.Locale)
+ */
+ @Override
+ public Number convertToModel(String value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ // Remove leading and trailing white space
+ value = value.trim();
+
+ // Parse and detect errors. If the full string was not used, it is
+ // an error.
+ ParsePosition parsePosition = new ParsePosition(0);
+ Number parsedValue = getFormat(locale).parse(value, parsePosition);
+ if (parsePosition.getIndex() != value.length()) {
+ throw new ConversionException("Could not convert '" + value
+ + "' to " + getModelType().getName());
+ }
+
+ if (parsedValue == null) {
+ // Convert "" to null
+ return null;
+ }
+ return parsedValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang
+ * .Object, java.util.Locale)
+ */
+ @Override
+ public String convertToPresentation(Number value, Locale locale)
+ throws ConversionException {
+ if (value == null) {
+ return null;
+ }
+
+ return getFormat(locale).format(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getModelType()
+ */
+ @Override
+ public Class<Number> getModelType() {
+ return Number.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.converter.Converter#getPresentationType()
+ */
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java
new file mode 100644
index 0000000000..482b10120c
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java
@@ -0,0 +1,76 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.data.Container.Filter;
+
+/**
+ * Abstract base class for filters that are composed of multiple sub-filters.
+ *
+ * The method {@link #appliesToProperty(Object)} is provided to help
+ * implementing {@link Filter} for in-memory filters.
+ *
+ * @since 6.6
+ */
+public abstract class AbstractJunctionFilter implements Filter {
+
+ protected final Collection<Filter> filters;
+
+ public AbstractJunctionFilter(Filter... filters) {
+ this.filters = Collections.unmodifiableCollection(Arrays
+ .asList(filters));
+ }
+
+ /**
+ * Returns an unmodifiable collection of the sub-filters of this composite
+ * filter.
+ *
+ * @return
+ */
+ public Collection<Filter> getFilters() {
+ return filters;
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. If some of the sub-filters are not in-memory filters, true is
+ * returned.
+ *
+ * By default, all sub-filters are iterated to check if any of them applies.
+ * If there are no sub-filters, false is returned - override in subclasses
+ * to change this behavior.
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ for (Filter filter : getFilters()) {
+ if (filter.appliesToProperty(propertyId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !getClass().equals(obj.getClass())) {
+ return false;
+ }
+ AbstractJunctionFilter other = (AbstractJunctionFilter) obj;
+ // contents comparison with equals()
+ return Arrays.equals(filters.toArray(), other.filters.toArray());
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = getFilters().size();
+ for (Filter filter : filters) {
+ hash = (hash << 1) ^ filter.hashCode();
+ }
+ return hash;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/filter/And.java b/server/src/com/vaadin/data/util/filter/And.java
new file mode 100644
index 0000000000..ca6c35aba7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/And.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * A compound {@link Filter} that accepts an item if all of its filters accept
+ * the item.
+ *
+ * If no filters are given, the filter should accept all items.
+ *
+ * This filter also directly supports in-memory filtering when all sub-filters
+ * do so.
+ *
+ * @see Or
+ *
+ * @since 6.6
+ */
+public final class And extends AbstractJunctionFilter {
+
+ /**
+ *
+ * @param filters
+ * filters of which the And filter will be composed
+ */
+ public And(Filter... filters) {
+ super(filters);
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedFilterException {
+ for (Filter filter : getFilters()) {
+ if (!filter.passesFilter(itemId, item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Between.java b/server/src/com/vaadin/data/util/filter/Between.java
new file mode 100644
index 0000000000..b00a74d13d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Between.java
@@ -0,0 +1,74 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+public class Between implements Filter {
+
+ private final Object propertyId;
+ private final Comparable startValue;
+ private final Comparable endValue;
+
+ public Between(Object propertyId, Comparable startValue, Comparable endValue) {
+ this.propertyId = propertyId;
+ this.startValue = startValue;
+ this.endValue = endValue;
+ }
+
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ public Comparable<?> getStartValue() {
+ return startValue;
+ }
+
+ public Comparable<?> getEndValue() {
+ return endValue;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ Object value = item.getItemProperty(getPropertyId()).getValue();
+ if (value instanceof Comparable) {
+ Comparable cval = (Comparable) value;
+ return cval.compareTo(getStartValue()) >= 0
+ && cval.compareTo(getEndValue()) <= 0;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId() != null && getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public int hashCode() {
+ return getPropertyId().hashCode() + getStartValue().hashCode()
+ + getEndValue().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final Between o = (Between) obj;
+
+ // Checks the properties one by one
+ boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId()
+ .equals(o.getPropertyId()) : null == o.getPropertyId();
+ boolean startValueEqual = (null != getStartValue()) ? getStartValue()
+ .equals(o.getStartValue()) : null == o.getStartValue();
+ boolean endValueEqual = (null != getEndValue()) ? getEndValue().equals(
+ o.getEndValue()) : null == o.getEndValue();
+ return propertyIdEqual && startValueEqual && endValueEqual;
+
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/Compare.java b/server/src/com/vaadin/data/util/filter/Compare.java
new file mode 100644
index 0000000000..4091f5b922
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Compare.java
@@ -0,0 +1,327 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple container filter comparing an item property value against a given
+ * constant value. Use the nested classes {@link Equal}, {@link Greater},
+ * {@link Less}, {@link GreaterOrEqual} and {@link LessOrEqual} instead of this
+ * class directly.
+ *
+ * This filter also directly supports in-memory filtering.
+ *
+ * The reference and actual values must implement {@link Comparable} and the
+ * class of the actual property value must be assignable from the class of the
+ * reference value.
+ *
+ * @since 6.6
+ */
+public abstract class Compare implements Filter {
+
+ public enum Operation {
+ EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL
+ };
+
+ private final Object propertyId;
+ private final Operation operation;
+ private final Object value;
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is equal to <code>value</code>.
+ *
+ * For in-memory filters, equals() is used for the comparison. For other
+ * containers, the comparison implementation is container dependent and may
+ * use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Equal extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is equal to <code>value</code>.
+ *
+ * For in-memory filters, equals() is used for the comparison. For other
+ * containers, the comparison implementation is container dependent and
+ * may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Equal(Object propertyId, Object value) {
+ super(propertyId, value, Operation.EQUAL);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is greater than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Greater extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is greater than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Greater(Object propertyId, Object value) {
+ super(propertyId, value, Operation.GREATER);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is less than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class Less extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is less than <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public Less(Object propertyId, Object value) {
+ super(propertyId, value, Operation.LESS);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is greater than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class GreaterOrEqual extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is greater than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public GreaterOrEqual(Object propertyId, Object value) {
+ super(propertyId, value, Operation.GREATER_OR_EQUAL);
+ }
+ }
+
+ /**
+ * A {@link Compare} filter that accepts items for which the identified
+ * property value is less than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable} and
+ * {@link Comparable#compareTo(Object)} is used for the comparison. For
+ * other containers, the comparison implementation is container dependent
+ * and may use e.g. database comparison operations.
+ *
+ * @since 6.6
+ */
+ public static final class LessOrEqual extends Compare {
+ /**
+ * Construct a filter that accepts items for which the identified
+ * property value is less than or equal to <code>value</code>.
+ *
+ * For in-memory filters, the values must implement {@link Comparable}
+ * and {@link Comparable#compareTo(Object)} is used for the comparison.
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare
+ * against value, not null
+ * @param value
+ * the value to compare against - null values may or may not
+ * be supported depending on the container
+ */
+ public LessOrEqual(Object propertyId, Object value) {
+ super(propertyId, value, Operation.LESS_OR_EQUAL);
+ }
+ }
+
+ /**
+ * Constructor for a {@link Compare} filter that compares the value of an
+ * item property with the given constant <code>value</code>.
+ *
+ * This constructor is intended to be used by the nested static classes only
+ * ({@link Equal}, {@link Greater}, {@link Less}, {@link GreaterOrEqual},
+ * {@link LessOrEqual}).
+ *
+ * For in-memory filtering, comparisons except EQUAL require that the values
+ * implement {@link Comparable} and {@link Comparable#compareTo(Object)} is
+ * used for the comparison. The equality comparison is performed using
+ * {@link Object#equals(Object)}.
+ *
+ * For other containers, the comparison implementation is container
+ * dependent and may use e.g. database comparison operations. Therefore, the
+ * behavior of comparisons might differ in some cases between in-memory and
+ * other containers.
+ *
+ * @param propertyId
+ * the identifier of the property whose value to compare against
+ * value, not null
+ * @param value
+ * the value to compare against - null values may or may not be
+ * supported depending on the container
+ * @param operation
+ * the comparison {@link Operation} to use
+ */
+ Compare(Object propertyId, Object value, Operation operation) {
+ this.propertyId = propertyId;
+ this.value = value;
+ this.operation = operation;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item) {
+ final Property<?> p = item.getItemProperty(getPropertyId());
+ if (null == p) {
+ return false;
+ }
+ Object value = p.getValue();
+ switch (getOperation()) {
+ case EQUAL:
+ return (null == this.value) ? (null == value) : this.value
+ .equals(value);
+ case GREATER:
+ return compareValue(value) > 0;
+ case LESS:
+ return compareValue(value) < 0;
+ case GREATER_OR_EQUAL:
+ return compareValue(value) >= 0;
+ case LESS_OR_EQUAL:
+ return compareValue(value) <= 0;
+ }
+ // all cases should have been processed above
+ return false;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ protected int compareValue(Object value1) {
+ if (null == value) {
+ return null == value1 ? 0 : -1;
+ } else if (null == value1) {
+ return 1;
+ } else if (getValue() instanceof Comparable
+ && value1.getClass().isAssignableFrom(getValue().getClass())) {
+ return -((Comparable) getValue()).compareTo(value1);
+ }
+ throw new IllegalArgumentException("Could not compare the arguments: "
+ + value1 + ", " + getValue());
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final Compare o = (Compare) obj;
+
+ // Checks the properties one by one
+ if (getPropertyId() != o.getPropertyId() && null != o.getPropertyId()
+ && !o.getPropertyId().equals(getPropertyId())) {
+ return false;
+ }
+ if (getOperation() != o.getOperation()) {
+ return false;
+ }
+ return (null == getValue()) ? null == o.getValue() : getValue().equals(
+ o.getValue());
+ }
+
+ @Override
+ public int hashCode() {
+ return (null != getPropertyId() ? getPropertyId().hashCode() : 0)
+ ^ (null != getValue() ? getValue().hashCode() : 0);
+ }
+
+ /**
+ * Returns the property id of the property to compare against the fixed
+ * value.
+ *
+ * @return property id (not null)
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the comparison operation.
+ *
+ * @return {@link Operation}
+ */
+ public Operation getOperation() {
+ return operation;
+ }
+
+ /**
+ * Returns the value to compare the property against.
+ *
+ * @return comparison reference value
+ */
+ public Object getValue() {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/IsNull.java b/server/src/com/vaadin/data/util/filter/IsNull.java
new file mode 100644
index 0000000000..3faf4153ee
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/IsNull.java
@@ -0,0 +1,79 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple container filter checking whether an item property value is null.
+ *
+ * This filter also directly supports in-memory filtering.
+ *
+ * @since 6.6
+ */
+public final class IsNull implements Filter {
+
+ private final Object propertyId;
+
+ /**
+ * Constructor for a filter that compares the value of an item property with
+ * null.
+ *
+ * For in-memory filtering, a simple == check is performed. For other
+ * containers, the comparison implementation is container dependent but
+ * should correspond to the in-memory null check.
+ *
+ * @param propertyId
+ * the identifier (not null) of the property whose value to check
+ */
+ public IsNull(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ final Property<?> p = item.getItemProperty(getPropertyId());
+ if (null == p) {
+ return false;
+ }
+ return null == p.getValue();
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final IsNull o = (IsNull) obj;
+
+ // Checks the properties one by one
+ return (null != getPropertyId()) ? getPropertyId().equals(
+ o.getPropertyId()) : null == o.getPropertyId();
+ }
+
+ @Override
+ public int hashCode() {
+ return (null != getPropertyId() ? getPropertyId().hashCode() : 0);
+ }
+
+ /**
+ * Returns the property id of the property tested by the filter, not null
+ * for valid filters.
+ *
+ * @return property id (not null)
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Like.java b/server/src/com/vaadin/data/util/filter/Like.java
new file mode 100644
index 0000000000..3dcc48e809
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Like.java
@@ -0,0 +1,83 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+public class Like implements Filter {
+ private final Object propertyId;
+ private final String value;
+ private boolean caseSensitive;
+
+ public Like(String propertyId, String value) {
+ this(propertyId, value, true);
+ }
+
+ public Like(String propertyId, String value, boolean caseSensitive) {
+ this.propertyId = propertyId;
+ this.value = value;
+ setCaseSensitive(caseSensitive);
+ }
+
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setCaseSensitive(boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ }
+
+ public boolean isCaseSensitive() {
+ return caseSensitive;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ if (!item.getItemProperty(getPropertyId()).getType()
+ .isAssignableFrom(String.class)) {
+ // We can only handle strings
+ return false;
+ }
+ String colValue = (String) item.getItemProperty(getPropertyId())
+ .getValue();
+
+ String pattern = getValue().replace("%", ".*");
+ if (isCaseSensitive()) {
+ return colValue.matches(pattern);
+ }
+ return colValue.toUpperCase().matches(pattern.toUpperCase());
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return getPropertyId() != null && getPropertyId().equals(propertyId);
+ }
+
+ @Override
+ public int hashCode() {
+ return getPropertyId().hashCode() + getValue().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // Only objects of the same class can be equal
+ if (!getClass().equals(obj.getClass())) {
+ return false;
+ }
+ final Like o = (Like) obj;
+
+ // Checks the properties one by one
+ boolean propertyIdEqual = (null != getPropertyId()) ? getPropertyId()
+ .equals(o.getPropertyId()) : null == o.getPropertyId();
+ boolean valueEqual = (null != getValue()) ? getValue().equals(
+ o.getValue()) : null == o.getValue();
+ return propertyIdEqual && valueEqual;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/Not.java b/server/src/com/vaadin/data/util/filter/Not.java
new file mode 100644
index 0000000000..bbfc9ca86a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Not.java
@@ -0,0 +1,70 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * Negating filter that accepts the items rejected by another filter.
+ *
+ * This filter directly supports in-memory filtering when the negated filter
+ * does so.
+ *
+ * @since 6.6
+ */
+public final class Not implements Filter {
+ private final Filter filter;
+
+ /**
+ * Constructs a filter that negates a filter.
+ *
+ * @param filter
+ * {@link Filter} to negate, not-null
+ */
+ public Not(Filter filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Returns the negated filter.
+ *
+ * @return Filter
+ */
+ public Filter getFilter() {
+ return filter;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ return !filter.passesFilter(itemId, item);
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. Return value is the same as {@link #appliesToProperty(Object)}
+ * for the negated filter.
+ *
+ * @return boolean
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return filter.appliesToProperty(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !getClass().equals(obj.getClass())) {
+ return false;
+ }
+ return filter.equals(((Not) obj).getFilter());
+ }
+
+ @Override
+ public int hashCode() {
+ return filter.hashCode();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/Or.java b/server/src/com/vaadin/data/util/filter/Or.java
new file mode 100644
index 0000000000..b60074f7e3
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/Or.java
@@ -0,0 +1,63 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+/**
+ * A compound {@link Filter} that accepts an item if any of its filters accept
+ * the item.
+ *
+ * If no filters are given, the filter should reject all items.
+ *
+ * This filter also directly supports in-memory filtering when all sub-filters
+ * do so.
+ *
+ * @see And
+ *
+ * @since 6.6
+ */
+public final class Or extends AbstractJunctionFilter {
+
+ /**
+ *
+ * @param filters
+ * filters of which the Or filter will be composed
+ */
+ public Or(Filter... filters) {
+ super(filters);
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedFilterException {
+ for (Filter filter : getFilters()) {
+ if (filter.passesFilter(itemId, item)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if a change in the named property may affect the filtering
+ * result. If some of the sub-filters are not in-memory filters, true is
+ * returned.
+ *
+ * By default, all sub-filters are iterated to check if any of them applies.
+ * If there are no sub-filters, true is returned as an empty Or rejects all
+ * items.
+ */
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ if (getFilters().isEmpty()) {
+ // empty Or filters out everything
+ return true;
+ } else {
+ return super.appliesToProperty(propertyId);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java
new file mode 100644
index 0000000000..f98b2c02b4
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/SimpleStringFilter.java
@@ -0,0 +1,152 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * Simple string filter for matching items that start with or contain a
+ * specified string. The matching can be case-sensitive or case-insensitive.
+ *
+ * This filter also directly supports in-memory filtering. When performing
+ * in-memory filtering, values of other types are converted using toString(),
+ * but other (lazy container) implementations do not need to perform such
+ * conversions and might not support values of different types.
+ *
+ * Note that this filter is modeled after the pre-6.6 filtering mechanisms, and
+ * might not be very efficient e.g. for database filtering.
+ *
+ * TODO this might still change
+ *
+ * @since 6.6
+ */
+public final class SimpleStringFilter implements Filter {
+
+ final Object propertyId;
+ final String filterString;
+ final boolean ignoreCase;
+ final boolean onlyMatchPrefix;
+
+ public SimpleStringFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ this.propertyId = propertyId;
+ this.filterString = ignoreCase ? filterString.toLowerCase()
+ : filterString;
+ this.ignoreCase = ignoreCase;
+ this.onlyMatchPrefix = onlyMatchPrefix;
+ }
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item) {
+ final Property<?> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ return false;
+ }
+ Object propertyValue = p.getValue();
+ if (propertyValue == null) {
+ return false;
+ }
+ final String value = ignoreCase ? propertyValue.toString()
+ .toLowerCase() : propertyValue.toString();
+ if (onlyMatchPrefix) {
+ if (!value.startsWith(filterString)) {
+ return false;
+ }
+ } else {
+ if (!value.contains(filterString)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return this.propertyId.equals(propertyId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ // Only ones of the objects of the same class can be equal
+ if (!(obj instanceof SimpleStringFilter)) {
+ return false;
+ }
+ final SimpleStringFilter o = (SimpleStringFilter) obj;
+
+ // Checks the properties one by one
+ if (propertyId != o.propertyId && o.propertyId != null
+ && !o.propertyId.equals(propertyId)) {
+ return false;
+ }
+ if (filterString != o.filterString && o.filterString != null
+ && !o.filterString.equals(filterString)) {
+ return false;
+ }
+ if (ignoreCase != o.ignoreCase) {
+ return false;
+ }
+ if (onlyMatchPrefix != o.onlyMatchPrefix) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return (propertyId != null ? propertyId.hashCode() : 0)
+ ^ (filterString != null ? filterString.hashCode() : 0);
+ }
+
+ /**
+ * Returns the property identifier to which this filter applies.
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the filter string.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return filter string given to the constructor
+ */
+ public String getFilterString() {
+ return filterString;
+ }
+
+ /**
+ * Returns whether the filter is case-insensitive or case-sensitive.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return true if performing case-insensitive filtering, false for
+ * case-sensitive
+ */
+ public boolean isIgnoreCase() {
+ return ignoreCase;
+ }
+
+ /**
+ * Returns true if the filter only applies to the beginning of the value
+ * string, false for any location in the value.
+ *
+ * Note: this method is intended only for implementations of lazy string
+ * filters and may change in the future.
+ *
+ * @return true if checking for matches at the beginning of the value only,
+ * false if matching any part of value
+ */
+ public boolean isOnlyMatchPrefix() {
+ return onlyMatchPrefix;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java
new file mode 100644
index 0000000000..c09cc474e9
--- /dev/null
+++ b/server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.filter;
+
+import java.io.Serializable;
+
+/**
+ * Exception for cases where a container does not support a specific type of
+ * filters.
+ *
+ * If possible, this should be thrown already when adding a filter to a
+ * container. If a problem is not detected at that point, an
+ * {@link UnsupportedOperationException} can be throws when attempting to
+ * perform filtering.
+ *
+ * @since 6.6
+ */
+public class UnsupportedFilterException extends RuntimeException implements
+ Serializable {
+ public UnsupportedFilterException() {
+ }
+
+ public UnsupportedFilterException(String message) {
+ super(message);
+ }
+
+ public UnsupportedFilterException(Exception cause) {
+ super(cause);
+ }
+
+ public UnsupportedFilterException(String message, Exception cause) {
+ super(message, cause);
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/package.html b/server/src/com/vaadin/data/util/package.html
new file mode 100644
index 0000000000..07e3acde9e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/package.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<p>Provides implementations of Property, Item and Container
+interfaces, and utilities for the data layer.</p>
+
+<p>Various Property, Item and Container implementations are provided
+in this package. Each implementation can have its own sets of
+constraints on the data it encapsulates and on how the implementation
+can be used. See the class javadocs for more information.</p>
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java
new file mode 100644
index 0000000000..788966048d
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java
@@ -0,0 +1,92 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.util.sqlcontainer.query.FreeformQuery;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate;
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+
+/**
+ * CacheFlushNotifier is a simple static notification mechanism to inform other
+ * SQLContainers that the contents of their caches may have become stale.
+ */
+class CacheFlushNotifier implements Serializable {
+ /*
+ * SQLContainer instance reference list and dead reference queue. Used for
+ * the cache flush notification feature.
+ */
+ private static List<WeakReference<SQLContainer>> allInstances = new ArrayList<WeakReference<SQLContainer>>();
+ private static ReferenceQueue<SQLContainer> deadInstances = new ReferenceQueue<SQLContainer>();
+
+ /**
+ * Adds the given SQLContainer to the cache flush notification receiver list
+ *
+ * @param c
+ * Container to add
+ */
+ public static void addInstance(SQLContainer c) {
+ removeDeadReferences();
+ if (c != null) {
+ allInstances.add(new WeakReference<SQLContainer>(c, deadInstances));
+ }
+ }
+
+ /**
+ * Removes dead references from instance list
+ */
+ private static void removeDeadReferences() {
+ java.lang.ref.Reference<? extends SQLContainer> dead = deadInstances
+ .poll();
+ while (dead != null) {
+ allInstances.remove(dead);
+ dead = deadInstances.poll();
+ }
+ }
+
+ /**
+ * Iterates through the instances and notifies containers which are
+ * connected to the same table or are using the same query string.
+ *
+ * @param c
+ * SQLContainer that issued the cache flush notification
+ */
+ public static void notifyOfCacheFlush(SQLContainer c) {
+ removeDeadReferences();
+ for (WeakReference<SQLContainer> wr : allInstances) {
+ if (wr.get() != null) {
+ SQLContainer wrc = wr.get();
+ if (wrc == null) {
+ continue;
+ }
+ /*
+ * If the reference points to the container sending the
+ * notification, do nothing.
+ */
+ if (wrc.equals(c)) {
+ continue;
+ }
+ /* Compare QueryDelegate types and tableName/queryString */
+ QueryDelegate wrQd = wrc.getQueryDelegate();
+ QueryDelegate qd = c.getQueryDelegate();
+ if (wrQd instanceof TableQuery
+ && qd instanceof TableQuery
+ && ((TableQuery) wrQd).getTableName().equals(
+ ((TableQuery) qd).getTableName())) {
+ wrc.refresh();
+ } else if (wrQd instanceof FreeformQuery
+ && qd instanceof FreeformQuery
+ && ((FreeformQuery) wrQd).getQueryString().equals(
+ ((FreeformQuery) qd).getQueryString())) {
+ wrc.refresh();
+ }
+ }
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java
new file mode 100644
index 0000000000..839fceb3c2
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * CacheMap extends LinkedHashMap, adding the possibility to adjust maximum
+ * number of items. In SQLContainer this is used for RowItem -cache. Cache size
+ * will be two times the page length parameter of the container.
+ */
+class CacheMap<K, V> extends LinkedHashMap<K, V> {
+ private static final long serialVersionUID = 679999766473555231L;
+ private int cacheLimit = SQLContainer.CACHE_RATIO
+ * SQLContainer.DEFAULT_PAGE_LENGTH;
+
+ @Override
+ protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+ return size() > cacheLimit;
+ }
+
+ void setCacheLimit(int limit) {
+ cacheLimit = limit > 0 ? limit : SQLContainer.DEFAULT_PAGE_LENGTH;
+ }
+
+ int getCacheLimit() {
+ return cacheLimit;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java
new file mode 100644
index 0000000000..168bce1880
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java
@@ -0,0 +1,248 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+
+import com.vaadin.data.Property;
+
+/**
+ * ColumnProperty represents the value of one column in a RowItem. In addition
+ * to the value, ColumnProperty also contains some basic column attributes such
+ * as nullability status, read-only status and data type.
+ *
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual column in a database table.
+ */
+final public class ColumnProperty implements Property {
+ private static final long serialVersionUID = -3694463129581802457L;
+
+ private RowItem owner;
+
+ private String propertyId;
+
+ private boolean readOnly;
+ private boolean allowReadOnlyChange = true;
+ private boolean nullable = true;
+
+ private Object value;
+ private Object changedValue;
+ private Class<?> type;
+
+ private boolean modified;
+
+ private boolean versionColumn;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private ColumnProperty() {
+ }
+
+ public ColumnProperty(String propertyId, boolean readOnly,
+ boolean allowReadOnlyChange, boolean nullable, Object value,
+ Class<?> type) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException("Properties must be named.");
+ }
+ if (type == null) {
+ throw new IllegalArgumentException("Property type must be set.");
+ }
+ this.propertyId = propertyId;
+ this.type = type;
+ this.value = value;
+
+ this.allowReadOnlyChange = allowReadOnlyChange;
+ this.nullable = nullable;
+ this.readOnly = readOnly;
+ }
+
+ @Override
+ public Object getValue() {
+ if (isModified()) {
+ return changedValue;
+ }
+ return value;
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ if (newValue == null && !nullable) {
+ throw new NotNullableException(
+ "Null values are not allowed for this property.");
+ }
+ if (readOnly) {
+ throw new ReadOnlyException(
+ "Cannot set value for read-only property.");
+ }
+
+ /* Check if this property is a date property. */
+ boolean isDateProperty = Time.class.equals(getType())
+ || Date.class.equals(getType())
+ || Timestamp.class.equals(getType());
+
+ if (newValue != null) {
+ /* Handle SQL dates, times and Timestamps given as java.util.Date */
+ if (isDateProperty) {
+ /*
+ * Try to get the millisecond value from the new value of this
+ * property. Possible type to convert from is java.util.Date.
+ */
+ long millis = 0;
+ if (newValue instanceof java.util.Date) {
+ millis = ((java.util.Date) newValue).getTime();
+ /*
+ * Create the new object based on the millisecond value,
+ * according to the type of this property.
+ */
+ if (Time.class.equals(getType())) {
+ newValue = new Time(millis);
+ } else if (Date.class.equals(getType())) {
+ newValue = new Date(millis);
+ } else if (Timestamp.class.equals(getType())) {
+ newValue = new Timestamp(millis);
+ }
+ }
+ }
+
+ if (!getType().isAssignableFrom(newValue.getClass())) {
+ throw new IllegalArgumentException(
+ "Illegal value type for ColumnProperty");
+ }
+
+ /*
+ * If the value to be set is the same that has already been set, do
+ * not set it again.
+ */
+ if (isValueAlreadySet(newValue)) {
+ return;
+ }
+ }
+
+ /* Set the new value and notify container of the change. */
+ changedValue = newValue;
+ modified = true;
+ owner.getContainer().itemChangeNotification(owner);
+ }
+
+ private boolean isValueAlreadySet(Object newValue) {
+ Object referenceValue = isModified() ? changedValue : value;
+
+ return (isNullable() && newValue == null && referenceValue == null)
+ || newValue.equals(referenceValue);
+ }
+
+ @Override
+ public Class<?> getType() {
+ return type;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public boolean isReadOnlyChangeAllowed() {
+ return allowReadOnlyChange;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ if (allowReadOnlyChange) {
+ readOnly = newStatus;
+ }
+ }
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the value of the Property in human readable textual format.
+ *
+ * @see java.lang.Object#toString()
+ * @deprecated get the string representation from the value
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ throw new UnsupportedOperationException(
+ "Use ColumnProperty.getValue() instead of ColumnProperty.toString()");
+ }
+
+ public void setOwner(RowItem owner) {
+ if (owner == null) {
+ throw new IllegalArgumentException("Owner can not be set to null.");
+ }
+ if (this.owner != null) {
+ throw new IllegalStateException(
+ "ColumnProperties can only be bound once.");
+ }
+ this.owner = owner;
+ }
+
+ public boolean isModified() {
+ return modified;
+ }
+
+ public boolean isVersionColumn() {
+ return versionColumn;
+ }
+
+ public void setVersionColumn(boolean versionColumn) {
+ this.versionColumn = versionColumn;
+ }
+
+ public boolean isNullable() {
+ return nullable;
+ }
+
+ /**
+ * An exception that signals that a <code>null</code> value was passed to
+ * the <code>setValue</code> method, but the value of this property can not
+ * be set to <code>null</code>.
+ */
+ @SuppressWarnings("serial")
+ public class NotNullableException extends RuntimeException {
+
+ /**
+ * Constructs a new <code>NotNullableException</code> without a detail
+ * message.
+ */
+ public NotNullableException() {
+ }
+
+ /**
+ * Constructs a new <code>NotNullableException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message
+ */
+ public NotNullableException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new <code>NotNullableException</code> from another
+ * exception.
+ *
+ * @param cause
+ * The cause of the failure
+ */
+ public NotNullableException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ public void commit() {
+ if (isModified()) {
+ modified = false;
+ value = changedValue;
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java
new file mode 100644
index 0000000000..adfd439ac8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+
+/**
+ * An OptimisticLockException is thrown when trying to update or delete a row
+ * that has been changed since last read from the database.
+ *
+ * OptimisticLockException is a runtime exception because optimistic locking is
+ * turned off by default, and as such will never be thrown in a default
+ * configuration. In order to turn on optimistic locking, you need to specify
+ * the version column in your TableQuery instance.
+ *
+ * @see TableQuery#setVersionColumn(String)
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+public class OptimisticLockException extends RuntimeException {
+
+ private final RowId rowId;
+
+ public OptimisticLockException(RowId rowId) {
+ super();
+ this.rowId = rowId;
+ }
+
+ public OptimisticLockException(String msg, RowId rowId) {
+ super(msg);
+ this.rowId = rowId;
+ }
+
+ public RowId getRowId() {
+ return rowId;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java
new file mode 100644
index 0000000000..c73ffce63a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+public class ReadOnlyRowId extends RowId {
+ private static final long serialVersionUID = -2626764781642012467L;
+ private final Integer rowNum;
+
+ public ReadOnlyRowId(int rowNum) {
+ super();
+ this.rowNum = rowNum;
+ }
+
+ @Override
+ public int hashCode() {
+ return rowNum.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof ReadOnlyRowId)) {
+ return false;
+ }
+ return rowNum.equals(((ReadOnlyRowId) obj).rowNum);
+ }
+
+ public int getRowNum() {
+ return rowNum;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/Reference.java b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java
new file mode 100644
index 0000000000..dea1aa87c0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/Reference.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+/**
+ * The reference class represents a simple [usually foreign key] reference to
+ * another SQLContainer. Actual foreign key reference in the database is not
+ * required, but it is recommended to make sure that certain constraints are
+ * followed.
+ */
+@SuppressWarnings("serial")
+class Reference implements Serializable {
+
+ /**
+ * The SQLContainer that this reference points to.
+ */
+ private SQLContainer referencedContainer;
+
+ /**
+ * The column ID/name in the referencing SQLContainer that contains the key
+ * used for the reference.
+ */
+ private String referencingColumn;
+
+ /**
+ * The column ID/name in the referenced SQLContainer that contains the key
+ * used for the reference.
+ */
+ private String referencedColumn;
+
+ /**
+ * Constructs a new reference to be used within the SQLContainer to
+ * reference another SQLContainer.
+ */
+ Reference(SQLContainer referencedContainer, String referencingColumn,
+ String referencedColumn) {
+ this.referencedContainer = referencedContainer;
+ this.referencingColumn = referencingColumn;
+ this.referencedColumn = referencedColumn;
+ }
+
+ SQLContainer getReferencedContainer() {
+ return referencedContainer;
+ }
+
+ String getReferencingColumn() {
+ return referencingColumn;
+ }
+
+ String getReferencedColumn() {
+ return referencedColumn;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowId.java b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java
new file mode 100644
index 0000000000..925325134a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/RowId.java
@@ -0,0 +1,81 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+/**
+ * RowId represents identifiers of a single database result set row.
+ *
+ * The data structure of a RowId is an Object array which contains the values of
+ * the primary key columns of the identified row. This allows easy equals()
+ * -comparison of RowItems.
+ */
+public class RowId implements Serializable {
+ private static final long serialVersionUID = -3161778404698901258L;
+ protected Object[] id;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ protected RowId() {
+ }
+
+ public RowId(Object[] id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id parameter must not be null!");
+ }
+ this.id = id;
+ }
+
+ public Object[] getId() {
+ return id;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 31;
+ if (id != null) {
+ for (Object o : id) {
+ if (o != null) {
+ result += o.hashCode();
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof RowId)) {
+ return false;
+ }
+ Object[] compId = ((RowId) obj).getId();
+ if (id == null && compId == null) {
+ return true;
+ }
+ if (id.length != compId.length) {
+ return false;
+ }
+ for (int i = 0; i < id.length; i++) {
+ if ((id[i] == null && compId[i] != null)
+ || (id[i] != null && !id[i].equals(compId[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer s = new StringBuffer();
+ for (int i = 0; i < id.length; i++) {
+ s.append(id[i]);
+ if (i < id.length - 1) {
+ s.append("/");
+ }
+ }
+ return s.toString();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java
new file mode 100644
index 0000000000..d613a06b63
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/RowItem.java
@@ -0,0 +1,133 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * RowItem represents one row of a result set obtained from a QueryDelegate.
+ *
+ * Note that depending on the QueryDelegate in use this does not necessarily map
+ * into an actual row in a database table.
+ */
+public final class RowItem implements Item {
+ private static final long serialVersionUID = -6228966439127951408L;
+ private SQLContainer container;
+ private RowId id;
+ private Collection<ColumnProperty> properties;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private RowItem() {
+ }
+
+ public RowItem(SQLContainer container, RowId id,
+ Collection<ColumnProperty> properties) {
+ if (container == null) {
+ throw new IllegalArgumentException("Container cannot be null.");
+ }
+ if (id == null) {
+ throw new IllegalArgumentException("Row ID cannot be null.");
+ }
+ this.container = container;
+ this.properties = properties;
+ /* Set this RowItem as owner to the properties */
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ p.setOwner(this);
+ }
+ }
+ this.id = id;
+ }
+
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ if (id instanceof String && id != null) {
+ for (ColumnProperty cp : properties) {
+ if (id.equals(cp.getPropertyId())) {
+ return cp;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ Collection<String> ids = new ArrayList<String>(properties.size());
+ for (ColumnProperty cp : properties) {
+ ids.add(cp.getPropertyId());
+ }
+ return Collections.unmodifiableCollection(ids);
+ }
+
+ /**
+ * Adding properties is not supported. Properties are generated by
+ * SQLContainer.
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Removing properties is not supported. Properties are generated by
+ * SQLContainer.
+ */
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ public RowId getId() {
+ return id;
+ }
+
+ public SQLContainer getContainer() {
+ return container;
+ }
+
+ public boolean isModified() {
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ if (p.isModified()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer s = new StringBuffer();
+ s.append("ID:");
+ s.append(getId().toString());
+ for (Object propId : getItemPropertyIds()) {
+ s.append("|");
+ s.append(propId.toString());
+ s.append(":");
+ Object value = getItemProperty(propId).getValue();
+ s.append((null != value) ? value.toString() : null);
+ }
+ return s.toString();
+ }
+
+ public void commit() {
+ if (properties != null) {
+ for (ColumnProperty p : properties) {
+ p.commit();
+ }
+ }
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java
new file mode 100644
index 0000000000..5827390723
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java
@@ -0,0 +1,1716 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate;
+import com.vaadin.data.util.sqlcontainer.query.QueryDelegate.RowIdChangeListener;
+import com.vaadin.data.util.sqlcontainer.query.TableQuery;
+import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator;
+
+public class SQLContainer implements Container, Container.Filterable,
+ Container.Indexed, Container.Sortable, Container.ItemSetChangeNotifier {
+
+ /** Query delegate */
+ private QueryDelegate delegate;
+ /** Auto commit mode, default = false */
+ private boolean autoCommit = false;
+
+ /** Page length = number of items contained in one page */
+ private int pageLength = DEFAULT_PAGE_LENGTH;
+ public static final int DEFAULT_PAGE_LENGTH = 100;
+
+ /** Number of items to cache = CACHE_RATIO x pageLength */
+ public static final int CACHE_RATIO = 2;
+
+ /** Item and index caches */
+ private final Map<Integer, RowId> itemIndexes = new HashMap<Integer, RowId>();
+ private final CacheMap<RowId, RowItem> cachedItems = new CacheMap<RowId, RowItem>();
+
+ /** Container properties = column names, data types and statuses */
+ private final List<String> propertyIds = new ArrayList<String>();
+ private final Map<String, Class<?>> propertyTypes = new HashMap<String, Class<?>>();
+ private final Map<String, Boolean> propertyReadOnly = new HashMap<String, Boolean>();
+ private final Map<String, Boolean> propertyNullable = new HashMap<String, Boolean>();
+
+ /** Filters (WHERE) and sorters (ORDER BY) */
+ private final List<Filter> filters = new ArrayList<Filter>();
+ private final List<OrderBy> sorters = new ArrayList<OrderBy>();
+
+ /**
+ * Total number of items available in the data source using the current
+ * query, filters and sorters.
+ */
+ private int size;
+
+ /**
+ * Size updating logic. Do not update size from data source if it has been
+ * updated in the last sizeValidMilliSeconds milliseconds.
+ */
+ private final int sizeValidMilliSeconds = 10000;
+ private boolean sizeDirty = true;
+ private Date sizeUpdated = new Date();
+
+ /** Starting row number of the currently fetched page */
+ private int currentOffset;
+
+ /** ItemSetChangeListeners */
+ private LinkedList<Container.ItemSetChangeListener> itemSetChangeListeners;
+
+ /** Temporary storage for modified items and items to be removed and added */
+ private final Map<RowId, RowItem> removedItems = new HashMap<RowId, RowItem>();
+ private final List<RowItem> addedItems = new ArrayList<RowItem>();
+ private final List<RowItem> modifiedItems = new ArrayList<RowItem>();
+
+ /** List of references to other SQLContainers */
+ private final Map<SQLContainer, Reference> references = new HashMap<SQLContainer, Reference>();
+
+ /** Cache flush notification system enabled. Disabled by default. */
+ private boolean notificationsEnabled;
+
+ /**
+ * Prevent instantiation without a QueryDelegate.
+ */
+ @SuppressWarnings("unused")
+ private SQLContainer() {
+ }
+
+ /**
+ * Creates and initializes SQLContainer using the given QueryDelegate
+ *
+ * @param delegate
+ * QueryDelegate implementation
+ * @throws SQLException
+ */
+ public SQLContainer(QueryDelegate delegate) throws SQLException {
+ if (delegate == null) {
+ throw new IllegalArgumentException(
+ "QueryDelegate must not be null.");
+ }
+ this.delegate = delegate;
+ getPropertyIds();
+ cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+ }
+
+ /**************************************/
+ /** Methods from interface Container **/
+ /**************************************/
+
+ /**
+ * Note! If auto commit mode is enabled, this method will still return the
+ * temporary row ID assigned for the item. Implement
+ * QueryDelegate.RowIdChangeListener to receive the actual Row ID value
+ * after the addition has been committed.
+ *
+ * {@inheritDoc}
+ */
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ Object emptyKey[] = new Object[delegate.getPrimaryKeyColumns().size()];
+ RowId itemId = new TemporaryRowId(emptyKey);
+ // Create new empty column properties for the row item.
+ List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+ for (String propertyId : propertyIds) {
+ /* Default settings for new item properties. */
+ itemProperties
+ .add(new ColumnProperty(propertyId, propertyReadOnly
+ .get(propertyId),
+ !propertyReadOnly.get(propertyId), propertyNullable
+ .get(propertyId), null, getType(propertyId)));
+ }
+ RowItem newRowItem = new RowItem(this, itemId, itemProperties);
+
+ if (autoCommit) {
+ /* Add and commit instantly */
+ try {
+ if (delegate instanceof TableQuery) {
+ itemId = ((TableQuery) delegate)
+ .storeRowImmediately(newRowItem);
+ } else {
+ delegate.beginTransaction();
+ delegate.storeRow(newRowItem);
+ delegate.commit();
+ }
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ getLogger().log(Level.FINER, "Row added to DB...");
+ return itemId;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to add row to DB. Rolling back.", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ getLogger().log(Level.SEVERE,
+ "Failed to roll back row addition", e);
+ }
+ return null;
+ }
+ } else {
+ addedItems.add(newRowItem);
+ fireContentsChange();
+ return itemId;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#containsId(java.lang.Object)
+ */
+
+ @Override
+ public boolean containsId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ if (cachedItems.containsKey(itemId)) {
+ return true;
+ } else {
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ return itemPassesFilters(item);
+ }
+ }
+ }
+ if (removedItems.containsKey(itemId)) {
+ return false;
+ }
+
+ if (itemId instanceof ReadOnlyRowId) {
+ int rowNum = ((ReadOnlyRowId) itemId).getRowNum();
+ return rowNum >= 0 && rowNum < size;
+ }
+
+ if (itemId instanceof RowId && !(itemId instanceof TemporaryRowId)) {
+ try {
+ return delegate.containsRowWithKey(((RowId) itemId).getId());
+ } catch (Exception e) {
+ /* Query failed, just return false. */
+ getLogger().log(Level.WARNING, "containsId query failed", e);
+ }
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ Item item = getItem(itemId);
+ if (item == null) {
+ return null;
+ }
+ return item.getItemProperty(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getItem(java.lang.Object)
+ */
+
+ @Override
+ public Item getItem(Object itemId) {
+ if (!cachedItems.containsKey(itemId)) {
+ int index = indexOfId(itemId);
+ if (index >= size) {
+ // The index is in the added items
+ int offset = index - size;
+ RowItem item = addedItems.get(offset);
+ if (itemPassesFilters(item)) {
+ return item;
+ } else {
+ return null;
+ }
+ } else {
+ // load the item into cache
+ updateOffsetAndCache(index);
+ }
+ }
+ return cachedItems.get(itemId);
+ }
+
+ /**
+ * Bypasses in-memory filtering to return items that are cached in memory.
+ * <em>NOTE</em>: This does not bypass database-level filtering.
+ *
+ * @param itemId
+ * the id of the item to retrieve.
+ * @return the item represented by itemId.
+ */
+ public Item getItemUnfiltered(Object itemId) {
+ if (!cachedItems.containsKey(itemId)) {
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ return item;
+ }
+ }
+ }
+ return cachedItems.get(itemId);
+ }
+
+ /**
+ * NOTE! Do not use this method if in any way avoidable. This method doesn't
+ * (and cannot) use lazy loading, which means that all rows in the database
+ * will be loaded into memory.
+ *
+ * {@inheritDoc}
+ */
+
+ @Override
+ public Collection<?> getItemIds() {
+ updateCount();
+ ArrayList<RowId> ids = new ArrayList<RowId>();
+ ResultSet rs = null;
+ try {
+ // Load ALL rows :(
+ delegate.beginTransaction();
+ rs = delegate.getResults(0, 0);
+ List<String> pKeys = delegate.getPrimaryKeyColumns();
+ while (rs.next()) {
+ RowId id = null;
+ if (pKeys.isEmpty()) {
+ /* Create a read only itemId */
+ id = new ReadOnlyRowId(rs.getRow());
+ } else {
+ /* Generate itemId for the row based on primary key(s) */
+ Object[] itemId = new Object[pKeys.size()];
+ for (int i = 0; i < pKeys.size(); i++) {
+ itemId[i] = rs.getObject(pKeys.get(i));
+ }
+ id = new RowId(itemId);
+ }
+ if (id != null && !removedItems.containsKey(id)) {
+ ids.add(id);
+ }
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "getItemIds() failed, rolling back.", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back state", e1);
+ }
+ try {
+ rs.getStatement().close();
+ rs.close();
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Closing session failed", e1);
+ }
+ throw new RuntimeException("Failed to fetch item indexes.", e);
+ }
+ for (RowItem item : getFilteredAddedItems()) {
+ ids.add(item.getId());
+ }
+ return Collections.unmodifiableCollection(ids);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ if (!propertyIds.contains(propertyId)) {
+ return null;
+ }
+ return propertyTypes.get(propertyId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#size()
+ */
+
+ @Override
+ public int size() {
+ updateCount();
+ return size + sizeOfAddedItems() - removedItems.size();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ if (!containsId(itemId)) {
+ return false;
+ }
+ for (RowItem item : addedItems) {
+ if (item.getId().equals(itemId)) {
+ addedItems.remove(item);
+ fireContentsChange();
+ return true;
+ }
+ }
+
+ if (autoCommit) {
+ /* Remove and commit instantly. */
+ Item i = getItem(itemId);
+ if (i == null) {
+ return false;
+ }
+ try {
+ delegate.beginTransaction();
+ boolean success = delegate.removeRow((RowItem) i);
+ delegate.commit();
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ if (success) {
+ getLogger().log(Level.FINER, "Row removed from DB...");
+ }
+ return success;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to remove row, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE,
+ "Failed to rollback row removal", ee);
+ }
+ return false;
+ } catch (OptimisticLockException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to remove row, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE,
+ "Failed to rollback row removal", ee);
+ }
+ throw e;
+ }
+ } else {
+ removedItems.put((RowId) itemId, (RowItem) getItem(itemId));
+ cachedItems.remove(itemId);
+ refresh();
+ return true;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ if (autoCommit) {
+ /* Remove and commit instantly. */
+ try {
+ delegate.beginTransaction();
+ boolean success = true;
+ for (Object id : getItemIds()) {
+ if (!delegate.removeRow((RowItem) getItem(id))) {
+ success = false;
+ }
+ }
+ if (success) {
+ delegate.commit();
+ getLogger().log(Level.FINER, "All rows removed from DB...");
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ } else {
+ delegate.rollback();
+ }
+ return success;
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "removeAllItems() failed, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Failed to roll back", ee);
+ }
+ return false;
+ } catch (OptimisticLockException e) {
+ getLogger().log(Level.WARNING,
+ "removeAllItems() failed, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Failed to roll back", ee);
+ }
+ throw e;
+ }
+ } else {
+ for (Object id : getItemIds()) {
+ removedItems.put((RowId) id, (RowItem) getItem(id));
+ cachedItems.remove(id);
+ }
+ refresh();
+ return true;
+ }
+ }
+
+ /*************************************************/
+ /** Methods from interface Container.Filterable **/
+ /*************************************************/
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void addContainerFilter(Filter filter)
+ throws UnsupportedFilterException {
+ // filter.setCaseSensitive(!ignoreCase);
+
+ filters.add(filter);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void removeContainerFilter(Filter filter) {
+ filters.remove(filter);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void addContainerFilter(Object propertyId, String filterString,
+ boolean ignoreCase, boolean onlyMatchPrefix) {
+ if (propertyId == null || !propertyIds.contains(propertyId)) {
+ return;
+ }
+
+ /* Generate Filter -object */
+ String likeStr = onlyMatchPrefix ? filterString + "%" : "%"
+ + filterString + "%";
+ Like like = new Like(propertyId.toString(), likeStr);
+ like.setCaseSensitive(!ignoreCase);
+ filters.add(like);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void removeContainerFilters(Object propertyId) {
+ ArrayList<Filter> toRemove = new ArrayList<Filter>();
+ for (Filter f : filters) {
+ if (f.appliesToProperty(propertyId)) {
+ toRemove.add(f);
+ }
+ }
+ filters.removeAll(toRemove);
+ refresh();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void removeAllContainerFilters() {
+ filters.clear();
+ refresh();
+ }
+
+ /**********************************************/
+ /** Methods from interface Container.Indexed **/
+ /**********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#indexOfId(java.lang.Object)
+ */
+
+ @Override
+ public int indexOfId(Object itemId) {
+ // First check if the id is in the added items
+ for (int ix = 0; ix < addedItems.size(); ix++) {
+ RowItem item = addedItems.get(ix);
+ if (item.getId().equals(itemId)) {
+ if (itemPassesFilters(item)) {
+ updateCount();
+ return size + ix;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ if (!containsId(itemId)) {
+ return -1;
+ }
+ if (cachedItems.isEmpty()) {
+ getPage();
+ }
+ int size = size();
+ boolean wrappedAround = false;
+ while (!wrappedAround) {
+ for (Integer i : itemIndexes.keySet()) {
+ if (itemIndexes.get(i).equals(itemId)) {
+ return i;
+ }
+ }
+ // load in the next page.
+ int nextIndex = (currentOffset / (pageLength * CACHE_RATIO) + 1)
+ * (pageLength * CACHE_RATIO);
+ if (nextIndex >= size) {
+ // Container wrapped around, start from index 0.
+ wrappedAround = true;
+ nextIndex = 0;
+ }
+ updateOffsetAndCache(nextIndex);
+ }
+ return -1;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#getIdByIndex(int)
+ */
+
+ @Override
+ public Object getIdByIndex(int index) {
+ if (index < 0 || index > size() - 1) {
+ return null;
+ }
+ if (index < size) {
+ if (itemIndexes.keySet().contains(index)) {
+ return itemIndexes.get(index);
+ }
+ updateOffsetAndCache(index);
+ return itemIndexes.get(index);
+ } else {
+ // The index is in the added items
+ int offset = index - size;
+ return addedItems.get(offset).getId();
+ }
+ }
+
+ /**********************************************/
+ /** Methods from interface Container.Ordered **/
+ /**********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return getIdByIndex(indexOfId(itemId) + 1);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return getIdByIndex(indexOfId(itemId) - 1);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#firstItemId()
+ */
+
+ @Override
+ public Object firstItemId() {
+ updateCount();
+ if (size == 0) {
+ if (addedItems.isEmpty()) {
+ return null;
+ } else {
+ int ix = -1;
+ do {
+ ix++;
+ } while (!itemPassesFilters(addedItems.get(ix))
+ && ix < addedItems.size());
+ if (ix < addedItems.size()) {
+ return addedItems.get(ix).getId();
+ }
+ }
+ }
+ if (!itemIndexes.containsKey(0)) {
+ updateOffsetAndCache(0);
+ }
+ return itemIndexes.get(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#lastItemId()
+ */
+
+ @Override
+ public Object lastItemId() {
+ if (addedItems.isEmpty()) {
+ int lastIx = size() - 1;
+ if (!itemIndexes.containsKey(lastIx)) {
+ updateOffsetAndCache(size - 1);
+ }
+ return itemIndexes.get(lastIx);
+ } else {
+ int ix = addedItems.size();
+ do {
+ ix--;
+ } while (!itemPassesFilters(addedItems.get(ix)) && ix >= 0);
+ if (ix >= 0) {
+ return addedItems.get(ix).getId();
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return firstItemId().equals(itemId);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return lastItemId().equals(itemId);
+ }
+
+ /***********************************************/
+ /** Methods from interface Container.Sortable **/
+ /***********************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ sorters.clear();
+ if (propertyId == null || propertyId.length == 0) {
+ refresh();
+ return;
+ }
+ /* Generate OrderBy -objects */
+ boolean asc = true;
+ for (int i = 0; i < propertyId.length; i++) {
+ /* Check that the property id is valid */
+ if (propertyId[i] instanceof String
+ && propertyIds.contains(propertyId[i])) {
+ try {
+ asc = ascending[i];
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "", e);
+ }
+ sorters.add(new OrderBy((String) propertyId[i], asc));
+ }
+ }
+ refresh();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ return getContainerPropertyIds();
+ }
+
+ /**************************************/
+ /** Methods specific to SQLContainer **/
+ /**************************************/
+
+ /**
+ * Refreshes the container - clears all caches and resets size and offset.
+ * Does NOT remove sorting or filtering rules!
+ */
+ public void refresh() {
+ sizeDirty = true;
+ currentOffset = 0;
+ cachedItems.clear();
+ itemIndexes.clear();
+ fireContentsChange();
+ }
+
+ /**
+ * Returns modify state of the container.
+ *
+ * @return true if contents of this container have been modified
+ */
+ public boolean isModified() {
+ return !removedItems.isEmpty() || !addedItems.isEmpty()
+ || !modifiedItems.isEmpty();
+ }
+
+ /**
+ * Set auto commit mode enabled or disabled. Auto commit mode means that all
+ * changes made to items of this container will be immediately written to
+ * the underlying data source.
+ *
+ * @param autoCommitEnabled
+ * true to enable auto commit mode
+ */
+ public void setAutoCommit(boolean autoCommitEnabled) {
+ autoCommit = autoCommitEnabled;
+ }
+
+ /**
+ * Returns status of the auto commit mode.
+ *
+ * @return true if auto commit mode is enabled
+ */
+ public boolean isAutoCommit() {
+ return autoCommit;
+ }
+
+ /**
+ * Returns the currently set page length.
+ *
+ * @return current page length
+ */
+ public int getPageLength() {
+ return pageLength;
+ }
+
+ /**
+ * Sets the page length used in lazy fetching of items from the data source.
+ * Also resets the cache size to match the new page length.
+ *
+ * As a side effect the container will be refreshed.
+ *
+ * @param pageLength
+ * new page length
+ */
+ public void setPageLength(int pageLength) {
+ setPageLengthInternal(pageLength);
+ refresh();
+ }
+
+ /**
+ * Sets the page length internally, without refreshing the container.
+ *
+ * @param pageLength
+ * the new page length
+ */
+ private void setPageLengthInternal(int pageLength) {
+ this.pageLength = pageLength > 0 ? pageLength : DEFAULT_PAGE_LENGTH;
+ cachedItems.setCacheLimit(CACHE_RATIO * getPageLength());
+ }
+
+ /**
+ * Adds the given OrderBy to this container and refreshes the container
+ * contents with the new sorting rules.
+ *
+ * Note that orderBy.getColumn() must return a column name that exists in
+ * this container.
+ *
+ * @param orderBy
+ * OrderBy to be added to the container sorting rules
+ */
+ public void addOrderBy(OrderBy orderBy) {
+ if (orderBy == null) {
+ return;
+ }
+ if (!propertyIds.contains(orderBy.getColumn())) {
+ throw new IllegalArgumentException(
+ "The column given for sorting does not exist in this container.");
+ }
+ sorters.add(orderBy);
+ refresh();
+ }
+
+ /**
+ * Commits all the changes, additions and removals made to the items of this
+ * container.
+ *
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public void commit() throws UnsupportedOperationException, SQLException {
+ try {
+ getLogger().log(Level.FINER,
+ "Commiting changes through delegate...");
+ delegate.beginTransaction();
+ /* Perform buffered deletions */
+ for (RowItem item : removedItems.values()) {
+ if (!delegate.removeRow(item)) {
+ throw new SQLException("Removal failed for row with ID: "
+ + item.getId());
+ }
+ }
+ /* Perform buffered modifications */
+ for (RowItem item : modifiedItems) {
+ if (delegate.storeRow(item) > 0) {
+ /*
+ * Also reset the modified state in the item in case it is
+ * reused e.g. in a form.
+ */
+ item.commit();
+ } else {
+ delegate.rollback();
+ refresh();
+ throw new ConcurrentModificationException(
+ "Item with the ID '" + item.getId()
+ + "' has been externally modified.");
+ }
+ }
+ /* Perform buffered additions */
+ for (RowItem item : addedItems) {
+ delegate.storeRow(item);
+ }
+ delegate.commit();
+ removedItems.clear();
+ addedItems.clear();
+ modifiedItems.clear();
+ refresh();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ } catch (SQLException e) {
+ delegate.rollback();
+ throw e;
+ } catch (OptimisticLockException e) {
+ delegate.rollback();
+ throw e;
+ }
+ }
+
+ /**
+ * Rolls back all the changes, additions and removals made to the items of
+ * this container.
+ *
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public void rollback() throws UnsupportedOperationException, SQLException {
+ getLogger().log(Level.FINE, "Rolling back changes...");
+ removedItems.clear();
+ addedItems.clear();
+ modifiedItems.clear();
+ refresh();
+ }
+
+ /**
+ * Notifies this container that a property in the given item has been
+ * modified. The change will be buffered or made instantaneously depending
+ * on auto commit mode.
+ *
+ * @param changedItem
+ * item that has a modified property
+ */
+ void itemChangeNotification(RowItem changedItem) {
+ if (autoCommit) {
+ try {
+ delegate.beginTransaction();
+ if (delegate.storeRow(changedItem) == 0) {
+ delegate.rollback();
+ refresh();
+ throw new ConcurrentModificationException(
+ "Item with the ID '" + changedItem.getId()
+ + "' has been externally modified.");
+ }
+ delegate.commit();
+ if (notificationsEnabled) {
+ CacheFlushNotifier.notifyOfCacheFlush(this);
+ }
+ getLogger().log(Level.FINER, "Row updated to DB...");
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "itemChangeNotification failed, rolling back...", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException ee) {
+ /* Nothing can be done here */
+ getLogger().log(Level.SEVERE, "Rollback failed", e);
+ }
+ throw new RuntimeException(e);
+ }
+ } else {
+ if (!(changedItem.getId() instanceof TemporaryRowId)
+ && !modifiedItems.contains(changedItem)) {
+ modifiedItems.add(changedItem);
+ }
+ }
+ }
+
+ /**
+ * Determines a new offset for updating the row cache. The offset is
+ * calculated from the given index, and will be fixed to match the start of
+ * a page, based on the value of pageLength.
+ *
+ * @param index
+ * Index of the item that was requested, but not found in cache
+ */
+ private void updateOffsetAndCache(int index) {
+ if (itemIndexes.containsKey(index)) {
+ return;
+ }
+ currentOffset = (index / (pageLength * CACHE_RATIO))
+ * (pageLength * CACHE_RATIO);
+ if (currentOffset < 0) {
+ currentOffset = 0;
+ }
+ getPage();
+ }
+
+ /**
+ * Fetches new count of rows from the data source, if needed.
+ */
+ private void updateCount() {
+ if (!sizeDirty
+ && new Date().getTime() < sizeUpdated.getTime()
+ + sizeValidMilliSeconds) {
+ return;
+ }
+ try {
+ try {
+ delegate.setFilters(filters);
+ } catch (UnsupportedOperationException e) {
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support filtering", e);
+ }
+ try {
+ delegate.setOrderBy(sorters);
+ } catch (UnsupportedOperationException e) {
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support filtering", e);
+ }
+ int newSize = delegate.getCount();
+ if (newSize != size) {
+ size = newSize;
+ refresh();
+ }
+ sizeUpdated = new Date();
+ sizeDirty = false;
+ getLogger().log(Level.FINER,
+ "Updated row count. New count is: " + size);
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to update item set size.", e);
+ }
+ }
+
+ /**
+ * Fetches property id's (column names and their types) from the data
+ * source.
+ *
+ * @throws SQLException
+ */
+ private void getPropertyIds() throws SQLException {
+ propertyIds.clear();
+ propertyTypes.clear();
+ delegate.setFilters(null);
+ delegate.setOrderBy(null);
+ ResultSet rs = null;
+ ResultSetMetaData rsmd = null;
+ try {
+ delegate.beginTransaction();
+ rs = delegate.getResults(0, 1);
+ boolean resultExists = rs.next();
+ rsmd = rs.getMetaData();
+ Class<?> type = null;
+ for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+ if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+ continue;
+ }
+ String colName = rsmd.getColumnLabel(i);
+ /*
+ * Make sure not to add the same colName twice. This can easily
+ * happen if the SQL query joins many tables with an ID column.
+ */
+ if (!propertyIds.contains(colName)) {
+ propertyIds.add(colName);
+ }
+ /* Try to determine the column's JDBC class by all means. */
+ if (resultExists && rs.getObject(i) != null) {
+ type = rs.getObject(i).getClass();
+ } else {
+ try {
+ type = Class.forName(rsmd.getColumnClassName(i));
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "Class not found", e);
+ /* On failure revert to Object and hope for the best. */
+ type = Object.class;
+ }
+ }
+ /*
+ * Determine read only and nullability status of the column. A
+ * column is read only if it is reported as either read only or
+ * auto increment by the database, and also it is set as the
+ * version column in a TableQuery delegate.
+ */
+ boolean readOnly = rsmd.isAutoIncrement(i)
+ || rsmd.isReadOnly(i);
+ if (delegate instanceof TableQuery
+ && rsmd.getColumnLabel(i).equals(
+ ((TableQuery) delegate).getVersionColumn())) {
+ readOnly = true;
+ }
+ propertyReadOnly.put(colName, readOnly);
+ propertyNullable.put(colName,
+ rsmd.isNullable(i) == ResultSetMetaData.columnNullable);
+ propertyTypes.put(colName, type);
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ getLogger().log(Level.FINER, "Property IDs fetched.");
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to fetch property ids, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back", e1);
+ }
+ try {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ }
+ rs.close();
+ }
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Failed to close session", e1);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Fetches a page from the data source based on the values of pageLenght and
+ * currentOffset. Also updates the set of primary keys, used in
+ * identification of RowItems.
+ */
+ private void getPage() {
+ updateCount();
+ ResultSet rs = null;
+ ResultSetMetaData rsmd = null;
+ cachedItems.clear();
+ itemIndexes.clear();
+ try {
+ try {
+ delegate.setOrderBy(sorters);
+ } catch (UnsupportedOperationException e) {
+ /* The query delegate doesn't support sorting. */
+ /* No need to do anything. */
+ getLogger().log(Level.FINE,
+ "The query delegate doesn't support sorting", e);
+ }
+ delegate.beginTransaction();
+ rs = delegate.getResults(currentOffset, pageLength * CACHE_RATIO);
+ rsmd = rs.getMetaData();
+ List<String> pKeys = delegate.getPrimaryKeyColumns();
+ // }
+ /* Create new items and column properties */
+ ColumnProperty cp = null;
+ int rowCount = currentOffset;
+ if (!delegate.implementationRespectsPagingLimits()) {
+ rowCount = currentOffset = 0;
+ setPageLengthInternal(size);
+ }
+ while (rs.next()) {
+ List<ColumnProperty> itemProperties = new ArrayList<ColumnProperty>();
+ /* Generate row itemId based on primary key(s) */
+ Object[] itemId = new Object[pKeys.size()];
+ for (int i = 0; i < pKeys.size(); i++) {
+ itemId[i] = rs.getObject(pKeys.get(i));
+ }
+ RowId id = null;
+ if (pKeys.isEmpty()) {
+ id = new ReadOnlyRowId(rs.getRow());
+ } else {
+ id = new RowId(itemId);
+ }
+ List<String> propertiesToAdd = new ArrayList<String>(
+ propertyIds);
+ if (!removedItems.containsKey(id)) {
+ for (int i = 1; i <= rsmd.getColumnCount(); i++) {
+ if (!isColumnIdentifierValid(rsmd.getColumnLabel(i))) {
+ continue;
+ }
+ String colName = rsmd.getColumnLabel(i);
+ Object value = rs.getObject(i);
+ Class<?> type = value != null ? value.getClass()
+ : Object.class;
+ if (value == null) {
+ for (String propName : propertyTypes.keySet()) {
+ if (propName.equals(rsmd.getColumnLabel(i))) {
+ type = propertyTypes.get(propName);
+ break;
+ }
+ }
+ }
+ /*
+ * In case there are more than one column with the same
+ * name, add only the first one. This can easily happen
+ * if you join many tables where each table has an ID
+ * column.
+ */
+ if (propertiesToAdd.contains(colName)) {
+ cp = new ColumnProperty(colName,
+ propertyReadOnly.get(colName),
+ !propertyReadOnly.get(colName),
+ propertyNullable.get(colName), value, type);
+ itemProperties.add(cp);
+ propertiesToAdd.remove(colName);
+ }
+ }
+ /* Cache item */
+ itemIndexes.put(rowCount, id);
+
+ // if an item with the id is contained in the modified
+ // cache, then use this record and add it to the cached
+ // items. Otherwise create a new item
+ int modifiedIndex = indexInModifiedCache(id);
+ if (modifiedIndex != -1) {
+ cachedItems.put(id, modifiedItems.get(modifiedIndex));
+ } else {
+ cachedItems.put(id, new RowItem(this, id,
+ itemProperties));
+ }
+
+ rowCount++;
+ }
+ }
+ rs.getStatement().close();
+ rs.close();
+ delegate.commit();
+ getLogger().log(
+ Level.FINER,
+ "Fetched " + pageLength * CACHE_RATIO
+ + " rows starting from " + currentOffset);
+ } catch (SQLException e) {
+ getLogger().log(Level.WARNING,
+ "Failed to fetch rows, rolling back", e);
+ try {
+ delegate.rollback();
+ } catch (SQLException e1) {
+ getLogger().log(Level.SEVERE, "Failed to roll back", e1);
+ }
+ try {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ rs.close();
+ }
+ }
+ } catch (SQLException e1) {
+ getLogger().log(Level.WARNING, "Failed to close session", e1);
+ }
+ throw new RuntimeException("Failed to fetch page.", e);
+ }
+ }
+
+ /**
+ * Returns the index of the item with the given itemId for the modified
+ * cache.
+ *
+ * @param itemId
+ * @return the index of the item with the itemId in the modified cache. Or
+ * -1 if not found.
+ */
+ private int indexInModifiedCache(Object itemId) {
+ for (int ix = 0; ix < modifiedItems.size(); ix++) {
+ RowItem item = modifiedItems.get(ix);
+ if (item.getId().equals(itemId)) {
+ return ix;
+ }
+ }
+ return -1;
+ }
+
+ private int sizeOfAddedItems() {
+ return getFilteredAddedItems().size();
+ }
+
+ private List<RowItem> getFilteredAddedItems() {
+ ArrayList<RowItem> filtered = new ArrayList<RowItem>(addedItems);
+ if (filters != null && !filters.isEmpty()) {
+ for (RowItem item : addedItems) {
+ if (!itemPassesFilters(item)) {
+ filtered.remove(item);
+ }
+ }
+ }
+ return filtered;
+ }
+
+ private boolean itemPassesFilters(RowItem item) {
+ for (Filter filter : filters) {
+ if (!filter.passesFilter(item.getId(), item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks is the given column identifier valid to be used with SQLContainer.
+ * Currently the only non-valid identifier is "rownum" when MSSQL or Oracle
+ * is used. This is due to the way the SELECT queries are constructed in
+ * order to implement paging in these databases.
+ *
+ * @param identifier
+ * Column identifier
+ * @return true if the identifier is valid
+ */
+ private boolean isColumnIdentifierValid(String identifier) {
+ if (identifier.equalsIgnoreCase("rownum")
+ && delegate instanceof TableQuery) {
+ TableQuery tq = (TableQuery) delegate;
+ if (tq.getSqlGenerator() instanceof MSSQLGenerator
+ || tq.getSqlGenerator() instanceof OracleGenerator) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the QueryDelegate set for this SQLContainer.
+ *
+ * @return current querydelegate
+ */
+ protected QueryDelegate getQueryDelegate() {
+ return delegate;
+ }
+
+ /************************************/
+ /** UNSUPPORTED CONTAINER FEATURES **/
+ /************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
+ */
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int, java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAt(int index, Object newItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Indexed#addItemAt(int)
+ */
+
+ @Override
+ public Object addItemAt(int index) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /******************************************/
+ /** ITEMSETCHANGENOTIFIER IMPLEMENTATION **/
+ /******************************************/
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin
+ * .data.Container.ItemSetChangeListener)
+ */
+
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (itemSetChangeListeners == null) {
+ itemSetChangeListeners = new LinkedList<Container.ItemSetChangeListener>();
+ }
+ itemSetChangeListeners.add(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin
+ * .data.Container.ItemSetChangeListener)
+ */
+
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (itemSetChangeListeners != null) {
+ itemSetChangeListeners.remove(listener);
+ }
+ }
+
+ protected void fireContentsChange() {
+ if (itemSetChangeListeners != null) {
+ final Object[] l = itemSetChangeListeners.toArray();
+ final Container.ItemSetChangeEvent event = new SQLContainer.ItemSetChangeEvent(
+ this);
+ for (int i = 0; i < l.length; i++) {
+ ((Container.ItemSetChangeListener) l[i])
+ .containerItemSetChange(event);
+ }
+ }
+ }
+
+ /**
+ * Simple ItemSetChangeEvent implementation.
+ */
+ @SuppressWarnings("serial")
+ public static class ItemSetChangeEvent extends EventObject implements
+ Container.ItemSetChangeEvent {
+
+ private ItemSetChangeEvent(SQLContainer source) {
+ super(source);
+ }
+
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+ }
+
+ /**************************************************/
+ /** ROWIDCHANGELISTENER PASSING TO QUERYDELEGATE **/
+ /**************************************************/
+
+ /**
+ * Adds a RowIdChangeListener to the QueryDelegate
+ *
+ * @param listener
+ */
+ public void addListener(RowIdChangeListener listener) {
+ if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+ ((QueryDelegate.RowIdChangeNotifier) delegate)
+ .addListener(listener);
+ }
+ }
+
+ /**
+ * Removes a RowIdChangeListener from the QueryDelegate
+ *
+ * @param listener
+ */
+ public void removeListener(RowIdChangeListener listener) {
+ if (delegate instanceof QueryDelegate.RowIdChangeNotifier) {
+ ((QueryDelegate.RowIdChangeNotifier) delegate)
+ .removeListener(listener);
+ }
+ }
+
+ /**
+ * Calling this will enable this SQLContainer to send and receive cache
+ * flush notifications for its lifetime.
+ */
+ public void enableCacheFlushNotifications() {
+ if (!notificationsEnabled) {
+ notificationsEnabled = true;
+ CacheFlushNotifier.addInstance(this);
+ }
+ }
+
+ /******************************************/
+ /** Referencing mechanism implementation **/
+ /******************************************/
+
+ /**
+ * Adds a new reference to the given SQLContainer. In addition to the
+ * container you must provide the column (property) names used for the
+ * reference in both this and the referenced SQLContainer.
+ *
+ * Note that multiple references pointing to the same SQLContainer are not
+ * supported.
+ *
+ * @param refdCont
+ * Target SQLContainer of the new reference
+ * @param refingCol
+ * Column (property) name in this container storing the (foreign
+ * key) reference
+ * @param refdCol
+ * Column (property) name in the referenced container storing the
+ * referenced key
+ */
+ public void addReference(SQLContainer refdCont, String refingCol,
+ String refdCol) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ if (!getContainerPropertyIds().contains(refingCol)) {
+ throw new IllegalArgumentException(
+ "Given referencing column name is invalid."
+ + " Please ensure that this container"
+ + " contains a property ID named: " + refingCol);
+ }
+ if (!refdCont.getContainerPropertyIds().contains(refdCol)) {
+ throw new IllegalArgumentException(
+ "Given referenced column name is invalid."
+ + " Please ensure that the referenced container"
+ + " contains a property ID named: " + refdCol);
+ }
+ if (references.keySet().contains(refdCont)) {
+ throw new IllegalArgumentException(
+ "An SQLContainer instance can only be referenced once.");
+ }
+ references.put(refdCont, new Reference(refdCont, refingCol, refdCol));
+ }
+
+ /**
+ * Removes the reference pointing to the given SQLContainer.
+ *
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return true if successful, false if the reference did not exist
+ */
+ public boolean removeReference(SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ return references.remove(refdCont) == null ? false : true;
+ }
+
+ /**
+ * Sets the referenced item. The referencing column of the item in this
+ * container is updated accordingly.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdItemId
+ * Item Id of the reference target (from referenced container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return true if the referenced item was successfully set, false on
+ * failure
+ */
+ public boolean setReferencedItem(Object itemId, Object refdItemId,
+ SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ Reference r = references.get(refdCont);
+ if (r == null) {
+ throw new IllegalArgumentException(
+ "Reference to the given SQLContainer not defined.");
+ }
+ try {
+ getContainerProperty(itemId, r.getReferencingColumn()).setValue(
+ refdCont.getContainerProperty(refdItemId,
+ r.getReferencedColumn()));
+ return true;
+ } catch (Exception e) {
+ getLogger()
+ .log(Level.WARNING, "Setting referenced item failed.", e);
+ return false;
+ }
+ }
+
+ /**
+ * Fetches the Item Id of the referenced item from the target SQLContainer.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return Item Id of the referenced item, or null if not found
+ */
+ public Object getReferencedItemId(Object itemId, SQLContainer refdCont) {
+ if (refdCont == null) {
+ throw new IllegalArgumentException(
+ "Referenced SQLContainer can not be null.");
+ }
+ Reference r = references.get(refdCont);
+ if (r == null) {
+ throw new IllegalArgumentException(
+ "Reference to the given SQLContainer not defined.");
+ }
+ Object refKey = getContainerProperty(itemId, r.getReferencingColumn())
+ .getValue();
+
+ refdCont.removeAllContainerFilters();
+ refdCont.addContainerFilter(new Equal(r.getReferencedColumn(), refKey));
+ Object toReturn = refdCont.firstItemId();
+ refdCont.removeAllContainerFilters();
+ return toReturn;
+ }
+
+ /**
+ * Fetches the referenced item from the target SQLContainer.
+ *
+ * @param itemId
+ * Item Id of the reference source (from this container)
+ * @param refdCont
+ * Target SQLContainer of the reference
+ * @return The referenced item, or null if not found
+ */
+ public Item getReferencedItem(Object itemId, SQLContainer refdCont) {
+ return refdCont.getItem(getReferencedItemId(itemId, refdCont));
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ }
+
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ if (notificationsEnabled) {
+ /*
+ * Register instance with CacheFlushNotifier after de-serialization
+ * if notifications are enabled
+ */
+ CacheFlushNotifier.addInstance(this);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(SQLContainer.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java
new file mode 100644
index 0000000000..4a48dbf499
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+import java.io.Serializable;
+
+public class SQLUtil implements Serializable {
+ /**
+ * Escapes different special characters in strings that are passed to SQL.
+ * Replaces the following:
+ *
+ * <list> <li>' is replaced with ''</li> <li>\x00 is removed</li> <li>\ is
+ * replaced with \\</li> <li>" is replaced with \"</li> <li>
+ * \x1a is removed</li> </list>
+ *
+ * Also note! The escaping done here may or may not be enough to prevent any
+ * and all SQL injections so it is recommended to check user input before
+ * giving it to the SQLContainer/TableQuery.
+ *
+ * @param constant
+ * @return \\\'\'
+ */
+ public static String escapeSQL(String constant) {
+ if (constant == null) {
+ return null;
+ }
+ String fixedConstant = constant;
+ fixedConstant = fixedConstant.replaceAll("\\\\x00", "");
+ fixedConstant = fixedConstant.replaceAll("\\\\x1a", "");
+ fixedConstant = fixedConstant.replaceAll("'", "''");
+ fixedConstant = fixedConstant.replaceAll("\\\\", "\\\\\\\\");
+ fixedConstant = fixedConstant.replaceAll("\\\"", "\\\\\"");
+ return fixedConstant;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java
new file mode 100644
index 0000000000..b4bca75a2a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java
@@ -0,0 +1,32 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer;
+
+public class TemporaryRowId extends RowId {
+ private static final long serialVersionUID = -641983830469018329L;
+
+ public TemporaryRowId(Object[] id) {
+ super(id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || !(obj instanceof TemporaryRowId)) {
+ return false;
+ }
+ Object[] compId = ((TemporaryRowId) obj).getId();
+ return id.equals(compId);
+ }
+
+ @Override
+ public String toString() {
+ return "Temporary row id";
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java
new file mode 100644
index 0000000000..9aa4f7c4be
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+public class J2EEConnectionPool implements JDBCConnectionPool {
+
+ private String dataSourceJndiName;
+
+ private DataSource dataSource = null;
+
+ public J2EEConnectionPool(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public J2EEConnectionPool(String dataSourceJndiName) {
+ this.dataSourceJndiName = dataSourceJndiName;
+ }
+
+ @Override
+ public Connection reserveConnection() throws SQLException {
+ Connection conn = getDataSource().getConnection();
+ conn.setAutoCommit(false);
+
+ return conn;
+ }
+
+ private DataSource getDataSource() throws SQLException {
+ if (dataSource == null) {
+ dataSource = lookupDataSource();
+ }
+ return dataSource;
+ }
+
+ private DataSource lookupDataSource() throws SQLException {
+ try {
+ InitialContext ic = new InitialContext();
+ return (DataSource) ic.lookup(dataSourceJndiName);
+ } catch (NamingException e) {
+ throw new SQLException(
+ "NamingException - Cannot connect to the database. Cause: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void releaseConnection(Connection conn) {
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ Logger.getLogger(J2EEConnectionPool.class.getName()).log(
+ Level.FINE, "Could not release SQL connection", e);
+ }
+ }
+ }
+
+ @Override
+ public void destroy() {
+ dataSource = null;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java
new file mode 100644
index 0000000000..cf12461588
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * Interface for implementing connection pools to be used with SQLContainer.
+ */
+public interface JDBCConnectionPool extends Serializable {
+ /**
+ * Retrieves a connection.
+ *
+ * @return a usable connection to the database
+ * @throws SQLException
+ */
+ public Connection reserveConnection() throws SQLException;
+
+ /**
+ * Releases a connection that was retrieved earlier.
+ *
+ * Note that depending on implementation, the transaction possibly open in
+ * the connection may or may not be rolled back.
+ *
+ * @param conn
+ * Connection to be released
+ */
+ public void releaseConnection(Connection conn);
+
+ /**
+ * Destroys the connection pool: close() is called an all the connections in
+ * the pool, whether available or reserved.
+ *
+ * This method was added to fix PostgreSQL -related issues with connections
+ * that were left hanging 'idle'.
+ */
+ public void destroy();
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java
new file mode 100644
index 0000000000..21760014b9
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java
@@ -0,0 +1,168 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.connection;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Simple implementation of the JDBCConnectionPool interface. Handles loading
+ * the JDBC driver, setting up the connections and ensuring they are still
+ * usable upon release.
+ */
+@SuppressWarnings("serial")
+public class SimpleJDBCConnectionPool implements JDBCConnectionPool {
+
+ private int initialConnections = 5;
+ private int maxConnections = 20;
+
+ private String driverName;
+ private String connectionUri;
+ private String userName;
+ private String password;
+
+ private transient Set<Connection> availableConnections;
+ private transient Set<Connection> reservedConnections;
+
+ private boolean initialized;
+
+ public SimpleJDBCConnectionPool(String driverName, String connectionUri,
+ String userName, String password) throws SQLException {
+ if (driverName == null) {
+ throw new IllegalArgumentException(
+ "JDBC driver class name must be given.");
+ }
+ if (connectionUri == null) {
+ throw new IllegalArgumentException(
+ "Database connection URI must be given.");
+ }
+ if (userName == null) {
+ throw new IllegalArgumentException(
+ "Database username must be given.");
+ }
+ if (password == null) {
+ throw new IllegalArgumentException(
+ "Database password must be given.");
+ }
+ this.driverName = driverName;
+ this.connectionUri = connectionUri;
+ this.userName = userName;
+ this.password = password;
+
+ /* Initialize JDBC driver */
+ try {
+ Class.forName(driverName).newInstance();
+ } catch (Exception ex) {
+ throw new RuntimeException("Specified JDBC Driver: " + driverName
+ + " - initialization failed.", ex);
+ }
+ }
+
+ public SimpleJDBCConnectionPool(String driverName, String connectionUri,
+ String userName, String password, int initialConnections,
+ int maxConnections) throws SQLException {
+ this(driverName, connectionUri, userName, password);
+ this.initialConnections = initialConnections;
+ this.maxConnections = maxConnections;
+ }
+
+ private void initializeConnections() throws SQLException {
+ availableConnections = new HashSet<Connection>(initialConnections);
+ reservedConnections = new HashSet<Connection>(initialConnections);
+ for (int i = 0; i < initialConnections; i++) {
+ availableConnections.add(createConnection());
+ }
+ initialized = true;
+ }
+
+ @Override
+ public synchronized Connection reserveConnection() throws SQLException {
+ if (!initialized) {
+ initializeConnections();
+ }
+ if (availableConnections.isEmpty()) {
+ if (reservedConnections.size() < maxConnections) {
+ availableConnections.add(createConnection());
+ } else {
+ throw new SQLException("Connection limit has been reached.");
+ }
+ }
+
+ Connection c = availableConnections.iterator().next();
+ availableConnections.remove(c);
+ reservedConnections.add(c);
+
+ return c;
+ }
+
+ @Override
+ public synchronized void releaseConnection(Connection conn) {
+ if (conn == null || !initialized) {
+ return;
+ }
+ /* Try to roll back if necessary */
+ try {
+ if (!conn.getAutoCommit()) {
+ conn.rollback();
+ }
+ } catch (SQLException e) {
+ /* Roll back failed, close and discard connection */
+ try {
+ conn.close();
+ } catch (SQLException e1) {
+ /* Nothing needs to be done */
+ }
+ reservedConnections.remove(conn);
+ return;
+ }
+ reservedConnections.remove(conn);
+ availableConnections.add(conn);
+ }
+
+ private Connection createConnection() throws SQLException {
+ Connection c = DriverManager.getConnection(connectionUri, userName,
+ password);
+ c.setAutoCommit(false);
+ if (driverName.toLowerCase().contains("mysql")) {
+ try {
+ Statement s = c.createStatement();
+ s.execute("SET SESSION sql_mode = 'ANSI'");
+ s.close();
+ } catch (Exception e) {
+ // Failed to set ansi mode; continue
+ }
+ }
+ return c;
+ }
+
+ @Override
+ public void destroy() {
+ for (Connection c : availableConnections) {
+ try {
+ c.close();
+ } catch (SQLException e) {
+ // No need to do anything
+ }
+ }
+ for (Connection c : reservedConnections) {
+ try {
+ c.close();
+ } catch (SQLException e) {
+ // No need to do anything
+ }
+ }
+
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ initialized = false;
+ out.defaultWriteObject();
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java
new file mode 100644
index 0000000000..ec986fab95
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java
@@ -0,0 +1,507 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLContainer;
+import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class FreeformQuery implements QueryDelegate {
+
+ FreeformQueryDelegate delegate = null;
+ private String queryString;
+ private List<String> primaryKeyColumns;
+ private JDBCConnectionPool connectionPool;
+ private transient Connection activeConnection = null;
+
+ /**
+ * Prevent no-parameters instantiation of FreeformQuery
+ */
+ @SuppressWarnings("unused")
+ private FreeformQuery() {
+ }
+
+ /**
+ * Creates a new freeform query delegate to be used with the
+ * {@link SQLContainer}.
+ *
+ * @param queryString
+ * The actual query to perform.
+ * @param primaryKeyColumns
+ * The primary key columns. Read-only mode is forced if this
+ * parameter is null or empty.
+ * @param connectionPool
+ * the JDBCConnectionPool to use to open connections to the SQL
+ * database.
+ * @deprecated @see
+ * {@link FreeformQuery#FreeformQuery(String, JDBCConnectionPool, String...)}
+ */
+ @Deprecated
+ public FreeformQuery(String queryString, List<String> primaryKeyColumns,
+ JDBCConnectionPool connectionPool) {
+ if (primaryKeyColumns == null) {
+ primaryKeyColumns = new ArrayList<String>();
+ }
+ if (primaryKeyColumns.contains("")) {
+ throw new IllegalArgumentException(
+ "The primary key columns contain an empty string!");
+ } else if (queryString == null || "".equals(queryString)) {
+ throw new IllegalArgumentException(
+ "The query string may not be empty or null!");
+ } else if (connectionPool == null) {
+ throw new IllegalArgumentException(
+ "The connectionPool may not be null!");
+ }
+ this.queryString = queryString;
+ this.primaryKeyColumns = Collections
+ .unmodifiableList(primaryKeyColumns);
+ this.connectionPool = connectionPool;
+ }
+
+ /**
+ * Creates a new freeform query delegate to be used with the
+ * {@link SQLContainer}.
+ *
+ * @param queryString
+ * The actual query to perform.
+ * @param connectionPool
+ * the JDBCConnectionPool to use to open connections to the SQL
+ * database.
+ * @param primaryKeyColumns
+ * The primary key columns. Read-only mode is forced if none are
+ * provided. (optional)
+ */
+ public FreeformQuery(String queryString, JDBCConnectionPool connectionPool,
+ String... primaryKeyColumns) {
+ this(queryString, Arrays.asList(primaryKeyColumns), connectionPool);
+ }
+
+ /**
+ * This implementation of getCount() actually fetches all records from the
+ * database, which might be a performance issue. Override this method with a
+ * SELECT COUNT(*) ... query if this is too slow for your needs.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public int getCount() throws SQLException {
+ // First try the delegate
+ int count = countByDelegate();
+ if (count < 0) {
+ // Couldn't use the delegate, use the bad way.
+ Connection conn = getConnection();
+ Statement statement = conn.createStatement(
+ ResultSet.TYPE_SCROLL_INSENSITIVE,
+ ResultSet.CONCUR_READ_ONLY);
+
+ ResultSet rs = statement.executeQuery(queryString);
+ if (rs.last()) {
+ count = rs.getRow();
+ } else {
+ count = 0;
+ }
+ rs.close();
+ statement.close();
+ releaseConnection(conn);
+ }
+ return count;
+ }
+
+ @SuppressWarnings("deprecation")
+ private int countByDelegate() throws SQLException {
+ int count = -1;
+ if (delegate == null) {
+ return count;
+ }
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getCountStatement();
+ Connection c = getConnection();
+ PreparedStatement pstmt = c.prepareStatement(sh
+ .getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ ResultSet rs = pstmt.executeQuery();
+ rs.next();
+ count = rs.getInt(1);
+ rs.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ releaseConnection(c);
+ return count;
+ } catch (UnsupportedOperationException e) {
+ // Count statement generation not supported
+ }
+ }
+ /* Try using regular statement */
+ try {
+ String countQuery = delegate.getCountQuery();
+ if (countQuery != null) {
+ Connection conn = getConnection();
+ Statement statement = conn.createStatement();
+ ResultSet rs = statement.executeQuery(countQuery);
+ rs.next();
+ count = rs.getInt(1);
+ rs.close();
+ statement.close();
+ releaseConnection(conn);
+ return count;
+ }
+ } catch (UnsupportedOperationException e) {
+ // Count query generation not supported
+ }
+ return count;
+ }
+
+ private Connection getConnection() throws SQLException {
+ if (activeConnection != null) {
+ return activeConnection;
+ }
+ return connectionPool.reserveConnection();
+ }
+
+ /**
+ * Fetches the results for the query. This implementation always fetches the
+ * entire record set, ignoring the offset and page length parameters. In
+ * order to support lazy loading of records, you must supply a
+ * FreeformQueryDelegate that implements the
+ * FreeformQueryDelegate.getQueryString(int,int) method.
+ *
+ * @throws SQLException
+ *
+ * @see FreeformQueryDelegate#getQueryString(int, int)
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public ResultSet getResults(int offset, int pagelength) throws SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction!");
+ }
+ String query = queryString;
+ if (delegate != null) {
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getQueryStatement(offset, pagelength);
+ PreparedStatement pstmt = activeConnection
+ .prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ return pstmt.executeQuery();
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ query = delegate.getQueryString(offset, pagelength);
+ } catch (UnsupportedOperationException e) {
+ // This is fine, we'll just use the default queryString.
+ }
+ }
+ Statement statement = activeConnection.createStatement();
+ ResultSet rs = statement.executeQuery(query);
+ return rs;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean implementationRespectsPagingLimits() {
+ if (delegate == null) {
+ return false;
+ }
+ /* First try using prepared statement */
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getCountStatement();
+ if (sh != null && sh.getQueryString() != null
+ && sh.getQueryString().length() > 0) {
+ return true;
+ }
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ String queryString = delegate.getQueryString(0, 50);
+ return queryString != null && queryString.length() > 0;
+ } catch (UnsupportedOperationException e) {
+ return false;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setFilters(java
+ * .util.List)
+ */
+ @Override
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException {
+ if (delegate != null) {
+ delegate.setFilters(filters);
+ } else if (filters != null) {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#setOrderBy(java
+ * .util.List)
+ */
+ @Override
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException {
+ if (delegate != null) {
+ delegate.setOrderBy(orderBys);
+ } else if (orderBys != null) {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin
+ * .data.util.sqlcontainer.RowItem)
+ */
+ @Override
+ public int storeRow(RowItem row) throws SQLException {
+ if (activeConnection == null) {
+ throw new IllegalStateException("No transaction is active!");
+ } else if (primaryKeyColumns.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Cannot store items fetched with a read-only freeform query!");
+ }
+ if (delegate != null) {
+ return delegate.storeRow(activeConnection, row);
+ } else {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin
+ * .data.util.sqlcontainer.RowItem)
+ */
+ @Override
+ public boolean removeRow(RowItem row) throws SQLException {
+ if (activeConnection == null) {
+ throw new IllegalStateException("No transaction is active!");
+ } else if (primaryKeyColumns.isEmpty()) {
+ throw new UnsupportedOperationException(
+ "Cannot remove items fetched with a read-only freeform query!");
+ }
+ if (delegate != null) {
+ return delegate.removeRow(activeConnection, row);
+ } else {
+ throw new UnsupportedOperationException(
+ "FreeFormQueryDelegate not set!");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#beginTransaction()
+ */
+ @Override
+ public synchronized void beginTransaction()
+ throws UnsupportedOperationException, SQLException {
+ if (activeConnection != null) {
+ throw new IllegalStateException("A transaction is already active!");
+ }
+ activeConnection = connectionPool.reserveConnection();
+ activeConnection.setAutoCommit(false);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#commit()
+ */
+ @Override
+ public synchronized void commit() throws UnsupportedOperationException,
+ SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction");
+ }
+ if (!activeConnection.getAutoCommit()) {
+ activeConnection.commit();
+ }
+ connectionPool.releaseConnection(activeConnection);
+ activeConnection = null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.util.sqlcontainer.query.QueryDelegate#rollback()
+ */
+ @Override
+ public synchronized void rollback() throws UnsupportedOperationException,
+ SQLException {
+ if (activeConnection == null) {
+ throw new SQLException("No active transaction");
+ }
+ activeConnection.rollback();
+ connectionPool.releaseConnection(activeConnection);
+ activeConnection = null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.util.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns
+ * ()
+ */
+ @Override
+ public List<String> getPrimaryKeyColumns() {
+ return primaryKeyColumns;
+ }
+
+ public String getQueryString() {
+ return queryString;
+ }
+
+ public FreeformQueryDelegate getDelegate() {
+ return delegate;
+ }
+
+ public void setDelegate(FreeformQueryDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * This implementation of the containsRowWithKey method rewrites existing
+ * WHERE clauses in the query string. The logic is, however, not very
+ * complex and some times can do the Wrong Thing<sup>TM</sup>. For the
+ * situations where this logic is not enough, you can implement the
+ * getContainsRowQueryString method in FreeformQueryDelegate and this will
+ * be used instead of the logic.
+ *
+ * @see FreeformQueryDelegate#getContainsRowQueryString(Object...)
+ *
+ */
+ @Override
+ @SuppressWarnings("deprecation")
+ public boolean containsRowWithKey(Object... keys) throws SQLException {
+ String query = null;
+ boolean contains = false;
+ if (delegate != null) {
+ if (delegate instanceof FreeformStatementDelegate) {
+ try {
+ StatementHelper sh = ((FreeformStatementDelegate) delegate)
+ .getContainsRowQueryStatement(keys);
+ Connection c = getConnection();
+ PreparedStatement pstmt = c.prepareStatement(sh
+ .getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ ResultSet rs = pstmt.executeQuery();
+ contains = rs.next();
+ rs.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ releaseConnection(c);
+ return contains;
+ } catch (UnsupportedOperationException e) {
+ // Statement generation not supported, continue...
+ }
+ }
+ try {
+ query = delegate.getContainsRowQueryString(keys);
+ } catch (UnsupportedOperationException e) {
+ query = modifyWhereClause(keys);
+ }
+ } else {
+ query = modifyWhereClause(keys);
+ }
+ Connection conn = getConnection();
+ try {
+ Statement statement = conn.createStatement();
+ ResultSet rs = statement.executeQuery(query);
+ contains = rs.next();
+ rs.close();
+ statement.close();
+ } finally {
+ releaseConnection(conn);
+ }
+ return contains;
+ }
+
+ /**
+ * Releases the connection if it is not part of an active transaction.
+ *
+ * @param conn
+ * the connection to release
+ */
+ private void releaseConnection(Connection conn) {
+ if (conn != activeConnection) {
+ connectionPool.releaseConnection(conn);
+ }
+ }
+
+ private String modifyWhereClause(Object... keys) {
+ // Build the where rules for the provided keys
+ StringBuffer where = new StringBuffer();
+ for (int ix = 0; ix < primaryKeyColumns.size(); ix++) {
+ where.append(QueryBuilder.quote(primaryKeyColumns.get(ix)));
+ if (keys[ix] == null) {
+ where.append(" IS NULL");
+ } else {
+ where.append(" = '").append(keys[ix]).append("'");
+ }
+ if (ix < primaryKeyColumns.size() - 1) {
+ where.append(" AND ");
+ }
+ }
+ // Is there already a WHERE clause in the query string?
+ int index = queryString.toLowerCase().indexOf("where ");
+ if (index > -1) {
+ // Rewrite the where clause
+ return queryString.substring(0, index) + "WHERE " + where + " AND "
+ + queryString.substring(index + 6);
+ }
+ // Append a where clause
+ return queryString + " WHERE " + where;
+ }
+
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ try {
+ rollback();
+ } catch (SQLException ignored) {
+ }
+ out.defaultWriteObject();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java
new file mode 100644
index 0000000000..433d742be8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java
@@ -0,0 +1,118 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+
+public interface FreeformQueryDelegate extends Serializable {
+ /**
+ * Should return the SQL query string to be performed. This method is
+ * responsible for gluing together the select query from the filters and the
+ * order by conditions if these are supported.
+ *
+ * @param offset
+ * the first record (row) to fetch.
+ * @param pagelength
+ * the number of records (rows) to fetch. 0 means all records
+ * starting from offset.
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getQueryString(int offset, int limit)
+ throws UnsupportedOperationException;
+
+ /**
+ * Generates and executes a query to determine the current row count from
+ * the DB. Row count will be fetched using filters that are currently set to
+ * the QueryDelegate.
+ *
+ * @return row count
+ * @throws SQLException
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getCountQuery() throws UnsupportedOperationException;
+
+ /**
+ * Sets the filters to apply when performing the SQL query. These are
+ * translated into a WHERE clause. Default filtering mode will be used.
+ *
+ * @param filters
+ * The filters to apply.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support filtering.
+ */
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException;
+
+ /**
+ * Sets the order in which to retrieve rows from the database. The result
+ * can be ordered by zero or more columns and each column can be in
+ * ascending or descending order. These are translated into an ORDER BY
+ * clause in the SQL query.
+ *
+ * @param orderBys
+ * A list of the OrderBy conditions.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support ordering.
+ */
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException;
+
+ /**
+ * Stores a row in the database. The implementation of this interface
+ * decides how to identify whether to store a new row or update an existing
+ * one.
+ *
+ * @param conn
+ * the JDBC connection to use
+ * @param row
+ * RowItem to be stored or updated.
+ * @throws UnsupportedOperationException
+ * if the implementation is read only.
+ * @throws SQLException
+ */
+ public int storeRow(Connection conn, RowItem row)
+ throws UnsupportedOperationException, SQLException;
+
+ /**
+ * Removes the given RowItem from the database.
+ *
+ * @param conn
+ * the JDBC connection to use
+ * @param row
+ * RowItem to be removed
+ * @return true on success
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public boolean removeRow(Connection conn, RowItem row)
+ throws UnsupportedOperationException, SQLException;
+
+ /**
+ * Generates an SQL Query string that allows the user of the FreeformQuery
+ * class to customize the query string used by the
+ * FreeformQuery.containsRowWithKeys() method. This is useful for cases when
+ * the logic in the containsRowWithKeys method is not enough to support more
+ * complex free form queries.
+ *
+ * @param keys
+ * the values of the primary keys
+ * @throws UnsupportedOperationException
+ * to use the default logic in FreeformQuery
+ * @deprecated Implement {@link FreeformStatementDelegate} instead of
+ * {@link FreeformQueryDelegate}
+ */
+ @Deprecated
+ public String getContainsRowQueryString(Object... keys)
+ throws UnsupportedOperationException;
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java
new file mode 100644
index 0000000000..95521c5019
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+/**
+ * FreeformStatementDelegate is an extension to FreeformQueryDelegate that
+ * provides definitions for methods that produce StatementHelper objects instead
+ * of basic query strings. This allows the FreeformQuery query delegate to use
+ * PreparedStatements instead of regular Statement when accessing the database.
+ *
+ * Due to the injection protection and other benefits of prepared statements, it
+ * is advisable to implement this interface instead of the FreeformQueryDelegate
+ * whenever possible.
+ */
+public interface FreeformStatementDelegate extends FreeformQueryDelegate {
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement. This
+ * method is responsible for gluing together the select query from the
+ * filters and the order by conditions if these are supported.
+ *
+ * @param offset
+ * the first record (row) to fetch.
+ * @param pagelength
+ * the number of records (rows) to fetch. 0 means all records
+ * starting from offset.
+ */
+ public StatementHelper getQueryStatement(int offset, int limit)
+ throws UnsupportedOperationException;
+
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement that
+ * will fetch the row count from the DB. Row count should be fetched using
+ * filters that are currently set to the QueryDelegate.
+ */
+ public StatementHelper getCountStatement()
+ throws UnsupportedOperationException;
+
+ /**
+ * Should return a new instance of StatementHelper that contains the query
+ * string and parameter values required to create a PreparedStatement used
+ * by the FreeformQuery.containsRowWithKeys() method. This is useful for
+ * cases when the default logic in said method is not enough to support more
+ * complex free form queries.
+ *
+ * @param keys
+ * the values of the primary keys
+ * @throws UnsupportedOperationException
+ * to use the default logic in FreeformQuery
+ */
+ public StatementHelper getContainsRowQueryStatement(Object... keys)
+ throws UnsupportedOperationException;
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java
new file mode 100644
index 0000000000..8ebe10067e
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java
@@ -0,0 +1,46 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+
+/**
+ * OrderBy represents a sorting rule to be applied to a query made by the
+ * SQLContainer's QueryDelegate.
+ *
+ * The sorting rule is simple and contains only the affected column's name and
+ * the direction of the sort.
+ */
+public class OrderBy implements Serializable {
+ private String column;
+ private boolean isAscending;
+
+ /**
+ * Prevent instantiation without required parameters.
+ */
+ @SuppressWarnings("unused")
+ private OrderBy() {
+ }
+
+ public OrderBy(String column, boolean isAscending) {
+ setColumn(column);
+ setAscending(isAscending);
+ }
+
+ public void setColumn(String column) {
+ this.column = column;
+ }
+
+ public String getColumn() {
+ return column;
+ }
+
+ public void setAscending(boolean isAscending) {
+ this.isAscending = isAscending;
+ }
+
+ public boolean isAscending() {
+ return isAscending;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java
new file mode 100644
index 0000000000..6e4396fad1
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java
@@ -0,0 +1,211 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.Serializable;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowId;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+
+public interface QueryDelegate extends Serializable {
+ /**
+ * Generates and executes a query to determine the current row count from
+ * the DB. Row count will be fetched using filters that are currently set to
+ * the QueryDelegate.
+ *
+ * @return row count
+ * @throws SQLException
+ */
+ public int getCount() throws SQLException;
+
+ /**
+ * Executes a paged SQL query and returns the ResultSet. The query is
+ * defined through implementations of this QueryDelegate interface.
+ *
+ * @param offset
+ * the first item of the page to load
+ * @param pagelength
+ * the length of the page to load
+ * @return a ResultSet containing the rows of the page
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public ResultSet getResults(int offset, int pagelength) throws SQLException;
+
+ /**
+ * Allows the SQLContainer implementation to check whether the QueryDelegate
+ * implementation implements paging in the getResults method.
+ *
+ * @see QueryDelegate#getResults(int, int)
+ *
+ * @return true if the delegate implements paging
+ */
+ public boolean implementationRespectsPagingLimits();
+
+ /**
+ * Sets the filters to apply when performing the SQL query. These are
+ * translated into a WHERE clause. Default filtering mode will be used.
+ *
+ * @param filters
+ * The filters to apply.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support filtering.
+ */
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException;
+
+ /**
+ * Sets the order in which to retrieve rows from the database. The result
+ * can be ordered by zero or more columns and each column can be in
+ * ascending or descending order. These are translated into an ORDER BY
+ * clause in the SQL query.
+ *
+ * @param orderBys
+ * A list of the OrderBy conditions.
+ * @throws UnsupportedOperationException
+ * if the implementation doesn't support ordering.
+ */
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException;
+
+ /**
+ * Stores a row in the database. The implementation of this interface
+ * decides how to identify whether to store a new row or update an existing
+ * one.
+ *
+ * @param columnToValueMap
+ * A map containing the values for all columns to be stored or
+ * updated.
+ * @return the number of affected rows in the database table
+ * @throws UnsupportedOperationException
+ * if the implementation is read only.
+ */
+ public int storeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException;
+
+ /**
+ * Removes the given RowItem from the database.
+ *
+ * @param row
+ * RowItem to be removed
+ * @return true on success
+ * @throws UnsupportedOperationException
+ * @throws SQLException
+ */
+ public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException;
+
+ /**
+ * Starts a new database transaction. Used when storing multiple changes.
+ *
+ * Note that if a transaction is already open, it will be rolled back when a
+ * new transaction is started.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void beginTransaction() throws SQLException;
+
+ /**
+ * Commits a transaction. If a transaction is not open nothing should
+ * happen.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void commit() throws SQLException;
+
+ /**
+ * Rolls a transaction back. If a transaction is not open nothing should
+ * happen.
+ *
+ * @throws SQLException
+ * if the database access fails.
+ */
+ public void rollback() throws SQLException;
+
+ /**
+ * Returns a list of primary key column names. The list is either fetched
+ * from the database (TableQuery) or given as an argument depending on
+ * implementation.
+ *
+ * @return
+ */
+ public List<String> getPrimaryKeyColumns();
+
+ /**
+ * Performs a query to find out whether the SQL table contains a row with
+ * the given set of primary keys.
+ *
+ * @param keys
+ * the primary keys
+ * @return true if the SQL table contains a row with the provided keys
+ * @throws SQLException
+ */
+ public boolean containsRowWithKey(Object... keys) throws SQLException;
+
+ /************************/
+ /** ROWID CHANGE EVENT **/
+ /************************/
+
+ /**
+ * An <code>Event</code> object specifying the old and new RowId of an added
+ * item after the addition has been successfully committed.
+ */
+ public interface RowIdChangeEvent extends Serializable {
+ /**
+ * Gets the old (temporary) RowId of the added row that raised this
+ * event.
+ *
+ * @return old RowId
+ */
+ public RowId getOldRowId();
+
+ /**
+ * Gets the new, possibly database assigned RowId of the added row that
+ * raised this event.
+ *
+ * @return new RowId
+ */
+ public RowId getNewRowId();
+ }
+
+ /** RowId change listener interface. */
+ public interface RowIdChangeListener extends Serializable {
+ /**
+ * Lets the listener know that a RowId has been changed.
+ *
+ * @param event
+ */
+ public void rowIdChange(QueryDelegate.RowIdChangeEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>RowIdChangeEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>RowIdChangeEvent</code> when it performs a
+ * database commit that may change the RowId.
+ */
+ public interface RowIdChangeNotifier extends Serializable {
+ /**
+ * Adds a RowIdChangeListener for the object.
+ *
+ * @param listener
+ * listener to be added
+ */
+ public void addListener(QueryDelegate.RowIdChangeListener listener);
+
+ /**
+ * Removes the specified RowIdChangeListener from the object.
+ *
+ * @param listener
+ * listener to be removed
+ */
+ public void removeListener(QueryDelegate.RowIdChangeListener listener);
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java
new file mode 100644
index 0000000000..d0606704f7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java
@@ -0,0 +1,715 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Compare.Equal;
+import com.vaadin.data.util.sqlcontainer.ColumnProperty;
+import com.vaadin.data.util.sqlcontainer.OptimisticLockException;
+import com.vaadin.data.util.sqlcontainer.RowId;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLUtil;
+import com.vaadin.data.util.sqlcontainer.TemporaryRowId;
+import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool;
+import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+@SuppressWarnings("serial")
+public class TableQuery implements QueryDelegate,
+ QueryDelegate.RowIdChangeNotifier {
+
+ /** Table name, primary key column name(s) and version column name */
+ private String tableName;
+ private List<String> primaryKeyColumns;
+ private String versionColumn;
+
+ /** Currently set Filters and OrderBys */
+ private List<Filter> filters;
+ private List<OrderBy> orderBys;
+
+ /** SQLGenerator instance to use for generating queries */
+ private SQLGenerator sqlGenerator;
+
+ /** Fields related to Connection and Transaction handling */
+ private JDBCConnectionPool connectionPool;
+ private transient Connection activeConnection;
+ private boolean transactionOpen;
+
+ /** Row ID change listeners */
+ private LinkedList<RowIdChangeListener> rowIdChangeListeners;
+ /** Row ID change events, stored until commit() is called */
+ private final List<RowIdChangeEvent> bufferedEvents = new ArrayList<RowIdChangeEvent>();
+
+ /** Set to true to output generated SQL Queries to System.out */
+ private 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.
+ *
+ * @param tableName
+ * Name of the database table to connect to
+ * @param connectionPool
+ * Connection pool for accessing the database
+ * @param sqlGenerator
+ * SQL query generator implementation
+ */
+ public TableQuery(String tableName, JDBCConnectionPool connectionPool,
+ SQLGenerator sqlGenerator) {
+ 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;
+ this.connectionPool = connectionPool;
+ fetchMetaData();
+ }
+
+ /**
+ * Creates a new TableQuery using the given connection pool and table name
+ * to fetch the data from. All parameters must be non-null. The default SQL
+ * generator will be used for queries.
+ *
+ * @param tableName
+ * Name of the database table to connect to
+ * @param connectionPool
+ * Connection pool for accessing the database
+ */
+ public TableQuery(String tableName, JDBCConnectionPool connectionPool) {
+ this(tableName, connectionPool, new DefaultSQLGenerator());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getCount()
+ */
+ @Override
+ public int getCount() throws SQLException {
+ getLogger().log(Level.FINE, "Fetching count...");
+ StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+ filters, null, 0, 0, "COUNT(*)");
+ boolean shouldCloseTransaction = false;
+ if (!transactionOpen) {
+ shouldCloseTransaction = true;
+ beginTransaction();
+ }
+ ResultSet r = executeQuery(sh);
+ r.next();
+ int count = r.getInt(1);
+ r.getStatement().close();
+ r.close();
+ if (shouldCloseTransaction) {
+ commit();
+ }
+ return count;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#getResults(int,
+ * int)
+ */
+ @Override
+ public ResultSet getResults(int offset, int pagelength) throws SQLException {
+ StatementHelper sh;
+ /*
+ * If no ordering is explicitly set, results will be ordered by the
+ * first primary key column.
+ */
+ if (orderBys == null || orderBys.isEmpty()) {
+ List<OrderBy> ob = new ArrayList<OrderBy>();
+ ob.add(new OrderBy(primaryKeyColumns.get(0), true));
+ sh = sqlGenerator.generateSelectQuery(tableName, filters, ob,
+ offset, pagelength, null);
+ } else {
+ sh = sqlGenerator.generateSelectQuery(tableName, filters, orderBys,
+ offset, pagelength, null);
+ }
+ return executeQuery(sh);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#
+ * implementationRespectsPagingLimits()
+ */
+ @Override
+ public boolean implementationRespectsPagingLimits() {
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#storeRow(com.vaadin
+ * .addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public int storeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException {
+ if (row == null) {
+ throw new IllegalArgumentException("Row argument must be non-null.");
+ }
+ StatementHelper sh;
+ int result = 0;
+ if (row.getId() instanceof TemporaryRowId) {
+ setVersionColumnFlagInProperty(row);
+ sh = sqlGenerator.generateInsertQuery(tableName, row);
+ result = executeUpdateReturnKeys(sh, row);
+ } else {
+ setVersionColumnFlagInProperty(row);
+ sh = sqlGenerator.generateUpdateQuery(tableName, row);
+ result = executeUpdate(sh);
+ }
+ if (versionColumn != null && result == 0) {
+ throw new OptimisticLockException(
+ "Someone else changed the row that was being updated.",
+ row.getId());
+ }
+ return result;
+ }
+
+ private void setVersionColumnFlagInProperty(RowItem row) {
+ ColumnProperty versionProperty = (ColumnProperty) row
+ .getItemProperty(versionColumn);
+ if (versionProperty != null) {
+ versionProperty.setVersionColumn(true);
+ }
+ }
+
+ /**
+ * Inserts the given row in the database table immediately. Begins and
+ * commits the transaction needed. This method was added specifically to
+ * solve the problem of returning the final RowId immediately on the
+ * SQLContainer.addItem() call when auto commit mode is enabled in the
+ * SQLContainer.
+ *
+ * @param row
+ * RowItem to add to the database
+ * @return Final RowId of the added row
+ * @throws SQLException
+ */
+ public RowId storeRowImmediately(RowItem row) throws SQLException {
+ beginTransaction();
+ /* Set version column, if one is provided */
+ setVersionColumnFlagInProperty(row);
+ /* Generate query */
+ StatementHelper sh = sqlGenerator.generateInsertQuery(tableName, row);
+ PreparedStatement pstmt = activeConnection.prepareStatement(
+ sh.getQueryString(), primaryKeyColumns.toArray(new String[0]));
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int result = pstmt.executeUpdate();
+ if (result > 0) {
+ /*
+ * If affected rows exist, we'll get the new RowId, commit the
+ * transaction and return the new RowId.
+ */
+ ResultSet generatedKeys = pstmt.getGeneratedKeys();
+ RowId newId = getNewRowId(row, generatedKeys);
+ generatedKeys.close();
+ pstmt.clearParameters();
+ pstmt.close();
+ commit();
+ return newId;
+ } else {
+ pstmt.clearParameters();
+ pstmt.close();
+ /* On failure return null */
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setFilters(java.util
+ * .List)
+ */
+ @Override
+ public void setFilters(List<Filter> filters)
+ throws UnsupportedOperationException {
+ if (filters == null) {
+ this.filters = null;
+ return;
+ }
+ this.filters = Collections.unmodifiableList(filters);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#setOrderBy(java.util
+ * .List)
+ */
+ @Override
+ public void setOrderBy(List<OrderBy> orderBys)
+ throws UnsupportedOperationException {
+ if (orderBys == null) {
+ this.orderBys = null;
+ return;
+ }
+ this.orderBys = Collections.unmodifiableList(orderBys);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#beginTransaction()
+ */
+ @Override
+ public void beginTransaction() throws UnsupportedOperationException,
+ SQLException {
+ if (transactionOpen && activeConnection != null) {
+ throw new IllegalStateException();
+ }
+
+ getLogger().log(Level.FINE, "DB -> begin transaction");
+ activeConnection = connectionPool.reserveConnection();
+ activeConnection.setAutoCommit(false);
+ transactionOpen = true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#commit()
+ */
+ @Override
+ public void commit() throws UnsupportedOperationException, SQLException {
+ if (transactionOpen && activeConnection != null) {
+ getLogger().log(Level.FINE, "DB -> commit");
+ activeConnection.commit();
+ connectionPool.releaseConnection(activeConnection);
+ } else {
+ throw new SQLException("No active transaction");
+ }
+ transactionOpen = false;
+
+ /* Handle firing row ID change events */
+ RowIdChangeEvent[] unFiredEvents = bufferedEvents
+ .toArray(new RowIdChangeEvent[] {});
+ bufferedEvents.clear();
+ if (rowIdChangeListeners != null && !rowIdChangeListeners.isEmpty()) {
+ for (RowIdChangeListener r : rowIdChangeListeners) {
+ for (RowIdChangeEvent e : unFiredEvents) {
+ r.rowIdChange(e);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.QueryDelegate#rollback()
+ */
+ @Override
+ public void rollback() throws UnsupportedOperationException, SQLException {
+ if (transactionOpen && activeConnection != null) {
+ getLogger().log(Level.FINE, "DB -> rollback");
+ activeConnection.rollback();
+ connectionPool.releaseConnection(activeConnection);
+ } else {
+ throw new SQLException("No active transaction");
+ }
+ transactionOpen = false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#getPrimaryKeyColumns()
+ */
+ @Override
+ public List<String> getPrimaryKeyColumns() {
+ return Collections.unmodifiableList(primaryKeyColumns);
+ }
+
+ public String getVersionColumn() {
+ return versionColumn;
+ }
+
+ public void setVersionColumn(String column) {
+ versionColumn = column;
+ }
+
+ public String getTableName() {
+ return tableName;
+ }
+
+ public SQLGenerator getSqlGenerator() {
+ return sqlGenerator;
+ }
+
+ /**
+ * Executes the given query string using either the active connection if a
+ * transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @return ResultSet of the query
+ * @throws SQLException
+ */
+ private ResultSet executeQuery(StatementHelper sh) throws SQLException {
+ Connection c = null;
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ throw new SQLException("No active transaction!");
+ }
+ PreparedStatement pstmt = c.prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ return pstmt.executeQuery();
+ }
+
+ /**
+ * Executes the given update query string using either the active connection
+ * if a transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @return Number of affected rows
+ * @throws SQLException
+ */
+ private int executeUpdate(StatementHelper sh) throws SQLException {
+ Connection c = null;
+ PreparedStatement pstmt = null;
+ try {
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ c = connectionPool.reserveConnection();
+ }
+ pstmt = c.prepareStatement(sh.getQueryString());
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int retval = pstmt.executeUpdate();
+ return retval;
+ } finally {
+ if (pstmt != null) {
+ pstmt.clearParameters();
+ pstmt.close();
+ }
+ if (!transactionOpen) {
+ connectionPool.releaseConnection(c);
+ }
+ }
+ }
+
+ /**
+ * Executes the given update query string using either the active connection
+ * if a transaction is already open, or a new connection from this query's
+ * connection pool.
+ *
+ * Additionally adds a new RowIdChangeEvent to the event buffer.
+ *
+ * @param sh
+ * an instance of StatementHelper, containing the query string
+ * and parameter values.
+ * @param row
+ * the row item to update
+ * @return Number of affected rows
+ * @throws SQLException
+ */
+ private int executeUpdateReturnKeys(StatementHelper sh, RowItem row)
+ throws SQLException {
+ Connection c = null;
+ PreparedStatement pstmt = null;
+ ResultSet genKeys = null;
+ try {
+ if (transactionOpen && activeConnection != null) {
+ c = activeConnection;
+ } else {
+ c = connectionPool.reserveConnection();
+ }
+ pstmt = c.prepareStatement(sh.getQueryString(),
+ primaryKeyColumns.toArray(new String[0]));
+ sh.setParameterValuesToStatement(pstmt);
+ getLogger().log(Level.FINE, "DB -> " + sh.getQueryString());
+ int result = pstmt.executeUpdate();
+ genKeys = pstmt.getGeneratedKeys();
+ RowId newId = getNewRowId(row, genKeys);
+ bufferedEvents.add(new RowIdChangeEvent(row.getId(), newId));
+ return result;
+ } finally {
+ if (genKeys != null) {
+ genKeys.close();
+ }
+ if (pstmt != null) {
+ pstmt.clearParameters();
+ pstmt.close();
+ }
+ if (!transactionOpen) {
+ connectionPool.releaseConnection(c);
+ }
+ }
+ }
+
+ /**
+ * Fetches name(s) of primary key column(s) from DB metadata.
+ *
+ * Also tries to get the escape string to be used in search strings.
+ */
+ private void fetchMetaData() {
+ Connection c = null;
+ try {
+ c = connectionPool.reserveConnection();
+ DatabaseMetaData dbmd = c.getMetaData();
+ if (dbmd != null) {
+ tableName = SQLUtil.escapeSQL(tableName);
+ ResultSet tables = dbmd.getTables(null, null, tableName, null);
+ if (!tables.next()) {
+ tables = dbmd.getTables(null, null,
+ tableName.toUpperCase(), null);
+ if (!tables.next()) {
+ throw new IllegalArgumentException(
+ "Table with the name \""
+ + tableName
+ + "\" was not found. Check your database contents.");
+ } else {
+ tableName = tableName.toUpperCase();
+ }
+ }
+ tables.close();
+ ResultSet rs = dbmd.getPrimaryKeys(null, null, tableName);
+ List<String> names = new ArrayList<String>();
+ while (rs.next()) {
+ names.add(rs.getString("COLUMN_NAME"));
+ }
+ rs.close();
+ if (!names.isEmpty()) {
+ primaryKeyColumns = names;
+ }
+ if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Primary key constraints have not been defined for the table \""
+ + tableName
+ + "\". Use FreeFormQuery to access this table.");
+ }
+ for (String colName : primaryKeyColumns) {
+ if (colName.equalsIgnoreCase("rownum")) {
+ if (getSqlGenerator() instanceof MSSQLGenerator
+ || getSqlGenerator() instanceof MSSQLGenerator) {
+ throw new IllegalArgumentException(
+ "When using Oracle or MSSQL, a primary key column"
+ + " named \'rownum\' is not allowed!");
+ }
+ }
+ }
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ } finally {
+ connectionPool.releaseConnection(c);
+ }
+ }
+
+ private RowId getNewRowId(RowItem row, ResultSet genKeys) {
+ try {
+ /* Fetch primary key values and generate a map out of them. */
+ Map<String, Object> values = new HashMap<String, Object>();
+ ResultSetMetaData rsmd = genKeys.getMetaData();
+ int colCount = rsmd.getColumnCount();
+ if (genKeys.next()) {
+ for (int i = 1; i <= colCount; i++) {
+ values.put(rsmd.getColumnName(i), genKeys.getObject(i));
+ }
+ }
+ /* Generate new RowId */
+ List<Object> newRowId = new ArrayList<Object>();
+ if (values.size() == 1) {
+ if (primaryKeyColumns.size() == 1) {
+ newRowId.add(values.get(values.keySet().iterator().next()));
+ } else {
+ for (String s : primaryKeyColumns) {
+ if (!((ColumnProperty) row.getItemProperty(s))
+ .isReadOnlyChangeAllowed()) {
+ newRowId.add(values.get(values.keySet().iterator()
+ .next()));
+ } else {
+ newRowId.add(values.get(s));
+ }
+ }
+ }
+ } else {
+ for (String s : primaryKeyColumns) {
+ newRowId.add(values.get(s));
+ }
+ }
+ return new RowId(newRowId.toArray());
+ } catch (Exception e) {
+ getLogger().log(Level.FINE,
+ "Failed to fetch key values on insert: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#removeRow(com.vaadin
+ * .addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public boolean removeRow(RowItem row) throws UnsupportedOperationException,
+ SQLException {
+ getLogger().log(Level.FINE,
+ "Removing row with id: " + row.getId().getId()[0].toString());
+ if (executeUpdate(sqlGenerator.generateDeleteQuery(getTableName(),
+ primaryKeyColumns, versionColumn, row)) == 1) {
+ return true;
+ }
+ if (versionColumn != null) {
+ throw new OptimisticLockException(
+ "Someone else changed the row that was being deleted.",
+ row.getId());
+ }
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.sqlcontainer.query.QueryDelegate#containsRowWithKey(
+ * java.lang.Object[])
+ */
+ @Override
+ public boolean containsRowWithKey(Object... keys) throws SQLException {
+ ArrayList<Filter> filtersAndKeys = new ArrayList<Filter>();
+ if (filters != null) {
+ filtersAndKeys.addAll(filters);
+ }
+ int ix = 0;
+ for (String colName : primaryKeyColumns) {
+ filtersAndKeys.add(new Equal(colName, keys[ix]));
+ ix++;
+ }
+ StatementHelper sh = sqlGenerator.generateSelectQuery(tableName,
+ filtersAndKeys, orderBys, 0, 0, "*");
+
+ boolean shouldCloseTransaction = false;
+ if (!transactionOpen) {
+ shouldCloseTransaction = true;
+ beginTransaction();
+ }
+ ResultSet rs = null;
+ try {
+ rs = executeQuery(sh);
+ boolean contains = rs.next();
+ return contains;
+ } finally {
+ if (rs != null) {
+ if (rs.getStatement() != null) {
+ rs.getStatement().close();
+ }
+ rs.close();
+ }
+ if (shouldCloseTransaction) {
+ commit();
+ }
+ }
+ }
+
+ /**
+ * Custom writeObject to call rollback() if object is serialized.
+ */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ try {
+ rollback();
+ } catch (SQLException ignored) {
+ }
+ out.defaultWriteObject();
+ }
+
+ /**
+ * Simple RowIdChangeEvent implementation.
+ */
+ public class RowIdChangeEvent extends EventObject implements
+ QueryDelegate.RowIdChangeEvent {
+ private final RowId oldId;
+ private final RowId newId;
+
+ private RowIdChangeEvent(RowId oldId, RowId newId) {
+ super(oldId);
+ this.oldId = oldId;
+ this.newId = newId;
+ }
+
+ @Override
+ public RowId getNewRowId() {
+ return newId;
+ }
+
+ @Override
+ public RowId getOldRowId() {
+ return oldId;
+ }
+ }
+
+ /**
+ * Adds RowIdChangeListener to this query
+ */
+ @Override
+ public void addListener(RowIdChangeListener listener) {
+ if (rowIdChangeListeners == null) {
+ rowIdChangeListeners = new LinkedList<QueryDelegate.RowIdChangeListener>();
+ }
+ rowIdChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes the given RowIdChangeListener from this query
+ */
+ @Override
+ public void removeListener(RowIdChangeListener listener) {
+ if (rowIdChangeListeners != null) {
+ rowIdChangeListeners.remove(listener);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(TableQuery.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java
new file mode 100644
index 0000000000..6485330541
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java
@@ -0,0 +1,367 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.ColumnProperty;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.SQLUtil;
+import com.vaadin.data.util.sqlcontainer.TemporaryRowId;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator;
+
+/**
+ * Generates generic SQL that is supported by HSQLDB, MySQL and PostgreSQL.
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+@SuppressWarnings("serial")
+public class DefaultSQLGenerator implements SQLGenerator {
+
+ private Class<? extends StatementHelper> statementHelperClass = null;
+
+ public DefaultSQLGenerator() {
+
+ }
+
+ /**
+ * Create a new DefaultSqlGenerator instance that uses the given
+ * implementation of {@link StatementHelper}
+ *
+ * @param statementHelper
+ */
+ public DefaultSQLGenerator(
+ Class<? extends StatementHelper> statementHelperClazz) {
+ this();
+ statementHelperClass = statementHelperClazz;
+ }
+
+ /**
+ * Construct a DefaultSQLGenerator with the specified identifiers for start
+ * and end of quoted strings. The identifiers may be different depending on
+ * the database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public DefaultSQLGenerator(String quoteStart, String quoteEnd) {
+ QueryBuilder.setStringDecorator(new StringDecorator(quoteStart,
+ quoteEnd));
+ }
+
+ /**
+ * Same as {@link #DefaultSQLGenerator(String, String)} but with support for
+ * custom {@link StatementHelper} implementation.
+ *
+ * @param quoteStart
+ * @param quoteEnd
+ * @param statementHelperClazz
+ */
+ public DefaultSQLGenerator(String quoteStart, String quoteEnd,
+ Class<? extends StatementHelper> statementHelperClazz) {
+ this(quoteStart, quoteEnd);
+ statementHelperClass = statementHelperClazz;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List, java.util.List,
+ * int, int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("SELECT " + toSelect + " FROM ").append(
+ SQLUtil.escapeSQL(tableName));
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ if (pagelength != 0) {
+ generateLimits(query, offset, pagelength);
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateUpdateQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateUpdateQuery(String tableName, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("Updated item must be given.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("UPDATE ").append(tableName).append(" SET");
+
+ /* Generate column<->value and rowidentifiers map */
+ Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+ Map<String, Object> rowIdentifiers = generateRowIdentifiers(item);
+ /* Generate columns and values to update */
+ boolean first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (first) {
+ query.append(" " + QueryBuilder.quote(column) + " = ?");
+ } else {
+ query.append(", " + QueryBuilder.quote(column) + " = ?");
+ }
+ sh.addParameterValue(columnToValueMap.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ /* Generate identifiers for the row to be updated */
+ first = true;
+ for (String column : rowIdentifiers.keySet()) {
+ if (first) {
+ query.append(" WHERE " + QueryBuilder.quote(column) + " = ?");
+ } else {
+ query.append(" AND " + QueryBuilder.quote(column) + " = ?");
+ }
+ sh.addParameterValue(rowIdentifiers.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateInsertQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateInsertQuery(String tableName, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException("New item must be given.");
+ }
+ if (!(item.getId() instanceof TemporaryRowId)) {
+ throw new IllegalArgumentException(
+ "Cannot generate an insert query for item already in database.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("INSERT INTO ").append(tableName).append(" (");
+
+ /* Generate column<->value map */
+ Map<String, Object> columnToValueMap = generateColumnToValueMap(item);
+ /* Generate column names for insert query */
+ boolean first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (!first) {
+ query.append(", ");
+ }
+ query.append(QueryBuilder.quote(column));
+ first = false;
+ }
+
+ /* Generate values for insert query */
+ query.append(") VALUES (");
+ first = true;
+ for (String column : columnToValueMap.keySet()) {
+ if (!first) {
+ query.append(", ");
+ }
+ query.append("?");
+ sh.addParameterValue(columnToValueMap.get(column), item
+ .getItemProperty(column).getType());
+ first = false;
+ }
+ query.append(")");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.SQLGenerator#
+ * generateDeleteQuery(java.lang.String,
+ * com.vaadin.addon.sqlcontainer.RowItem)
+ */
+ @Override
+ public StatementHelper generateDeleteQuery(String tableName,
+ List<String> primaryKeyColumns, String versionColumn, RowItem item) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ if (item == null) {
+ throw new IllegalArgumentException(
+ "Item to be deleted must be given.");
+ }
+ if (primaryKeyColumns == null || primaryKeyColumns.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Valid keyColumnNames must be provided.");
+ }
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+ query.append("DELETE FROM ").append(tableName).append(" WHERE ");
+ int count = 1;
+ for (String keyColName : primaryKeyColumns) {
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && keyColName.equalsIgnoreCase("rownum")) {
+ count++;
+ continue;
+ }
+ if (count > 1) {
+ query.append(" AND ");
+ }
+ if (item.getItemProperty(keyColName).getValue() != null) {
+ query.append(QueryBuilder.quote(keyColName) + " = ?");
+ sh.addParameterValue(item.getItemProperty(keyColName)
+ .getValue(), item.getItemProperty(keyColName).getType());
+ }
+ count++;
+ }
+ if (versionColumn != null) {
+ query.append(String.format(" AND %s = ?",
+ QueryBuilder.quote(versionColumn)));
+ sh.addParameterValue(
+ item.getItemProperty(versionColumn).getValue(), item
+ .getItemProperty(versionColumn).getType());
+ }
+
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /**
+ * Generates sorting rules as an ORDER BY -clause
+ *
+ * @param sb
+ * StringBuffer to which the clause is appended.
+ * @param o
+ * OrderBy object to be added into the sb.
+ * @param firstOrderBy
+ * If true, this is the first OrderBy.
+ * @return
+ */
+ protected StringBuffer generateOrderBy(StringBuffer sb, OrderBy o,
+ boolean firstOrderBy) {
+ if (firstOrderBy) {
+ sb.append(" ORDER BY ");
+ } else {
+ sb.append(", ");
+ }
+ sb.append(QueryBuilder.quote(o.getColumn()));
+ if (o.isAscending()) {
+ sb.append(" ASC");
+ } else {
+ sb.append(" DESC");
+ }
+ return sb;
+ }
+
+ /**
+ * Generates the LIMIT and OFFSET clause.
+ *
+ * @param sb
+ * StringBuffer to which the clause is appended.
+ * @param offset
+ * Value for offset.
+ * @param pagelength
+ * Value for pagelength.
+ * @return StringBuffer with LIMIT and OFFSET clause added.
+ */
+ protected StringBuffer generateLimits(StringBuffer sb, int offset,
+ int pagelength) {
+ sb.append(" LIMIT ").append(pagelength).append(" OFFSET ")
+ .append(offset);
+ return sb;
+ }
+
+ protected Map<String, Object> generateColumnToValueMap(RowItem item) {
+ Map<String, Object> columnToValueMap = new HashMap<String, Object>();
+ for (Object id : item.getItemPropertyIds()) {
+ ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+ /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+ continue;
+ }
+ Object value = cp.getValue() == null ? null : cp.getValue();
+ /* Only include properties whose read-only status can be altered */
+ if (cp.isReadOnlyChangeAllowed() && !cp.isVersionColumn()) {
+ columnToValueMap.put(cp.getPropertyId(), value);
+ }
+ }
+ return columnToValueMap;
+ }
+
+ protected Map<String, Object> generateRowIdentifiers(RowItem item) {
+ Map<String, Object> rowIdentifiers = new HashMap<String, Object>();
+ for (Object id : item.getItemPropertyIds()) {
+ ColumnProperty cp = (ColumnProperty) item.getItemProperty(id);
+ /* Prevent "rownum" usage as a column name if MSSQL or ORACLE */
+ if ((this instanceof MSSQLGenerator || this instanceof OracleGenerator)
+ && cp.getPropertyId().equalsIgnoreCase("rownum")) {
+ continue;
+ }
+ Object value = cp.getValue() == null ? null : cp.getValue();
+ if (!cp.isReadOnlyChangeAllowed() || cp.isVersionColumn()) {
+ rowIdentifiers.put(cp.getPropertyId(), value);
+ }
+ }
+ return rowIdentifiers;
+ }
+
+ /**
+ * Returns the statement helper for the generator. Override this to handle
+ * platform specific data types.
+ *
+ * @see http://dev.vaadin.com/ticket/9148
+ * @return a new instance of the statement helper
+ */
+ protected StatementHelper getStatementHelper() {
+ if (statementHelperClass == null) {
+ return new StatementHelper();
+ }
+
+ try {
+ return statementHelperClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException(
+ "Unable to instantiate custom StatementHelper", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(
+ "Unable to instantiate custom StatementHelper", e);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java
new file mode 100644
index 0000000000..13ef1d0090
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java
@@ -0,0 +1,101 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class MSSQLGenerator extends DefaultSQLGenerator {
+
+ public MSSQLGenerator() {
+
+ }
+
+ /**
+ * Construct a MSSQLGenerator with the specified identifiers for start and
+ * end of quoted strings. The identifiers may be different depending on the
+ * database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public MSSQLGenerator(String quoteStart, String quoteEnd) {
+ super(quoteStart, quoteEnd);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List,
+ * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+ * int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ /* Adjust offset and page length parameters to match "row numbers" */
+ offset = pagelength > 1 ? ++offset : offset;
+ pagelength = pagelength > 1 ? --pagelength : pagelength;
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+
+ /* Row count request is handled here */
+ if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+ query.append(String.format(
+ "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+ QueryBuilder.quote("rowcount"), tableName));
+ if (filters != null && !filters.isEmpty()) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(") AS t");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* SELECT without row number constraints */
+ if (offset == 0 && pagelength == 0) {
+ query.append("SELECT ").append(toSelect).append(" FROM ")
+ .append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* Remaining SELECT cases are handled here */
+ query.append("SELECT * FROM (SELECT row_number() OVER (");
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ query.append(") AS rownum, " + toSelect + " FROM ").append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(") AS a WHERE a.rownum BETWEEN ").append(offset)
+ .append(" AND ").append(Integer.toString(offset + pagelength));
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java
new file mode 100644
index 0000000000..43a562d3a8
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder;
+
+@SuppressWarnings("serial")
+public class OracleGenerator extends DefaultSQLGenerator {
+
+ public OracleGenerator() {
+
+ }
+
+ public OracleGenerator(Class<? extends StatementHelper> statementHelperClazz) {
+ super(statementHelperClazz);
+ }
+
+ /**
+ * Construct an OracleSQLGenerator with the specified identifiers for start
+ * and end of quoted strings. The identifiers may be different depending on
+ * the database engine and it's settings.
+ *
+ * @param quoteStart
+ * the identifier (character) denoting the start of a quoted
+ * string
+ * @param quoteEnd
+ * the identifier (character) denoting the end of a quoted string
+ */
+ public OracleGenerator(String quoteStart, String quoteEnd) {
+ super(quoteStart, quoteEnd);
+ }
+
+ public OracleGenerator(String quoteStart, String quoteEnd,
+ Class<? extends StatementHelper> statementHelperClazz) {
+ super(quoteStart, quoteEnd, statementHelperClazz);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.sqlcontainer.query.generator.DefaultSQLGenerator#
+ * generateSelectQuery(java.lang.String, java.util.List,
+ * com.vaadin.addon.sqlcontainer.query.FilteringMode, java.util.List, int,
+ * int, java.lang.String)
+ */
+ @Override
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect) {
+ if (tableName == null || tableName.trim().equals("")) {
+ throw new IllegalArgumentException("Table name must be given.");
+ }
+ /* Adjust offset and page length parameters to match "row numbers" */
+ offset = pagelength > 1 ? ++offset : offset;
+ pagelength = pagelength > 1 ? --pagelength : pagelength;
+ toSelect = toSelect == null ? "*" : toSelect;
+ StatementHelper sh = getStatementHelper();
+ StringBuffer query = new StringBuffer();
+
+ /* Row count request is handled here */
+ if ("COUNT(*)".equalsIgnoreCase(toSelect)) {
+ query.append(String.format(
+ "SELECT COUNT(*) AS %s FROM (SELECT * FROM %s",
+ QueryBuilder.quote("rowcount"), tableName));
+ if (filters != null && !filters.isEmpty()) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ query.append(")");
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* SELECT without row number constraints */
+ if (offset == 0 && pagelength == 0) {
+ query.append("SELECT ").append(toSelect).append(" FROM ")
+ .append(tableName);
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+ /* Remaining SELECT cases are handled here */
+ query.append(String
+ .format("SELECT * FROM (SELECT x.*, ROWNUM AS %s FROM (SELECT %s FROM %s",
+ QueryBuilder.quote("rownum"), toSelect, tableName));
+ if (filters != null) {
+ query.append(QueryBuilder.getWhereStringForFilters(filters, sh));
+ }
+ if (orderBys != null) {
+ for (OrderBy o : orderBys) {
+ generateOrderBy(query, o, orderBys.indexOf(o) == 0);
+ }
+ }
+ query.append(String.format(") x) WHERE %s BETWEEN %d AND %d",
+ QueryBuilder.quote("rownum"), offset, offset + pagelength));
+ sh.setQueryString(query.toString());
+ return sh;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java
new file mode 100644
index 0000000000..dde7077eee
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java
@@ -0,0 +1,88 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.io.Serializable;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.RowItem;
+import com.vaadin.data.util.sqlcontainer.query.OrderBy;
+
+/**
+ * The SQLGenerator interface is meant to be implemented for each different SQL
+ * syntax that is to be supported. By default there are implementations for
+ * HSQLDB, MySQL, PostgreSQL, MSSQL and Oracle syntaxes.
+ *
+ * @author Jonatan Kronqvist / Vaadin Ltd
+ */
+public interface SQLGenerator extends Serializable {
+ /**
+ * Generates a SELECT query with the provided parameters. Uses default
+ * filtering mode (INCLUSIVE).
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param filters
+ * The filters, converted into a WHERE clause
+ * @param orderBys
+ * The the ordering conditions, converted into an ORDER BY clause
+ * @param offset
+ * The offset of the first row to be included
+ * @param pagelength
+ * The number of rows to be returned when the query executes
+ * @param toSelect
+ * String containing what to select, e.g. "*", "COUNT(*)"
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateSelectQuery(String tableName,
+ List<Filter> filters, List<OrderBy> orderBys, int offset,
+ int pagelength, String toSelect);
+
+ /**
+ * Generates an UPDATE query with the provided parameters.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param item
+ * RowItem containing the updated values update.
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateUpdateQuery(String tableName, RowItem item);
+
+ /**
+ * Generates an INSERT query for inserting a new row with the provided
+ * values.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param item
+ * New RowItem to be inserted into the database.
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateInsertQuery(String tableName, RowItem item);
+
+ /**
+ * Generates a DELETE query for deleting data related to the given RowItem
+ * from the database.
+ *
+ * @param tableName
+ * Name of the table queried
+ * @param primaryKeyColumns
+ * the names of the columns holding the primary key. Usually just
+ * one column, but might be several.
+ * @param versionColumn
+ * the column containing the version number of the row, null if
+ * versioning (optimistic locking) not enabled.
+ * @param item
+ * Item to be deleted from the database
+ * @return StatementHelper instance containing the query string for a
+ * PreparedStatement and the values required for the parameters
+ */
+ public StatementHelper generateDeleteQuery(String tableName,
+ List<String> primaryKeyColumns, String versionColumn, RowItem item);
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java
new file mode 100644
index 0000000000..b012ce7685
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java
@@ -0,0 +1,163 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * StatementHelper is a simple helper class that assists TableQuery and the
+ * query generators in filling a PreparedStatement. The actual statement is
+ * generated by the query generator methods, but the resulting statement and all
+ * the parameter values are stored in an instance of StatementHelper.
+ *
+ * This class will also fill the values with correct setters into the
+ * PreparedStatement on request.
+ */
+public class StatementHelper implements Serializable {
+
+ private String queryString;
+
+ private List<Object> parameters = new ArrayList<Object>();
+ private Map<Integer, Class<?>> dataTypes = new HashMap<Integer, Class<?>>();
+
+ public StatementHelper() {
+ }
+
+ public void setQueryString(String queryString) {
+ this.queryString = queryString;
+ }
+
+ public String getQueryString() {
+ return queryString;
+ }
+
+ public void addParameterValue(Object parameter) {
+ if (parameter != null) {
+ parameters.add(parameter);
+ dataTypes.put(parameters.size() - 1, parameter.getClass());
+ } else {
+ throw new IllegalArgumentException(
+ "You cannot add null parameters using addParamaters(Object). "
+ + "Use addParameters(Object,Class) instead");
+ }
+ }
+
+ public void addParameterValue(Object parameter, Class<?> type) {
+ parameters.add(parameter);
+ dataTypes.put(parameters.size() - 1, type);
+ }
+
+ public void setParameterValuesToStatement(PreparedStatement pstmt)
+ throws SQLException {
+ for (int i = 0; i < parameters.size(); i++) {
+ if (parameters.get(i) == null) {
+ handleNullValue(i, pstmt);
+ } else {
+ pstmt.setObject(i + 1, parameters.get(i));
+ }
+ }
+
+ /*
+ * The following list contains the data types supported by
+ * PreparedStatement but not supported by SQLContainer:
+ *
+ * [The list is provided as PreparedStatement method signatures]
+ *
+ * setNCharacterStream(int parameterIndex, Reader value)
+ *
+ * setNClob(int parameterIndex, NClob value)
+ *
+ * setNString(int parameterIndex, String value)
+ *
+ * setRef(int parameterIndex, Ref x)
+ *
+ * setRowId(int parameterIndex, RowId x)
+ *
+ * setSQLXML(int parameterIndex, SQLXML xmlObject)
+ *
+ * setBytes(int parameterIndex, byte[] x)
+ *
+ * setCharacterStream(int parameterIndex, Reader reader)
+ *
+ * setClob(int parameterIndex, Clob x)
+ *
+ * setURL(int parameterIndex, URL x)
+ *
+ * setArray(int parameterIndex, Array x)
+ *
+ * setAsciiStream(int parameterIndex, InputStream x)
+ *
+ * setBinaryStream(int parameterIndex, InputStream x)
+ *
+ * setBlob(int parameterIndex, Blob x)
+ */
+ }
+
+ private void handleNullValue(int i, PreparedStatement pstmt)
+ throws SQLException {
+ if (BigDecimal.class.equals(dataTypes.get(i))) {
+ pstmt.setBigDecimal(i + 1, null);
+ } else if (Boolean.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.BOOLEAN);
+ } else if (Byte.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.SMALLINT);
+ } else if (Date.class.equals(dataTypes.get(i))) {
+ pstmt.setDate(i + 1, null);
+ } else if (Double.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.DOUBLE);
+ } else if (Float.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.FLOAT);
+ } else if (Integer.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.INTEGER);
+ } else if (Long.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.BIGINT);
+ } else if (Short.class.equals(dataTypes.get(i))) {
+ pstmt.setNull(i + 1, Types.SMALLINT);
+ } else if (String.class.equals(dataTypes.get(i))) {
+ pstmt.setString(i + 1, null);
+ } else if (Time.class.equals(dataTypes.get(i))) {
+ pstmt.setTime(i + 1, null);
+ } else if (Timestamp.class.equals(dataTypes.get(i))) {
+ pstmt.setTimestamp(i + 1, null);
+ } else {
+
+ if (handleUnrecognizedTypeNullValue(i, pstmt, dataTypes)) {
+ return;
+ }
+
+ throw new SQLException("Data type not supported by SQLContainer: "
+ + parameters.get(i).getClass().toString());
+ }
+ }
+
+ /**
+ * Handle unrecognized null values. Override this to handle null values for
+ * platform specific data types that are not handled by the default
+ * implementation of the {@link StatementHelper}.
+ *
+ * @param i
+ * @param pstmt
+ * @param dataTypes2
+ *
+ * @return true if handled, false otherwise
+ *
+ * @see {@link http://dev.vaadin.com/ticket/9148}
+ */
+ protected boolean handleUnrecognizedTypeNullValue(int i,
+ PreparedStatement pstmt, Map<Integer, Class<?>> dataTypes)
+ throws SQLException {
+ return false;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java
new file mode 100644
index 0000000000..251a543a8a
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.And;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class AndTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof And;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ return QueryBuilder.group(QueryBuilder.getJoinedFilterString(
+ ((And) filter).getFilters(), "AND", sh));
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java
new file mode 100644
index 0000000000..4fcaf759ea
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Between;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class BetweenTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Between;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Between between = (Between) filter;
+ sh.addParameterValue(between.getStartValue());
+ sh.addParameterValue(between.getEndValue());
+ return QueryBuilder.quote(between.getPropertyId()) + " BETWEEN ? AND ?";
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java
new file mode 100644
index 0000000000..4293e1d630
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class CompareTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Compare;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Compare compare = (Compare) filter;
+ sh.addParameterValue(compare.getValue());
+ String prop = QueryBuilder.quote(compare.getPropertyId());
+ switch (compare.getOperation()) {
+ case EQUAL:
+ return prop + " = ?";
+ case GREATER:
+ return prop + " > ?";
+ case GREATER_OR_EQUAL:
+ return prop + " >= ?";
+ case LESS:
+ return prop + " < ?";
+ case LESS_OR_EQUAL:
+ return prop + " <= ?";
+ default:
+ return "";
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java
new file mode 100644
index 0000000000..84af9d5c97
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java
@@ -0,0 +1,16 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public interface FilterTranslator extends Serializable {
+ public boolean translatesFilter(Filter filter);
+
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh);
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java
new file mode 100644
index 0000000000..a2a6cd2c09
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java
@@ -0,0 +1,22 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class IsNullTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof IsNull;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ IsNull in = (IsNull) filter;
+ return QueryBuilder.quote(in.getPropertyId()) + " IS NULL";
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java
new file mode 100644
index 0000000000..25a85caec0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class LikeTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Like;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Like like = (Like) filter;
+ if (like.isCaseSensitive()) {
+ sh.addParameterValue(like.getValue());
+ return QueryBuilder.quote(like.getPropertyId()) + " LIKE ?";
+ } else {
+ sh.addParameterValue(like.getValue().toUpperCase());
+ return "UPPER(" + QueryBuilder.quote(like.getPropertyId())
+ + ") LIKE ?";
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java
new file mode 100644
index 0000000000..5dfbe240e7
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.IsNull;
+import com.vaadin.data.util.filter.Not;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class NotTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Not;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ Not not = (Not) filter;
+ if (not.getFilter() instanceof IsNull) {
+ IsNull in = (IsNull) not.getFilter();
+ return QueryBuilder.quote(in.getPropertyId()) + " IS NOT NULL";
+ }
+ return "NOT "
+ + QueryBuilder.getWhereStringForFilter(not.getFilter(), sh);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java
new file mode 100644
index 0000000000..2f0ed814e0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java
@@ -0,0 +1,23 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Or;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class OrTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof Or;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ return QueryBuilder.group(QueryBuilder.getJoinedFilterString(
+ ((Or) filter).getFilters(), "OR", sh));
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java
new file mode 100644
index 0000000000..24be8963e0
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java
@@ -0,0 +1,98 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class QueryBuilder implements Serializable {
+
+ private static ArrayList<FilterTranslator> filterTranslators = new ArrayList<FilterTranslator>();
+ private static StringDecorator stringDecorator = new StringDecorator("\"",
+ "\"");
+
+ static {
+ /* Register all default filter translators */
+ addFilterTranslator(new AndTranslator());
+ addFilterTranslator(new OrTranslator());
+ addFilterTranslator(new LikeTranslator());
+ addFilterTranslator(new BetweenTranslator());
+ addFilterTranslator(new CompareTranslator());
+ addFilterTranslator(new NotTranslator());
+ addFilterTranslator(new IsNullTranslator());
+ addFilterTranslator(new SimpleStringTranslator());
+ }
+
+ public synchronized static void addFilterTranslator(
+ FilterTranslator translator) {
+ filterTranslators.add(translator);
+ }
+
+ /**
+ * Allows specification of a custom ColumnQuoter instance that handles
+ * quoting of column names for the current DB dialect.
+ *
+ * @param decorator
+ * the ColumnQuoter instance to use.
+ */
+ public static void setStringDecorator(StringDecorator decorator) {
+ stringDecorator = decorator;
+ }
+
+ public static String quote(Object str) {
+ return stringDecorator.quote(str);
+ }
+
+ public static String group(String str) {
+ return stringDecorator.group(str);
+ }
+
+ /**
+ * Constructs and returns a string representing the filter that can be used
+ * in a WHERE clause.
+ *
+ * @param filter
+ * the filter to translate
+ * @param sh
+ * the statement helper to update with the value(s) of the filter
+ * @return a string representing the filter.
+ */
+ public synchronized static String getWhereStringForFilter(Filter filter,
+ StatementHelper sh) {
+ for (FilterTranslator ft : filterTranslators) {
+ if (ft.translatesFilter(filter)) {
+ return ft.getWhereStringForFilter(filter, sh);
+ }
+ }
+ return "";
+ }
+
+ public static String getJoinedFilterString(Collection<Filter> filters,
+ String joinString, StatementHelper sh) {
+ StringBuilder result = new StringBuilder();
+ for (Filter f : filters) {
+ result.append(getWhereStringForFilter(f, sh));
+ result.append(" ").append(joinString).append(" ");
+ }
+ // Remove the last instance of joinString
+ result.delete(result.length() - joinString.length() - 2,
+ result.length());
+ return result.toString();
+ }
+
+ public static String getWhereStringForFilters(List<Filter> filters,
+ StatementHelper sh) {
+ if (filters == null || filters.isEmpty()) {
+ return "";
+ }
+ StringBuilder where = new StringBuilder(" WHERE ");
+ where.append(getJoinedFilterString(filters, "AND", sh));
+ return where.toString();
+ }
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java
new file mode 100644
index 0000000000..f108003535
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.util.filter.Like;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper;
+
+public class SimpleStringTranslator implements FilterTranslator {
+
+ @Override
+ public boolean translatesFilter(Filter filter) {
+ return filter instanceof SimpleStringFilter;
+ }
+
+ @Override
+ public String getWhereStringForFilter(Filter filter, StatementHelper sh) {
+ SimpleStringFilter ssf = (SimpleStringFilter) filter;
+ // Create a Like filter based on the SimpleStringFilter and execute the
+ // LikeTranslator
+ String likeStr = ssf.isOnlyMatchPrefix() ? ssf.getFilterString() + "%"
+ : "%" + ssf.getFilterString() + "%";
+ Like like = new Like(ssf.getPropertyId().toString(), likeStr);
+ like.setCaseSensitive(!ssf.isIgnoreCase());
+ return new LikeTranslator().getWhereStringForFilter(like, sh);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java
new file mode 100644
index 0000000000..8d2eabb5bc
--- /dev/null
+++ b/server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.util.sqlcontainer.query.generator.filter;
+
+import java.io.Serializable;
+
+/**
+ * The StringDecorator knows how to produce a quoted string using the specified
+ * quote start and quote end characters. It also handles grouping of a string
+ * (surrounding it in parenthesis).
+ *
+ * Extend this class if you need to support special characters for grouping
+ * (parenthesis).
+ *
+ * @author Vaadin Ltd
+ */
+public class StringDecorator implements Serializable {
+
+ private final String quoteStart;
+ private final String quoteEnd;
+
+ /**
+ * Constructs a StringDecorator that uses the quoteStart and quoteEnd
+ * characters to create quoted strings.
+ *
+ * @param quoteStart
+ * the character denoting the start of a quote.
+ * @param quoteEnd
+ * the character denoting the end of a quote.
+ */
+ public StringDecorator(String quoteStart, String quoteEnd) {
+ this.quoteStart = quoteStart;
+ this.quoteEnd = quoteEnd;
+ }
+
+ /**
+ * Surround a string with quote characters.
+ *
+ * @param str
+ * the string to quote
+ * @return the quoted string
+ */
+ public String quote(Object str) {
+ return quoteStart + str + quoteEnd;
+ }
+
+ /**
+ * Groups a string by surrounding it in parenthesis
+ *
+ * @param str
+ * the string to group
+ * @return the grouped string
+ */
+ public String group(String str) {
+ return "(" + str + ")";
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/AbstractStringValidator.java b/server/src/com/vaadin/data/validator/AbstractStringValidator.java
new file mode 100644
index 0000000000..5267cc7b7b
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/AbstractStringValidator.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator base class for validating strings.
+ * <p>
+ * To include the value that failed validation in the exception message you can
+ * use "{0}" in the error message. This will be replaced with the failed value
+ * (converted to string using {@link #toString()}) or "null" if the value is
+ * null.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractStringValidator extends AbstractValidator<String> {
+
+ /**
+ * Constructs a validator for strings.
+ *
+ * <p>
+ * Null and empty string values are always accepted. To reject empty values,
+ * set the field being validated as required.
+ * </p>
+ *
+ * @param errorMessage
+ * the message to be included in an {@link InvalidValueException}
+ * (with "{0}" replaced by the value that failed validation).
+ * */
+ public AbstractStringValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/AbstractValidator.java b/server/src/com/vaadin/data/validator/AbstractValidator.java
new file mode 100644
index 0000000000..8febe5338a
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/AbstractValidator.java
@@ -0,0 +1,139 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import com.vaadin.data.Validator;
+
+/**
+ * Abstract {@link com.vaadin.data.Validator Validator} implementation that
+ * provides a basic Validator implementation except the
+ * {@link #isValidValue(Object)} method.
+ * <p>
+ * To include the value that failed validation in the exception message you can
+ * use "{0}" in the error message. This will be replaced with the failed value
+ * (converted to string using {@link #toString()}) or "null" if the value is
+ * null.
+ * </p>
+ * <p>
+ * The default implementation of AbstractValidator does not support HTML in
+ * error messages. To enable HTML support, override
+ * {@link InvalidValueException#getHtmlMessage()} and throw such exceptions from
+ * {@link #validate(Object)}.
+ * </p>
+ * <p>
+ * Since Vaadin 7, subclasses can either implement {@link #validate(Object)}
+ * directly or implement {@link #isValidValue(Object)} when migrating legacy
+ * applications. To check validity, {@link #validate(Object)} should be used.
+ * </p>
+ *
+ * @param <T>
+ * The type
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+public abstract class AbstractValidator<T> implements Validator {
+
+ /**
+ * Error message that is included in an {@link InvalidValueException} if
+ * such is thrown.
+ */
+ private String errorMessage;
+
+ /**
+ * Constructs a validator with the given error message.
+ *
+ * @param errorMessage
+ * the message to be included in an {@link InvalidValueException}
+ * (with "{0}" replaced by the value that failed validation).
+ */
+ public AbstractValidator(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ /**
+ * Since Vaadin 7, subclasses of AbstractValidator should override
+ * {@link #isValidValue(Object)} or {@link #validate(Object)} instead of
+ * {@link #isValid(Object)}. {@link #validate(Object)} should normally be
+ * used to check values.
+ *
+ * @param value
+ * @return true if the value is valid
+ */
+ public boolean isValid(Object value) {
+ try {
+ validate(value);
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Internally check the validity of a value. This method can be used to
+ * perform validation in subclasses if customization of the error message is
+ * not needed. Otherwise, subclasses should override
+ * {@link #validate(Object)} and the return value of this method is ignored.
+ *
+ * This method should not be called from outside the validator class itself.
+ *
+ * @param value
+ * @return
+ */
+ protected abstract boolean isValidValue(T value);
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ // isValidType ensures that value can safely be cast to TYPE
+ if (!isValidType(value) || !isValidValue((T) value)) {
+ String message = getErrorMessage().replace("{0}",
+ String.valueOf(value));
+ throw new InvalidValueException(message);
+ }
+ }
+
+ /**
+ * Checks the type of the value to validate to ensure it conforms with
+ * getType. Enables sub classes to handle the specific type instead of
+ * Object.
+ *
+ * @param value
+ * The value to check
+ * @return true if the value can safely be cast to the type specified by
+ * {@link #getType()}
+ */
+ protected boolean isValidType(Object value) {
+ if (value == null) {
+ return true;
+ }
+
+ return getType().isAssignableFrom(value.getClass());
+ }
+
+ /**
+ * Returns the message to be included in the exception in case the value
+ * does not validate.
+ *
+ * @return the error message provided in the constructor or using
+ * {@link #setErrorMessage(String)}.
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Sets the message to be included in the exception in case the value does
+ * not validate. The exception message is typically shown to the end user.
+ *
+ * @param errorMessage
+ * the error message. "{0}" is automatically replaced by the
+ * value that did not validate.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+ public abstract Class<T> getType();
+}
diff --git a/server/src/com/vaadin/data/validator/BeanValidator.java b/server/src/com/vaadin/data/validator/BeanValidator.java
new file mode 100644
index 0000000000..816ff79b83
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/BeanValidator.java
@@ -0,0 +1,176 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.MessageInterpolator.Context;
+import javax.validation.Validation;
+import javax.validation.ValidatorFactory;
+import javax.validation.metadata.ConstraintDescriptor;
+
+import com.vaadin.data.Validator;
+
+/**
+ * Vaadin {@link Validator} using the JSR-303 (javax.validation)
+ * annotation-based bean validation.
+ *
+ * The annotations of the fields of the beans are used to determine the
+ * validation to perform.
+ *
+ * Note that a JSR-303 implementation (e.g. Hibernate Validator or Apache Bean
+ * Validation - formerly agimatec validation) must be present on the project
+ * classpath when using bean validation.
+ *
+ * @since 7.0
+ *
+ * @author Petri Hakala
+ * @author Henri Sara
+ */
+public class BeanValidator implements Validator {
+
+ private static final long serialVersionUID = 1L;
+ private static ValidatorFactory factory;
+
+ private transient javax.validation.Validator javaxBeanValidator;
+ private String propertyName;
+ private Class<?> beanClass;
+ private Locale locale;
+
+ /**
+ * Simple implementation of a message interpolator context that returns
+ * fixed values.
+ */
+ protected static class SimpleContext implements Context, Serializable {
+
+ private final Object value;
+ private final ConstraintDescriptor<?> descriptor;
+
+ /**
+ * Create a simple immutable message interpolator context.
+ *
+ * @param value
+ * value being validated
+ * @param descriptor
+ * ConstraintDescriptor corresponding to the constraint being
+ * validated
+ */
+ public SimpleContext(Object value, ConstraintDescriptor<?> descriptor) {
+ this.value = value;
+ this.descriptor = descriptor;
+ }
+
+ @Override
+ public ConstraintDescriptor<?> getConstraintDescriptor() {
+ return descriptor;
+ }
+
+ @Override
+ public Object getValidatedValue() {
+ return value;
+ }
+
+ }
+
+ /**
+ * Creates a Vaadin {@link Validator} utilizing JSR-303 bean validation.
+ *
+ * @param beanClass
+ * bean class based on which the validation should be performed
+ * @param propertyName
+ * property to validate
+ */
+ public BeanValidator(Class<?> beanClass, String propertyName) {
+ this.beanClass = beanClass;
+ this.propertyName = propertyName;
+ locale = Locale.getDefault();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Validator#validate(java.lang.Object)
+ */
+ @Override
+ public void validate(final Object value) throws InvalidValueException {
+ Set<?> violations = getJavaxBeanValidator().validateValue(beanClass,
+ propertyName, value);
+ if (violations.size() > 0) {
+ List<String> exceptions = new ArrayList<String>();
+ for (Object v : violations) {
+ final ConstraintViolation<?> violation = (ConstraintViolation<?>) v;
+ String msg = getJavaxBeanValidatorFactory()
+ .getMessageInterpolator().interpolate(
+ violation.getMessageTemplate(),
+ new SimpleContext(value, violation
+ .getConstraintDescriptor()), locale);
+ exceptions.add(msg);
+ }
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < exceptions.size(); i++) {
+ if (i != 0) {
+ b.append("<br/>");
+ }
+ b.append(exceptions.get(i));
+ }
+ throw new InvalidValueException(b.toString());
+ }
+ }
+
+ /**
+ * Sets the locale used for validation error messages.
+ *
+ * Revalidation is not automatically triggered by setting the locale.
+ *
+ * @param locale
+ */
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * Gets the locale used for validation error messages.
+ *
+ * @return locale used for validation
+ */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * Returns the underlying JSR-303 bean validator factory used. A factory is
+ * created using {@link Validation} if necessary.
+ *
+ * @return {@link ValidatorFactory} to use
+ */
+ protected static ValidatorFactory getJavaxBeanValidatorFactory() {
+ if (factory == null) {
+ factory = Validation.buildDefaultValidatorFactory();
+ }
+
+ return factory;
+ }
+
+ /**
+ * Returns a shared Validator instance to use. An instance is created using
+ * the validator factory if necessary and thereafter reused by the
+ * {@link BeanValidator} instance.
+ *
+ * @return the JSR-303 {@link javax.validation.Validator} to use
+ */
+ protected javax.validation.Validator getJavaxBeanValidator() {
+ if (javaxBeanValidator == null) {
+ javaxBeanValidator = getJavaxBeanValidatorFactory().getValidator();
+ }
+
+ return javaxBeanValidator;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/data/validator/CompositeValidator.java b/server/src/com/vaadin/data/validator/CompositeValidator.java
new file mode 100644
index 0000000000..cad31c9d4d
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/CompositeValidator.java
@@ -0,0 +1,259 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.data.Validator;
+
+/**
+ * The <code>CompositeValidator</code> allows you to chain (compose) many
+ * validators to validate one field. The contained validators may be required to
+ * all validate the value to validate or it may be enough that one contained
+ * validator validates the value. This behaviour is controlled by the modes
+ * <code>AND</code> and <code>OR</code>.
+ *
+ * @author Vaadin Ltd.
+ * @version @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class CompositeValidator implements Validator {
+
+ public enum CombinationMode {
+ /**
+ * The validators are combined with <code>AND</code> clause: validity of
+ * the composite implies validity of the all validators it is composed
+ * of must be valid.
+ */
+ AND,
+ /**
+ * The validators are combined with <code>OR</code> clause: validity of
+ * the composite implies that some of validators it is composed of must
+ * be valid.
+ */
+ OR;
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link CombinationMode#AND} instead    
+ */
+ @Deprecated
+ public static final CombinationMode MODE_AND = CombinationMode.AND;
+ /**
+ * @deprecated from 7.0, use {@link CombinationMode#OR} instead    
+ */
+ @Deprecated
+ public static final CombinationMode MODE_OR = CombinationMode.OR;
+
+ private String errorMessage;
+
+ /**
+ * Operation mode.
+ */
+ private CombinationMode mode = CombinationMode.AND;
+
+ /**
+ * List of contained validators.
+ */
+ private final List<Validator> validators = new LinkedList<Validator>();
+
+ /**
+ * Construct a composite validator in <code>AND</code> mode without error
+ * message.
+ */
+ public CompositeValidator() {
+ this(CombinationMode.AND, "");
+ }
+
+ /**
+ * Constructs a composite validator in given mode.
+ *
+ * @param mode
+ * @param errorMessage
+ */
+ public CompositeValidator(CombinationMode mode, String errorMessage) {
+ setErrorMessage(errorMessage);
+ setMode(mode);
+ }
+
+ /**
+ * Validates the given value.
+ * <p>
+ * The value is valid, if:
+ * <ul>
+ * <li><code>MODE_AND</code>: All of the sub-validators are valid
+ * <li><code>MODE_OR</code>: Any of the sub-validators are valid
+ * </ul>
+ *
+ * If the value is invalid, validation error is thrown. If the error message
+ * is set (non-null), it is used. If the error message has not been set, the
+ * first error occurred is thrown.
+ * </p>
+ *
+ * @param value
+ * the value to check.
+ * @throws Validator.InvalidValueException
+ * if the value is not valid.
+ */
+ @Override
+ public void validate(Object value) throws Validator.InvalidValueException {
+ switch (mode) {
+ case AND:
+ for (Validator validator : validators) {
+ validator.validate(value);
+ }
+ return;
+
+ case OR:
+ Validator.InvalidValueException first = null;
+ for (Validator v : validators) {
+ try {
+ v.validate(value);
+ return;
+ } catch (final Validator.InvalidValueException e) {
+ if (first == null) {
+ first = e;
+ }
+ }
+ }
+ if (first == null) {
+ return;
+ }
+ final String em = getErrorMessage();
+ if (em != null) {
+ throw new Validator.InvalidValueException(em);
+ } else {
+ throw first;
+ }
+ }
+ }
+
+ /**
+ * Gets the mode of the validator.
+ *
+ * @return Operation mode of the validator: {@link CombinationMode#AND} or
+ * {@link CombinationMode#OR}.
+ */
+ public final CombinationMode getMode() {
+ return mode;
+ }
+
+ /**
+ * Sets the mode of the validator. The valid modes are:
+ * <ul>
+ * <li>{@link CombinationMode#AND} (default)
+ * <li>{@link CombinationMode#OR}
+ * </ul>
+ *
+ * @param mode
+ * the mode to set.
+ */
+ public void setMode(CombinationMode mode) {
+ if (mode == null) {
+ throw new IllegalArgumentException(
+ "The validator can't be set to null");
+ }
+ this.mode = mode;
+ }
+
+ /**
+ * Gets the error message for the composite validator. If the error message
+ * is null, original error messages of the sub-validators are used instead.
+ */
+ public String getErrorMessage() {
+ if (errorMessage != null) {
+ return errorMessage;
+ }
+
+ // TODO Return composite error message
+
+ return null;
+ }
+
+ /**
+ * Adds validator to the interface.
+ *
+ * @param validator
+ * the Validator object which performs validation checks on this
+ * set of data field values.
+ */
+ public void addValidator(Validator validator) {
+ if (validator == null) {
+ return;
+ }
+ validators.add(validator);
+ }
+
+ /**
+ * Removes a validator from the composite.
+ *
+ * @param validator
+ * the Validator object which performs validation checks on this
+ * set of data field values.
+ */
+ public void removeValidator(Validator validator) {
+ validators.remove(validator);
+ }
+
+ /**
+ * Gets sub-validators by class.
+ *
+ * <p>
+ * If the component contains directly or recursively (it contains another
+ * composite containing the validator) validators compatible with given type
+ * they are returned. This only applies to <code>AND</code> mode composite
+ * validators.
+ * </p>
+ *
+ * <p>
+ * If the validator is in <code>OR</code> mode or does not contain any
+ * validators of given type null is returned.
+ * </p>
+ *
+ * @param validatorType
+ * The type of validators to return
+ *
+ * @return Collection<Validator> of validators compatible with given type
+ * that must apply or null if none found.
+ */
+ public Collection<Validator> getSubValidators(Class validatorType) {
+ if (mode != CombinationMode.AND) {
+ return null;
+ }
+
+ final HashSet<Validator> found = new HashSet<Validator>();
+ for (Validator v : validators) {
+ if (validatorType.isAssignableFrom(v.getClass())) {
+ found.add(v);
+ }
+ if (v instanceof CompositeValidator
+ && ((CompositeValidator) v).getMode() == MODE_AND) {
+ final Collection<Validator> c = ((CompositeValidator) v)
+ .getSubValidators(validatorType);
+ if (c != null) {
+ found.addAll(c);
+ }
+ }
+ }
+
+ return found.isEmpty() ? null : found;
+ }
+
+ /**
+ * Sets the message to be included in the exception in case the value does
+ * not validate. The exception message is typically shown to the end user.
+ *
+ * @param errorMessage
+ * the error message.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DateRangeValidator.java b/server/src/com/vaadin/data/validator/DateRangeValidator.java
new file mode 100644
index 0000000000..24f3d3ce10
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DateRangeValidator.java
@@ -0,0 +1,51 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import java.util.Date;
+
+import com.vaadin.ui.DateField.Resolution;
+
+/**
+ * Validator for validating that a Date is inside a given range.
+ *
+ * <p>
+ * Note that the comparison is done directly on the Date object so take care
+ * that the hours/minutes/seconds/milliseconds of the min/max values are
+ * properly set.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class DateRangeValidator extends RangeValidator<Date> {
+
+ /**
+ * Creates a validator for checking that an Date is within a given range.
+ * <p>
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ * </p>
+ * <p>
+ * Note that the comparison is done directly on the Date object so take care
+ * that the hours/minutes/seconds/milliseconds of the min/max values are
+ * properly set.
+ * </p>
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public DateRangeValidator(String errorMessage, Date minValue,
+ Date maxValue, Resolution resolution) {
+ super(errorMessage, Date.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DoubleRangeValidator.java b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java
new file mode 100644
index 0000000000..05ae2f827e
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DoubleRangeValidator.java
@@ -0,0 +1,37 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator for validating that a {@link Double} is inside a given range.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+@SuppressWarnings("serial")
+public class DoubleRangeValidator extends RangeValidator<Double> {
+
+ /**
+ * Creates a validator for checking that an Double is within a given range.
+ *
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ *
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public DoubleRangeValidator(String errorMessage, Double minValue,
+ Double maxValue) {
+ super(errorMessage, Double.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/DoubleValidator.java b/server/src/com/vaadin/data/validator/DoubleValidator.java
new file mode 100644
index 0000000000..18f1909add
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/DoubleValidator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for a double precision floating point number. See
+ * {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ * @deprecated in Vaadin 7.0. Use an Double converter on the field instead.
+ */
+@Deprecated
+@SuppressWarnings("serial")
+public class DoubleValidator extends AbstractStringValidator {
+
+ /**
+ * Creates a validator for checking that a string can be parsed as an
+ * double.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @deprecated in Vaadin 7.0. Use a Double converter on the field instead
+ * and/or use a {@link DoubleRangeValidator} for validating that
+ * the value is inside a given range.
+ */
+ @Deprecated
+ public DoubleValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ @Override
+ protected boolean isValidValue(String value) {
+ try {
+ Double.parseDouble(value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ if (value != null && value instanceof Double) {
+ // Allow Doubles to pass through the validator for easier
+ // migration. Otherwise a TextField connected to an double property
+ // with a DoubleValidator will fail.
+ return;
+ }
+
+ super.validate(value);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/EmailValidator.java b/server/src/com/vaadin/data/validator/EmailValidator.java
new file mode 100644
index 0000000000..c76d7e13dc
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/EmailValidator.java
@@ -0,0 +1,35 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for e-mail addresses. The e-mail address syntax is not
+ * complete according to RFC 822 but handles the vast majority of valid e-mail
+ * addresses correctly.
+ *
+ * See {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class EmailValidator extends RegexpValidator {
+
+ /**
+ * Creates a validator for checking that a string is a syntactically valid
+ * e-mail address.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public EmailValidator(String errorMessage) {
+ super(
+ "^([a-zA-Z0-9_\\.\\-+])+@(([a-zA-Z0-9-])+\\.)+([a-zA-Z0-9]{2,4})+$",
+ true, errorMessage);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/IntegerRangeValidator.java b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java
new file mode 100644
index 0000000000..c171dd97d8
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/IntegerRangeValidator.java
@@ -0,0 +1,37 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * Validator for validating that an {@link Integer} is inside a given range.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class IntegerRangeValidator extends RangeValidator<Integer> {
+
+ /**
+ * Creates a validator for checking that an Integer is within a given range.
+ *
+ * By default the range is inclusive i.e. both minValue and maxValue are
+ * valid values. Use {@link #setMinValueIncluded(boolean)} or
+ * {@link #setMaxValueIncluded(boolean)} to change it.
+ *
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minValue
+ * The minimum value to accept or null for no limit
+ * @param maxValue
+ * The maximum value to accept or null for no limit
+ */
+ public IntegerRangeValidator(String errorMessage, Integer minValue,
+ Integer maxValue) {
+ super(errorMessage, Integer.class, minValue, maxValue);
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/IntegerValidator.java b/server/src/com/vaadin/data/validator/IntegerValidator.java
new file mode 100644
index 0000000000..88ae9f3f0b
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/IntegerValidator.java
@@ -0,0 +1,58 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * String validator for integers. See
+ * {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead.
+ */
+@SuppressWarnings("serial")
+@Deprecated
+public class IntegerValidator extends AbstractStringValidator {
+
+ /**
+ * Creates a validator for checking that a string can be parsed as an
+ * integer.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @deprecated in Vaadin 7.0. Use an Integer converter on the field instead
+ * and/or use an {@link IntegerRangeValidator} for validating
+ * that the value is inside a given range.
+ */
+ @Deprecated
+ public IntegerValidator(String errorMessage) {
+ super(errorMessage);
+
+ }
+
+ @Override
+ protected boolean isValidValue(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void validate(Object value) throws InvalidValueException {
+ if (value != null && value instanceof Integer) {
+ // Allow Integers to pass through the validator for easier
+ // migration. Otherwise a TextField connected to an integer property
+ // with an IntegerValidator will fail.
+ return;
+ }
+
+ super.validate(value);
+ }
+}
diff --git a/server/src/com/vaadin/data/validator/NullValidator.java b/server/src/com/vaadin/data/validator/NullValidator.java
new file mode 100644
index 0000000000..551d88c776
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/NullValidator.java
@@ -0,0 +1,92 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+import com.vaadin.data.Validator;
+
+/**
+ * This validator is used for validating properties that do or do not allow null
+ * values. By default, nulls are not allowed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class NullValidator implements Validator {
+
+ private boolean onlyNullAllowed;
+
+ private String errorMessage;
+
+ /**
+ * Creates a new NullValidator.
+ *
+ * @param errorMessage
+ * the error message to display on invalidation.
+ * @param onlyNullAllowed
+ * Are only nulls allowed?
+ */
+ public NullValidator(String errorMessage, boolean onlyNullAllowed) {
+ setErrorMessage(errorMessage);
+ setNullAllowed(onlyNullAllowed);
+ }
+
+ /**
+ * Validates the data given in value.
+ *
+ * @param value
+ * the value to validate.
+ * @throws Validator.InvalidValueException
+ * if the value was invalid.
+ */
+ @Override
+ public void validate(Object value) throws Validator.InvalidValueException {
+ if ((onlyNullAllowed && value != null)
+ || (!onlyNullAllowed && value == null)) {
+ throw new Validator.InvalidValueException(errorMessage);
+ }
+ }
+
+ /**
+ * Returns <code>true</code> if nulls are allowed otherwise
+ * <code>false</code>.
+ */
+ public final boolean isNullAllowed() {
+ return onlyNullAllowed;
+ }
+
+ /**
+ * Sets if nulls (and only nulls) are to be allowed.
+ *
+ * @param onlyNullAllowed
+ * If true, only nulls are allowed. If false only non-nulls are
+ * allowed. Do we allow nulls?
+ */
+ public void setNullAllowed(boolean onlyNullAllowed) {
+ this.onlyNullAllowed = onlyNullAllowed;
+ }
+
+ /**
+ * Gets the error message that is displayed in case the value is invalid.
+ *
+ * @return the Error Message.
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Sets the error message to be displayed on invalid value.
+ *
+ * @param errorMessage
+ * the Error Message to set.
+ */
+ public void setErrorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/RangeValidator.java b/server/src/com/vaadin/data/validator/RangeValidator.java
new file mode 100644
index 0000000000..433271274f
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/RangeValidator.java
@@ -0,0 +1,186 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+/**
+ * An base implementation for validating any objects that implement
+ * {@link Comparable}.
+ *
+ * Verifies that the value is of the given type and within the (optionally)
+ * given limits. Typically you want to use a sub class of this like
+ * {@link IntegerRangeValidator}, {@link DoubleRangeValidator} or
+ * {@link DateRangeValidator} in applications.
+ * <p>
+ * Note that {@link RangeValidator} always accept null values. Make a field
+ * required to ensure that no empty values are accepted or override
+ * {@link #isValidValue(Comparable)}.
+ * </p>
+ *
+ * @param <T>
+ * The type of Number to validate. Must implement Comparable so that
+ * minimum and maximum checks work.
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+public class RangeValidator<T extends Comparable> extends AbstractValidator<T> {
+
+ private T minValue = null;
+ private boolean minValueIncluded = true;
+ private T maxValue = null;
+ private boolean maxValueIncluded = true;
+ private Class<T> type;
+
+ /**
+ * Creates a new range validator of the given type.
+ *
+ * @param errorMessage
+ * The error message to use if validation fails
+ * @param type
+ * The type of object the validator can validate.
+ * @param minValue
+ * The minimum value that should be accepted or null for no limit
+ * @param maxValue
+ * The maximum value that should be accepted or null for no limit
+ */
+ public RangeValidator(String errorMessage, Class<T> type, T minValue,
+ T maxValue) {
+ super(errorMessage);
+ this.type = type;
+ this.minValue = minValue;
+ this.maxValue = maxValue;
+ }
+
+ /**
+ * Checks if the minimum value is part of the accepted range
+ *
+ * @return true if the minimum value is part of the range, false otherwise
+ */
+ public boolean isMinValueIncluded() {
+ return minValueIncluded;
+ }
+
+ /**
+ * Sets if the minimum value is part of the accepted range
+ *
+ * @param minValueIncluded
+ * true if the minimum value should be part of the range, false
+ * otherwise
+ */
+ public void setMinValueIncluded(boolean minValueIncluded) {
+ this.minValueIncluded = minValueIncluded;
+ }
+
+ /**
+ * Checks if the maximum value is part of the accepted range
+ *
+ * @return true if the maximum value is part of the range, false otherwise
+ */
+ public boolean isMaxValueIncluded() {
+ return maxValueIncluded;
+ }
+
+ /**
+ * Sets if the maximum value is part of the accepted range
+ *
+ * @param maxValueIncluded
+ * true if the maximum value should be part of the range, false
+ * otherwise
+ */
+ public void setMaxValueIncluded(boolean maxValueIncluded) {
+ this.maxValueIncluded = maxValueIncluded;
+ }
+
+ /**
+ * Gets the minimum value of the range
+ *
+ * @return the minimum value
+ */
+ public T getMinValue() {
+ return minValue;
+ }
+
+ /**
+ * Sets the minimum value of the range. Use
+ * {@link #setMinValueIncluded(boolean)} to control whether this value is
+ * part of the range or not.
+ *
+ * @param minValue
+ * the minimum value
+ */
+ public void setMinValue(T minValue) {
+ this.minValue = minValue;
+ }
+
+ /**
+ * Gets the maximum value of the range
+ *
+ * @return the maximum value
+ */
+ public T getMaxValue() {
+ return maxValue;
+ }
+
+ /**
+ * Sets the maximum value of the range. Use
+ * {@link #setMaxValueIncluded(boolean)} to control whether this value is
+ * part of the range or not.
+ *
+ * @param maxValue
+ * the maximum value
+ */
+ public void setMaxValue(T maxValue) {
+ this.maxValue = maxValue;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object
+ * )
+ */
+ @Override
+ protected boolean isValidValue(T value) {
+ if (value == null) {
+ return true;
+ }
+
+ if (getMinValue() != null) {
+ // Ensure that the min limit is ok
+ int result = value.compareTo(getMinValue());
+ if (result < 0) {
+ // value less than min value
+ return false;
+ } else if (result == 0 && !isMinValueIncluded()) {
+ // values equal and min value not included
+ return false;
+ }
+ }
+ if (getMaxValue() != null) {
+ // Ensure that the Max limit is ok
+ int result = value.compareTo(getMaxValue());
+ if (result > 0) {
+ // value greater than max value
+ return false;
+ } else if (result == 0 && !isMaxValueIncluded()) {
+ // values equal and max value not included
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.validator.AbstractValidator#getType()
+ */
+ @Override
+ public Class<T> getType() {
+ return type;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/RegexpValidator.java b/server/src/com/vaadin/data/validator/RegexpValidator.java
new file mode 100644
index 0000000000..8143d54c97
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/RegexpValidator.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.data.validator;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * String validator comparing the string against a Java regular expression. Both
+ * complete matches and substring matches are supported.
+ *
+ * <p>
+ * For the Java regular expression syntax, see
+ * {@link java.util.regex.Pattern#sum}
+ * </p>
+ * <p>
+ * See {@link com.vaadin.data.validator.AbstractStringValidator} for more
+ * information.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.4
+ */
+@SuppressWarnings("serial")
+public class RegexpValidator extends AbstractStringValidator {
+
+ private Pattern pattern;
+ private boolean complete;
+ private transient Matcher matcher = null;
+
+ /**
+ * Creates a validator for checking that the regular expression matches the
+ * complete string to validate.
+ *
+ * @param regexp
+ * a Java regular expression
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public RegexpValidator(String regexp, String errorMessage) {
+ this(regexp, true, errorMessage);
+ }
+
+ /**
+ * Creates a validator for checking that the regular expression matches the
+ * string to validate.
+ *
+ * @param regexp
+ * a Java regular expression
+ * @param complete
+ * true to use check for a complete match, false to look for a
+ * matching substring
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public RegexpValidator(String regexp, boolean complete, String errorMessage) {
+ super(errorMessage);
+ pattern = Pattern.compile(regexp);
+ this.complete = complete;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.validator.AbstractValidator#isValidValue(java.lang.Object
+ * )
+ */
+ @Override
+ protected boolean isValidValue(String value) {
+ if (complete) {
+ return getMatcher(value).matches();
+ } else {
+ return getMatcher(value).find();
+ }
+ }
+
+ /**
+ * Get a new or reused matcher for the pattern
+ *
+ * @param value
+ * the string to find matches in
+ * @return Matcher for the string
+ */
+ private Matcher getMatcher(String value) {
+ if (matcher == null) {
+ matcher = pattern.matcher(value);
+ } else {
+ matcher.reset(value);
+ }
+ return matcher;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/StringLengthValidator.java b/server/src/com/vaadin/data/validator/StringLengthValidator.java
new file mode 100644
index 0000000000..54b2d28f58
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/StringLengthValidator.java
@@ -0,0 +1,139 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.data.validator;
+
+/**
+ * This <code>StringLengthValidator</code> is used to validate the length of
+ * strings.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class StringLengthValidator extends AbstractStringValidator {
+
+ private Integer minLength = null;
+
+ private Integer maxLength = null;
+
+ private boolean allowNull = true;
+
+ /**
+ * Creates a new StringLengthValidator with a given error message.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ */
+ public StringLengthValidator(String errorMessage) {
+ super(errorMessage);
+ }
+
+ /**
+ * Creates a new StringLengthValidator with a given error message and
+ * minimum and maximum length limits.
+ *
+ * @param errorMessage
+ * the message to display in case the value does not validate.
+ * @param minLength
+ * the minimum permissible length of the string or null for no
+ * limit. A negative value for no limit is also supported for
+ * backwards compatibility.
+ * @param maxLength
+ * the maximum permissible length of the string or null for no
+ * limit. A negative value for no limit is also supported for
+ * backwards compatibility.
+ * @param allowNull
+ * Are null strings permissible? This can be handled better by
+ * setting a field as required or not.
+ */
+ public StringLengthValidator(String errorMessage, Integer minLength,
+ Integer maxLength, boolean allowNull) {
+ this(errorMessage);
+ setMinLength(minLength);
+ setMaxLength(maxLength);
+ setNullAllowed(allowNull);
+ }
+
+ /**
+ * Checks if the given value is valid.
+ *
+ * @param value
+ * the value to validate.
+ * @return <code>true</code> for valid value, otherwise <code>false</code>.
+ */
+ @Override
+ protected boolean isValidValue(String value) {
+ if (value == null) {
+ return allowNull;
+ }
+ final int len = value.length();
+ if ((minLength != null && minLength > -1 && len < minLength)
+ || (maxLength != null && maxLength > -1 && len > maxLength)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if null strings are allowed.
+ *
+ * @return <code>true</code> if allows null string, otherwise
+ * <code>false</code>.
+ */
+ @Deprecated
+ public final boolean isNullAllowed() {
+ return allowNull;
+ }
+
+ /**
+ * Gets the maximum permissible length of the string.
+ *
+ * @return the maximum length of the string or null if there is no limit
+ */
+ public Integer getMaxLength() {
+ return maxLength;
+ }
+
+ /**
+ * Gets the minimum permissible length of the string.
+ *
+ * @return the minimum length of the string or null if there is no limit
+ */
+ public Integer getMinLength() {
+ return minLength;
+ }
+
+ /**
+ * Sets whether null-strings are to be allowed. This can be better handled
+ * by setting a field as required or not.
+ */
+ @Deprecated
+ public void setNullAllowed(boolean allowNull) {
+ this.allowNull = allowNull;
+ }
+
+ /**
+ * Sets the maximum permissible length of the string.
+ *
+ * @param maxLength
+ * the maximum length to accept or null for no limit
+ */
+ public void setMaxLength(Integer maxLength) {
+ this.maxLength = maxLength;
+ }
+
+ /**
+ * Sets the minimum permissible length.
+ *
+ * @param minLength
+ * the minimum length to accept or null for no limit
+ */
+ public void setMinLength(Integer minLength) {
+ this.minLength = minLength;
+ }
+
+}
diff --git a/server/src/com/vaadin/data/validator/package.html b/server/src/com/vaadin/data/validator/package.html
new file mode 100644
index 0000000000..c991bfc82a
--- /dev/null
+++ b/server/src/com/vaadin/data/validator/package.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides various {@link com.vaadin.data.Validator}
+implementations.</p>
+
+<p>{@link com.vaadin.data.validator.AbstractValidator
+AbstractValidator} provides an abstract implementation of the {@link
+com.vaadin.data.Validator} interface and can be extended for custom
+validation needs. {@link
+com.vaadin.data.validator.AbstractStringValidator
+AbstractStringValidator} can also be extended if the value is a String.</p>
+
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/event/Action.java b/server/src/com/vaadin/event/Action.java
new file mode 100644
index 0000000000..6c218c25dc
--- /dev/null
+++ b/server/src/com/vaadin/event/Action.java
@@ -0,0 +1,195 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.Resource;
+
+/**
+ * Implements the action framework. This class contains subinterfaces for action
+ * handling and listing, and for action handler registrations and
+ * unregistration.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Action implements Serializable {
+
+ /**
+ * Action title.
+ */
+ private String caption;
+
+ /**
+ * Action icon.
+ */
+ private Resource icon = null;
+
+ /**
+ * Constructs a new action with the given caption.
+ *
+ * @param caption
+ * the caption for the new action.
+ */
+ public Action(String caption) {
+ this.caption = caption;
+ }
+
+ /**
+ * Constructs a new action with the given caption string and icon.
+ *
+ * @param caption
+ * the caption for the new action.
+ * @param icon
+ * the icon for the new action.
+ */
+ public Action(String caption, Resource icon) {
+ this.caption = caption;
+ this.icon = icon;
+ }
+
+ /**
+ * Returns the action's caption.
+ *
+ * @return the action's caption as a <code>String</code>.
+ */
+ public String getCaption() {
+ return caption;
+ }
+
+ /**
+ * Returns the action's icon.
+ *
+ * @return the action's Icon.
+ */
+ public Resource getIcon() {
+ return icon;
+ }
+
+ /**
+ * An Action that implements this interface can be added to an
+ * Action.Notifier (or NotifierProxy) via the <code>addAction()</code>
+ * -method, which in many cases is easier than implementing the
+ * Action.Handler interface.<br/>
+ *
+ */
+ public interface Listener extends Serializable {
+ public void handleAction(Object sender, Object target);
+ }
+
+ /**
+ * Action.Containers implementing this support an easier way of adding
+ * single Actions than the more involved Action.Handler. The added actions
+ * must be Action.Listeners, thus handling the action themselves.
+ *
+ */
+ public interface Notifier extends Container {
+ public <T extends Action & Action.Listener> void addAction(T action);
+
+ public <T extends Action & Action.Listener> void removeAction(T action);
+ }
+
+ public interface ShortcutNotifier extends Serializable {
+ public void addShortcutListener(ShortcutListener shortcut);
+
+ public void removeShortcutListener(ShortcutListener shortcut);
+ }
+
+ /**
+ * Interface implemented by classes who wish to handle actions.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Handler extends Serializable {
+
+ /**
+ * Gets the list of actions applicable to this handler.
+ *
+ * @param target
+ * the target handler to list actions for. For item
+ * containers this is the item id.
+ * @param sender
+ * the party that would be sending the actions. Most of this
+ * is the action container.
+ * @return the list of Action
+ */
+ public Action[] getActions(Object target, Object sender);
+
+ /**
+ * Handles an action for the given target. The handler method may just
+ * discard the action if it's not suitable.
+ *
+ * @param action
+ * the action to be handled.
+ * @param sender
+ * the sender of the action. This is most often the action
+ * container.
+ * @param target
+ * the target of the action. For item containers this is the
+ * item id.
+ */
+ public void handleAction(Action action, Object sender, Object target);
+ }
+
+ /**
+ * Interface implemented by all components where actions can be registered.
+ * This means that the components lets others to register as action handlers
+ * to it. When the component receives an action targeting its contents it
+ * should loop all action handlers registered to it and let them handle the
+ * action.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Container extends Serializable {
+
+ /**
+ * Registers a new action handler for this container
+ *
+ * @param actionHandler
+ * the new handler to be added.
+ */
+ public void addActionHandler(Action.Handler actionHandler);
+
+ /**
+ * Removes a previously registered action handler for the contents of
+ * this container.
+ *
+ * @param actionHandler
+ * the handler to be removed.
+ */
+ public void removeActionHandler(Action.Handler actionHandler);
+ }
+
+ /**
+ * Sets the caption.
+ *
+ * @param caption
+ * the caption to set.
+ */
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ /**
+ * Sets the icon.
+ *
+ * @param icon
+ * the icon to set.
+ */
+ public void setIcon(Resource icon) {
+ this.icon = icon;
+ }
+
+}
diff --git a/server/src/com/vaadin/event/ActionManager.java b/server/src/com/vaadin/event/ActionManager.java
new file mode 100644
index 0000000000..64fdeea69b
--- /dev/null
+++ b/server/src/com/vaadin/event/ActionManager.java
@@ -0,0 +1,249 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.util.HashSet;
+import java.util.Map;
+
+import com.vaadin.event.Action.Container;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.terminal.KeyMapper;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.ui.Component;
+
+/**
+ * Javadoc TODO
+ *
+ * Notes:
+ * <p>
+ * Empties the keymapper for each repaint to avoid leaks; can cause problems in
+ * the future if the client assumes key don't change. (if lazyloading, one must
+ * not cache results)
+ * </p>
+ *
+ *
+ */
+public class ActionManager implements Action.Container, Action.Handler,
+ Action.Notifier {
+
+ private static final long serialVersionUID = 1641868163608066491L;
+
+ /** List of action handlers */
+ protected HashSet<Action> ownActions = null;
+
+ /** List of action handlers */
+ protected HashSet<Handler> actionHandlers = null;
+
+ /** Action mapper */
+ protected KeyMapper<Action> actionMapper = null;
+
+ protected Component viewer;
+
+ private boolean clientHasActions = false;
+
+ public ActionManager() {
+
+ }
+
+ public <T extends Component & Container & VariableOwner> ActionManager(
+ T viewer) {
+ this.viewer = viewer;
+ }
+
+ private void requestRepaint() {
+ if (viewer != null) {
+ viewer.requestRepaint();
+ }
+ }
+
+ public <T extends Component & Container & VariableOwner> void setViewer(
+ T viewer) {
+ if (viewer == this.viewer) {
+ return;
+ }
+ if (this.viewer != null) {
+ ((Container) this.viewer).removeActionHandler(this);
+ }
+ requestRepaint(); // this goes to the old viewer
+ if (viewer != null) {
+ viewer.addActionHandler(this);
+ }
+ this.viewer = viewer;
+ requestRepaint(); // this goes to the new viewer
+ }
+
+ @Override
+ public <T extends Action & Action.Listener> void addAction(T action) {
+ if (ownActions == null) {
+ ownActions = new HashSet<Action>();
+ }
+ if (ownActions.add(action)) {
+ requestRepaint();
+ }
+ }
+
+ @Override
+ public <T extends Action & Action.Listener> void removeAction(T action) {
+ if (ownActions != null) {
+ if (ownActions.remove(action)) {
+ requestRepaint();
+ }
+ }
+ }
+
+ @Override
+ public void addActionHandler(Handler actionHandler) {
+ if (actionHandler == this) {
+ // don't add the actionHandler to itself
+ return;
+ }
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new HashSet<Handler>();
+ }
+
+ if (actionHandlers.add(actionHandler)) {
+ requestRepaint();
+ }
+ }
+ }
+
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ if (actionHandlers.remove(actionHandler)) {
+ requestRepaint();
+ }
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ }
+
+ }
+ }
+
+ public void removeAllActionHandlers() {
+ if (actionHandlers != null) {
+ actionHandlers = null;
+ requestRepaint();
+ }
+ }
+
+ public void paintActions(Object actionTarget, PaintTarget paintTarget)
+ throws PaintException {
+
+ actionMapper = null;
+
+ HashSet<Action> actions = new HashSet<Action>();
+ if (actionHandlers != null) {
+ for (Action.Handler handler : actionHandlers) {
+ Action[] as = handler.getActions(actionTarget, viewer);
+ if (as != null) {
+ for (Action action : as) {
+ actions.add(action);
+ }
+ }
+ }
+ }
+ if (ownActions != null) {
+ actions.addAll(ownActions);
+ }
+
+ /*
+ * Must repaint whenever there are actions OR if all actions have been
+ * removed but still exist on client side
+ */
+ if (!actions.isEmpty() || clientHasActions) {
+ actionMapper = new KeyMapper<Action>();
+
+ paintTarget.addVariable((VariableOwner) viewer, "action", "");
+ paintTarget.startTag("actions");
+
+ for (final Action a : actions) {
+ paintTarget.startTag("action");
+ final String akey = actionMapper.key(a);
+ paintTarget.addAttribute("key", akey);
+ if (a.getCaption() != null) {
+ paintTarget.addAttribute("caption", a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ paintTarget.addAttribute("icon", a.getIcon());
+ }
+ if (a instanceof ShortcutAction) {
+ final ShortcutAction sa = (ShortcutAction) a;
+ paintTarget.addAttribute("kc", sa.getKeyCode());
+ final int[] modifiers = sa.getModifiers();
+ if (modifiers != null) {
+ final String[] smodifiers = new String[modifiers.length];
+ for (int i = 0; i < modifiers.length; i++) {
+ smodifiers[i] = String.valueOf(modifiers[i]);
+ }
+ paintTarget.addAttribute("mk", smodifiers);
+ }
+ }
+ paintTarget.endTag("action");
+ }
+
+ paintTarget.endTag("actions");
+ }
+
+ /*
+ * Update flag for next repaint so we know if we need to paint empty
+ * actions or not (must send actions is client had actions before and
+ * all actions were removed).
+ */
+ clientHasActions = !actions.isEmpty();
+ }
+
+ public void handleActions(Map<String, Object> variables, Container sender) {
+ if (variables.containsKey("action") && actionMapper != null) {
+ final String key = (String) variables.get("action");
+ final Action action = actionMapper.get(key);
+ final Object target = variables.get("actiontarget");
+ if (action != null) {
+ handleAction(action, sender, target);
+ }
+ }
+ }
+
+ @Override
+ public Action[] getActions(Object target, Object sender) {
+ HashSet<Action> actions = new HashSet<Action>();
+ if (ownActions != null) {
+ for (Action a : ownActions) {
+ actions.add(a);
+ }
+ }
+ if (actionHandlers != null) {
+ for (Action.Handler h : actionHandlers) {
+ Action[] as = h.getActions(target, sender);
+ if (as != null) {
+ for (Action a : as) {
+ actions.add(a);
+ }
+ }
+ }
+ }
+ return actions.toArray(new Action[actions.size()]);
+ }
+
+ @Override
+ public void handleAction(Action action, Object sender, Object target) {
+ if (actionHandlers != null) {
+ Handler[] array = actionHandlers.toArray(new Handler[actionHandlers
+ .size()]);
+ for (Handler handler : array) {
+ handler.handleAction(action, sender, target);
+ }
+ }
+ if (ownActions != null && ownActions.contains(action)
+ && action instanceof Action.Listener) {
+ ((Action.Listener) action).handleAction(sender, target);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/event/ComponentEventListener.java b/server/src/com/vaadin/event/ComponentEventListener.java
new file mode 100644
index 0000000000..21fe8683f6
--- /dev/null
+++ b/server/src/com/vaadin/event/ComponentEventListener.java
@@ -0,0 +1,11 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.util.EventListener;
+
+public interface ComponentEventListener extends EventListener, Serializable {
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/DataBoundTransferable.java b/server/src/com/vaadin/event/DataBoundTransferable.java
new file mode 100644
index 0000000000..6f742e68d3
--- /dev/null
+++ b/server/src/com/vaadin/event/DataBoundTransferable.java
@@ -0,0 +1,66 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.ui.Component;
+
+/**
+ * Parent class for {@link Transferable} implementations that have a Vaadin
+ * container as a data source. The transfer is associated with an item
+ * (identified by its Id) and optionally also a property identifier (e.g. a
+ * table column identifier when transferring a single table cell).
+ *
+ * The component must implement the interface
+ * {@link com.vaadin.data.Container.Viewer}.
+ *
+ * In most cases, receivers of data transfers should depend on this class
+ * instead of its concrete subclasses.
+ *
+ * @since 6.3
+ */
+public abstract class DataBoundTransferable extends TransferableImpl {
+
+ public DataBoundTransferable(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ super(sourceComponent, rawVariables);
+ }
+
+ /**
+ * Returns the identifier of the item being transferred.
+ *
+ * @return item identifier
+ */
+ public abstract Object getItemId();
+
+ /**
+ * Returns the optional property identifier that the transfer concerns.
+ *
+ * This can be e.g. the table column from which a drag operation originated.
+ *
+ * @return property identifier
+ */
+ public abstract Object getPropertyId();
+
+ /**
+ * Returns the container data source from which the transfer occurs.
+ *
+ * {@link com.vaadin.data.Container.Viewer#getContainerDataSource()} is used
+ * to obtain the underlying container of the source component.
+ *
+ * @return Container
+ */
+ public Container getSourceContainer() {
+ Component sourceComponent = getSourceComponent();
+ if (sourceComponent instanceof Container.Viewer) {
+ return ((Container.Viewer) sourceComponent)
+ .getContainerDataSource();
+ } else {
+ // this should not happen
+ return null;
+ }
+ }
+}
diff --git a/server/src/com/vaadin/event/EventRouter.java b/server/src/com/vaadin/event/EventRouter.java
new file mode 100644
index 0000000000..90c080b860
--- /dev/null
+++ b/server/src/com/vaadin/event/EventRouter.java
@@ -0,0 +1,201 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * <code>EventRouter</code> class implementing the inheritable event listening
+ * model. For more information on the event model see the
+ * {@link com.vaadin.event package documentation}.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class EventRouter implements MethodEventSource {
+
+ /**
+ * List of registered listeners.
+ */
+ private LinkedHashSet<ListenerMethod> listenerList = null;
+
+ /*
+ * Registers a new listener with the specified activation method to listen
+ * events generated by this component. Don't add a JavaDoc comment here, we
+ * use the default documentation from implemented interface.
+ */
+ @Override
+ public void addListener(Class<?> eventType, Object object, Method method) {
+ if (listenerList == null) {
+ listenerList = new LinkedHashSet<ListenerMethod>();
+ }
+ listenerList.add(new ListenerMethod(eventType, object, method));
+ }
+
+ /*
+ * Registers a new listener with the specified named activation method to
+ * listen events generated by this component. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public void addListener(Class<?> eventType, Object object, String methodName) {
+ if (listenerList == null) {
+ listenerList = new LinkedHashSet<ListenerMethod>();
+ }
+ listenerList.add(new ListenerMethod(eventType, object, methodName));
+ }
+
+ /*
+ * Removes all registered listeners matching the given parameters. Don't add
+ * a JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target) {
+ if (listenerList != null) {
+ final Iterator<ListenerMethod> i = listenerList.iterator();
+ while (i.hasNext()) {
+ final ListenerMethod lm = i.next();
+ if (lm.matches(eventType, target)) {
+ i.remove();
+ return;
+ }
+ }
+ }
+ }
+
+ /*
+ * Removes the event listener methods matching the given given paramaters.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target, Method method) {
+ if (listenerList != null) {
+ final Iterator<ListenerMethod> i = listenerList.iterator();
+ while (i.hasNext()) {
+ final ListenerMethod lm = i.next();
+ if (lm.matches(eventType, target, method)) {
+ i.remove();
+ return;
+ }
+ }
+ }
+ }
+
+ /*
+ * Removes the event listener method matching the given given parameters.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target,
+ String methodName) {
+
+ // Find the correct method
+ final Method[] methods = target.getClass().getMethods();
+ Method method = null;
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getName().equals(methodName)) {
+ method = methods[i];
+ }
+ }
+ if (method == null) {
+ throw new IllegalArgumentException();
+ }
+
+ // Remove the listeners
+ if (listenerList != null) {
+ final Iterator<ListenerMethod> i = listenerList.iterator();
+ while (i.hasNext()) {
+ final ListenerMethod lm = i.next();
+ if (lm.matches(eventType, target, method)) {
+ i.remove();
+ return;
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Removes all listeners from event router.
+ */
+ public void removeAllListeners() {
+ listenerList = null;
+ }
+
+ /**
+ * Sends an event to all registered listeners. The listeners will decide if
+ * the activation method should be called or not.
+ *
+ * @param event
+ * the Event to be sent to all listeners.
+ */
+ public void fireEvent(EventObject event) {
+ // It is not necessary to send any events if there are no listeners
+ if (listenerList != null) {
+
+ // Make a copy of the listener list to allow listeners to be added
+ // inside listener methods. Fixes #3605.
+
+ // Send the event to all listeners. The listeners themselves
+ // will filter out unwanted events.
+ final Object[] listeners = listenerList.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((ListenerMethod) listeners[i]).receiveEvent(event);
+ }
+
+ }
+ }
+
+ /**
+ * Checks if the given Event type is listened by a listener registered to
+ * this router.
+ *
+ * @param eventType
+ * the event type to be checked
+ * @return true if a listener is registered for the given event type
+ */
+ public boolean hasListeners(Class<?> eventType) {
+ if (listenerList != null) {
+ for (ListenerMethod lm : listenerList) {
+ if (lm.isType(eventType)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns all listeners that match or extend the given event type.
+ *
+ * @param eventType
+ * The type of event to return listeners for.
+ * @return A collection with all registered listeners. Empty if no listeners
+ * are found.
+ */
+ public Collection<?> getListeners(Class<?> eventType) {
+ List<Object> listeners = new ArrayList<Object>();
+ if (listenerList != null) {
+ for (ListenerMethod lm : listenerList) {
+ if (lm.isOrExtendsType(eventType)) {
+ listeners.add(lm.getTarget());
+ }
+ }
+ }
+ return listeners;
+ }
+}
diff --git a/server/src/com/vaadin/event/FieldEvents.java b/server/src/com/vaadin/event/FieldEvents.java
new file mode 100644
index 0000000000..8f101c1913
--- /dev/null
+++ b/server/src/com/vaadin/event/FieldEvents.java
@@ -0,0 +1,275 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Component.Event;
+import com.vaadin.ui.Field;
+import com.vaadin.ui.Field.ValueChangeEvent;
+import com.vaadin.ui.TextField;
+
+/**
+ * Interface that serves as a wrapper for {@link Field} related events.
+ */
+public interface FieldEvents {
+
+ /**
+ * The interface for adding and removing <code>FocusEvent</code> listeners.
+ * By implementing this interface a class explicitly announces that it will
+ * generate a <code>FocusEvent</code> when it receives keyboard focus.
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @since 6.2
+ * @see FocusListener
+ * @see FocusEvent
+ */
+ public interface FocusNotifier extends Serializable {
+ /**
+ * Adds a <code>FocusListener</code> to the Component which gets fired
+ * when a <code>Field</code> receives keyboard focus.
+ *
+ * @param listener
+ * @see FocusListener
+ * @since 6.2
+ */
+ public void addListener(FocusListener listener);
+
+ /**
+ * Removes a <code>FocusListener</code> from the Component.
+ *
+ * @param listener
+ * @see FocusListener
+ * @since 6.2
+ */
+ public void removeListener(FocusListener listener);
+ }
+
+ /**
+ * The interface for adding and removing <code>BlurEvent</code> listeners.
+ * By implementing this interface a class explicitly announces that it will
+ * generate a <code>BlurEvent</code> when it loses keyboard focus.
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @since 6.2
+ * @see BlurListener
+ * @see BlurEvent
+ */
+ public interface BlurNotifier extends Serializable {
+ /**
+ * Adds a <code>BlurListener</code> to the Component which gets fired
+ * when a <code>Field</code> loses keyboard focus.
+ *
+ * @param listener
+ * @see BlurListener
+ * @since 6.2
+ */
+ public void addListener(BlurListener listener);
+
+ /**
+ * Removes a <code>BlurListener</code> from the Component.
+ *
+ * @param listener
+ * @see BlurListener
+ * @since 6.2
+ */
+ public void removeListener(BlurListener listener);
+ }
+
+ /**
+ * <code>FocusEvent</code> class for holding additional event information.
+ * Fired when a <code>Field</code> receives keyboard focus.
+ *
+ * @since 6.2
+ */
+ @SuppressWarnings("serial")
+ public class FocusEvent extends Component.Event {
+
+ /**
+ * Identifier for event that can be used in {@link EventRouter}
+ */
+ public static final String EVENT_ID = EventId.FOCUS;
+
+ public FocusEvent(Component source) {
+ super(source);
+ }
+ }
+
+ /**
+ * <code>FocusListener</code> interface for listening for
+ * <code>FocusEvent</code> fired by a <code>Field</code>.
+ *
+ * @see FocusEvent
+ * @since 6.2
+ */
+ public interface FocusListener extends ComponentEventListener {
+
+ public static final Method focusMethod = ReflectTools.findMethod(
+ FocusListener.class, "focus", FocusEvent.class);
+
+ /**
+ * Component has been focused
+ *
+ * @param event
+ * Component focus event.
+ */
+ public void focus(FocusEvent event);
+ }
+
+ /**
+ * <code>BlurEvent</code> class for holding additional event information.
+ * Fired when a <code>Field</code> loses keyboard focus.
+ *
+ * @since 6.2
+ */
+ @SuppressWarnings("serial")
+ public class BlurEvent extends Component.Event {
+
+ /**
+ * Identifier for event that can be used in {@link EventRouter}
+ */
+ public static final String EVENT_ID = EventId.BLUR;
+
+ public BlurEvent(Component source) {
+ super(source);
+ }
+ }
+
+ /**
+ * <code>BlurListener</code> interface for listening for
+ * <code>BlurEvent</code> fired by a <code>Field</code>.
+ *
+ * @see BlurEvent
+ * @since 6.2
+ */
+ public interface BlurListener extends ComponentEventListener {
+
+ public static final Method blurMethod = ReflectTools.findMethod(
+ BlurListener.class, "blur", BlurEvent.class);
+
+ /**
+ * Component has been blurred
+ *
+ * @param event
+ * Component blur event.
+ */
+ public void blur(BlurEvent event);
+ }
+
+ /**
+ * TextChangeEvents are fired when the user is editing the text content of a
+ * field. Most commonly text change events are triggered by typing text with
+ * keyboard, but e.g. pasting content from clip board to a text field also
+ * triggers an event.
+ * <p>
+ * TextChangeEvents differ from {@link ValueChangeEvent}s so that they are
+ * triggered repeatedly while the end user is filling the field.
+ * ValueChangeEvents are not fired until the user for example hits enter or
+ * focuses another field. Also note the difference that TextChangeEvents are
+ * only fired if the change is triggered from the user, while
+ * ValueChangeEvents are also fired if the field value is set by the
+ * application code.
+ * <p>
+ * The {@link TextChangeNotifier}s implementation may decide when exactly
+ * TextChangeEvents are fired. TextChangeEvents are not necessary fire for
+ * example on each key press, but buffered with a small delay. The
+ * {@link TextField} component supports different modes for triggering
+ * TextChangeEvents.
+ *
+ * @see TextChangeListener
+ * @see TextChangeNotifier
+ * @see TextField#setTextChangeEventMode(com.vaadin.ui.TextField.TextChangeEventMode)
+ * @since 6.5
+ */
+ public static abstract class TextChangeEvent extends Component.Event {
+ public TextChangeEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * @return the text content of the field after the
+ * {@link TextChangeEvent}
+ */
+ public abstract String getText();
+
+ /**
+ * @return the cursor position during after the {@link TextChangeEvent}
+ */
+ public abstract int getCursorPosition();
+ }
+
+ /**
+ * A listener for {@link TextChangeEvent}s.
+ *
+ * @since 6.5
+ */
+ public interface TextChangeListener extends ComponentEventListener {
+
+ public static String EVENT_ID = "ie";
+ public static Method EVENT_METHOD = ReflectTools.findMethod(
+ TextChangeListener.class, "textChange", TextChangeEvent.class);
+
+ /**
+ * This method is called repeatedly while the text is edited by a user.
+ *
+ * @param event
+ * the event providing details of the text change
+ */
+ public void textChange(TextChangeEvent event);
+ }
+
+ /**
+ * An interface implemented by a {@link Field} supporting
+ * {@link TextChangeEvent}s. An example a {@link TextField} supports
+ * {@link TextChangeListener}s.
+ */
+ public interface TextChangeNotifier extends Serializable {
+ public void addListener(TextChangeListener listener);
+
+ public void removeListener(TextChangeListener listener);
+ }
+
+ public static abstract class FocusAndBlurServerRpcImpl implements
+ FocusAndBlurServerRpc {
+
+ private Component component;
+
+ public FocusAndBlurServerRpcImpl(Component component) {
+ this.component = component;
+ }
+
+ protected abstract void fireEvent(Event event);
+
+ @Override
+ public void blur() {
+ fireEvent(new BlurEvent(component));
+ }
+
+ @Override
+ public void focus() {
+ fireEvent(new FocusEvent(component));
+ }
+ };
+
+}
diff --git a/server/src/com/vaadin/event/ItemClickEvent.java b/server/src/com/vaadin/event/ItemClickEvent.java
new file mode 100644
index 0000000000..0aa0e106c5
--- /dev/null
+++ b/server/src/com/vaadin/event/ItemClickEvent.java
@@ -0,0 +1,121 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.ui.Component;
+
+/**
+ *
+ * Click event fired by a {@link Component} implementing
+ * {@link com.vaadin.data.Container} interface. ItemClickEvents happens on an
+ * {@link Item} rendered somehow on terminal. Event may also contain a specific
+ * {@link Property} on which the click event happened.
+ *
+ * @since 5.3
+ *
+ */
+@SuppressWarnings("serial")
+public class ItemClickEvent extends ClickEvent implements Serializable {
+ private Item item;
+ private Object itemId;
+ private Object propertyId;
+
+ public ItemClickEvent(Component source, Item item, Object itemId,
+ Object propertyId, MouseEventDetails details) {
+ super(source, details);
+ this.item = item;
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the item on which the click event occurred.
+ *
+ * @return item which was clicked
+ */
+ public Item getItem() {
+ return item;
+ }
+
+ /**
+ * Gets a possible identifier in source for clicked Item
+ *
+ * @return
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns property on which click event occurred. Returns null if source
+ * cannot be resolved at property leve. For example if clicked a cell in
+ * table, the "column id" is returned.
+ *
+ * @return a property id of clicked property or null if click didn't occur
+ * on any distinct property.
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ public static final Method ITEM_CLICK_METHOD;
+
+ static {
+ try {
+ ITEM_CLICK_METHOD = ItemClickListener.class.getDeclaredMethod(
+ "itemClick", new Class[] { ItemClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException();
+ }
+ }
+
+ public interface ItemClickListener extends Serializable {
+ public void itemClick(ItemClickEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>ItemClickEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate an <code>ItemClickEvent</code> when one of its
+ * items is clicked.
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @since 6.5
+ * @see ItemClickListener
+ * @see ItemClickEvent
+ */
+ public interface ItemClickNotifier extends Serializable {
+ /**
+ * Register a listener to handle {@link ItemClickEvent}s.
+ *
+ * @param listener
+ * ItemClickListener to be registered
+ */
+ public void addListener(ItemClickListener listener);
+
+ /**
+ * Removes an ItemClickListener.
+ *
+ * @param listener
+ * ItemClickListener to be removed
+ */
+ public void removeListener(ItemClickListener listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/event/LayoutEvents.java b/server/src/com/vaadin/event/LayoutEvents.java
new file mode 100644
index 0000000000..602440ea07
--- /dev/null
+++ b/server/src/com/vaadin/event/LayoutEvents.java
@@ -0,0 +1,138 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ComponentContainer;
+
+public interface LayoutEvents {
+
+ public interface LayoutClickListener extends ComponentEventListener {
+
+ public static final Method clickMethod = ReflectTools.findMethod(
+ LayoutClickListener.class, "layoutClick",
+ LayoutClickEvent.class);
+
+ /**
+ * Layout has been clicked
+ *
+ * @param event
+ * Component click event.
+ */
+ public void layoutClick(LayoutClickEvent event);
+ }
+
+ /**
+ * The interface for adding and removing <code>LayoutClickEvent</code>
+ * listeners. By implementing this interface a class explicitly announces
+ * that it will generate a <code>LayoutClickEvent</code> when a component
+ * inside it is clicked and a <code>LayoutClickListener</code> is
+ * registered.
+ * <p>
+ * Note: The general Java convention is not to explicitly declare that a
+ * class generates events, but to directly define the
+ * <code>addListener</code> and <code>removeListener</code> methods. That
+ * way the caller of these methods has no real way of finding out if the
+ * class really will send the events, or if it just defines the methods to
+ * be able to implement an interface.
+ * </p>
+ *
+ * @since 6.5.2
+ * @see LayoutClickListener
+ * @see LayoutClickEvent
+ */
+ public interface LayoutClickNotifier extends Serializable {
+ /**
+ * Add a click listener to the layout. The listener is called whenever
+ * the user clicks inside the layout. An event is also triggered when
+ * the click targets a component inside a nested layout or Panel,
+ * provided the targeted component does not prevent the click event from
+ * propagating. A caption is not considered part of a component.
+ *
+ * The child component that was clicked is included in the
+ * {@link LayoutClickEvent}.
+ *
+ * Use {@link #removeListener(LayoutClickListener)} to remove the
+ * listener.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addListener(LayoutClickListener listener);
+
+ /**
+ * Removes an LayoutClickListener.
+ *
+ * @param listener
+ * LayoutClickListener to be removed
+ */
+ public void removeListener(LayoutClickListener listener);
+ }
+
+ /**
+ * An event fired when the layout has been clicked. The event contains
+ * information about the target layout (component) and the child component
+ * that was clicked. If no child component was found it is set to null.
+ */
+ public static class LayoutClickEvent extends ClickEvent {
+
+ private final Component clickedComponent;
+ private final Component childComponent;
+
+ public LayoutClickEvent(Component source,
+ MouseEventDetails mouseEventDetails,
+ Component clickedComponent, Component childComponent) {
+ super(source, mouseEventDetails);
+ this.clickedComponent = clickedComponent;
+ this.childComponent = childComponent;
+ }
+
+ /**
+ * Returns the component that was clicked, which is somewhere inside the
+ * parent layout on which the listener was registered.
+ *
+ * For the direct child component of the layout, see
+ * {@link #getChildComponent()}.
+ *
+ * @return clicked {@link Component}, null if none found
+ */
+ public Component getClickedComponent() {
+ return clickedComponent;
+ }
+
+ /**
+ * Returns the direct child component of the layout which contains the
+ * clicked component.
+ *
+ * For the clicked component inside that child component of the layout,
+ * see {@link #getClickedComponent()}.
+ *
+ * @return direct child {@link Component} of the layout which contains
+ * the clicked Component, null if none found
+ */
+ public Component getChildComponent() {
+ return childComponent;
+ }
+
+ public static LayoutClickEvent createEvent(ComponentContainer layout,
+ MouseEventDetails mouseDetails, Connector clickedConnector) {
+ Component clickedComponent = (Component) clickedConnector;
+ Component childComponent = clickedComponent;
+ while (childComponent != null
+ && childComponent.getParent() != layout) {
+ childComponent = childComponent.getParent();
+ }
+
+ return new LayoutClickEvent(layout, mouseDetails, clickedComponent,
+ childComponent);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/ListenerMethod.java b/server/src/com/vaadin/event/ListenerMethod.java
new file mode 100644
index 0000000000..f7dc8a7f13
--- /dev/null
+++ b/server/src/com/vaadin/event/ListenerMethod.java
@@ -0,0 +1,663 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.EventListener;
+import java.util.EventObject;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * <p>
+ * One registered event listener. This class contains the listener object
+ * reference, listened event type, the trigger method to call when the event
+ * fires, and the optional argument list to pass to the method and the index of
+ * the argument to replace with the event object.
+ * </p>
+ *
+ * <p>
+ * This Class provides several constructors that allow omission of the optional
+ * arguments, and giving the listener method directly, or having the constructor
+ * to reflect it using merely the name of the method.
+ * </p>
+ *
+ * <p>
+ * It should be pointed out that the method
+ * {@link #receiveEvent(EventObject event)} is the one that filters out the
+ * events that do not match with the given event type and thus do not result in
+ * calling of the trigger method.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ListenerMethod implements EventListener, Serializable {
+
+ /**
+ * Type of the event that should trigger this listener. Also the subclasses
+ * of this class are accepted to trigger the listener.
+ */
+ private final Class<?> eventType;
+
+ /**
+ * The object containing the trigger method.
+ */
+ private final Object target;
+
+ /**
+ * The trigger method to call when an event passing the given criteria
+ * fires.
+ */
+ private transient Method method;
+
+ /**
+ * Optional argument set to pass to the trigger method.
+ */
+ private Object[] arguments;
+
+ /**
+ * Optional index to <code>arguments</code> that point out which one should
+ * be replaced with the triggering event object and thus be passed to the
+ * trigger method.
+ */
+ private int eventArgumentIndex;
+
+ /* Special serialization to handle method references */
+ private void writeObject(java.io.ObjectOutputStream out) throws IOException {
+ try {
+ out.defaultWriteObject();
+ String name = method.getName();
+ Class<?>[] paramTypes = method.getParameterTypes();
+ out.writeObject(name);
+ out.writeObject(paramTypes);
+ } catch (NotSerializableException e) {
+ getLogger().warning(
+ "Error in serialization of the application: Class "
+ + target.getClass().getName()
+ + " must implement serialization.");
+ throw e;
+ }
+
+ };
+
+ /* Special serialization to handle method references */
+ private void readObject(java.io.ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ try {
+ String name = (String) in.readObject();
+ Class<?>[] paramTypes = (Class<?>[]) in.readObject();
+ // We can not use getMethod directly as we want to support anonymous
+ // inner classes
+ method = findHighestMethod(target.getClass(), name, paramTypes);
+ } catch (SecurityException e) {
+ getLogger().log(Level.SEVERE, "Internal deserialization error", e);
+ }
+ };
+
+ private static Method findHighestMethod(Class<?> cls, String method,
+ Class<?>[] paramTypes) {
+ Class<?>[] ifaces = cls.getInterfaces();
+ for (int i = 0; i < ifaces.length; i++) {
+ Method ifaceMethod = findHighestMethod(ifaces[i], method,
+ paramTypes);
+ if (ifaceMethod != null) {
+ return ifaceMethod;
+ }
+ }
+ if (cls.getSuperclass() != null) {
+ Method parentMethod = findHighestMethod(cls.getSuperclass(),
+ method, paramTypes);
+ if (parentMethod != null) {
+ return parentMethod;
+ }
+ }
+ Method[] methods = cls.getMethods();
+ for (int i = 0; i < methods.length; i++) {
+ // we ignore parameter types for now - you need to add this
+ if (methods[i].getName().equals(method)) {
+ return methods[i];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from a trigger method, it's arguments and
+ * the argument index specifying which one is replaced with the event object
+ * when the trigger method is called.
+ * </p>
+ *
+ * <p>
+ * This constructor gets the trigger method as a parameter so it does not
+ * need to reflect to find it out.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method
+ * @param method
+ * the trigger method
+ * @param arguments
+ * the arguments to be passed to the trigger method
+ * @param eventArgumentIndex
+ * An index to the argument list. This index points out the
+ * argument that is replaced with the event object before the
+ * argument set is passed to the trigger method. If the
+ * eventArgumentIndex is negative, the triggering event object
+ * will not be passed to the trigger method, though it is still
+ * called.
+ * @throws java.lang.IllegalArgumentException
+ * if <code>method</code> is not a member of <code>target</code>
+ * .
+ */
+ public ListenerMethod(Class<?> eventType, Object target, Method method,
+ Object[] arguments, int eventArgumentIndex)
+ throws java.lang.IllegalArgumentException {
+
+ // Checks that the object is of correct type
+ if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
+ throw new java.lang.IllegalArgumentException("The method "
+ + method.getName()
+ + " cannot be used for the given target: "
+ + target.getClass().getName());
+ }
+
+ // Checks that the event argument is null
+ if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) {
+ throw new java.lang.IllegalArgumentException("argument["
+ + eventArgumentIndex + "] must be null");
+ }
+
+ // Checks the event type is supported by the method
+ if (eventArgumentIndex >= 0
+ && !method.getParameterTypes()[eventArgumentIndex]
+ .isAssignableFrom(eventType)) {
+ throw new java.lang.IllegalArgumentException("The method "
+ + method.getName()
+ + " does not accept the given eventType: "
+ + eventType.getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ this.arguments = arguments;
+ this.eventArgumentIndex = eventArgumentIndex;
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from a trigger method name, it's
+ * arguments and the argument index specifying which one is replaced with
+ * the event object. The actual trigger method is reflected from
+ * <code>object</code>, and <code>java.lang.IllegalArgumentException</code>
+ * is thrown unless exactly one match is found.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method.
+ * @param methodName
+ * the name of the trigger method. If the object does not contain
+ * the method or it contains more than one matching methods
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * @param arguments
+ * the arguments to be passed to the trigger method.
+ * @param eventArgumentIndex
+ * An index to the argument list. This index points out the
+ * argument that is replaced with the event object before the
+ * argument set is passed to the trigger method. If the
+ * eventArgumentIndex is negative, the triggering event object
+ * will not be passed to the trigger method, though it is still
+ * called.
+ * @throws java.lang.IllegalArgumentException
+ * unless exactly one match <code>methodName</code> is found in
+ * <code>target</code>.
+ */
+ public ListenerMethod(Class<?> eventType, Object target, String methodName,
+ Object[] arguments, int eventArgumentIndex)
+ throws java.lang.IllegalArgumentException {
+
+ // Finds the correct method
+ final Method[] methods = target.getClass().getMethods();
+ Method method = null;
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getName().equals(methodName)) {
+ method = methods[i];
+ }
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("Method " + methodName
+ + " not found in class " + target.getClass().getName());
+ }
+
+ // Checks that the event argument is null
+ if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) {
+ throw new java.lang.IllegalArgumentException("argument["
+ + eventArgumentIndex + "] must be null");
+ }
+
+ // Checks the event type is supported by the method
+ if (eventArgumentIndex >= 0
+ && !method.getParameterTypes()[eventArgumentIndex]
+ .isAssignableFrom(eventType)) {
+ throw new java.lang.IllegalArgumentException("The method "
+ + method.getName()
+ + " does not accept the given eventType: "
+ + eventType.getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ this.arguments = arguments;
+ this.eventArgumentIndex = eventArgumentIndex;
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from the trigger method and it's
+ * arguments. Since the the index to the replaced parameter is not specified
+ * the event triggering this listener will not be passed to the trigger
+ * method.
+ * </p>
+ *
+ * <p>
+ * This constructor gets the trigger method as a parameter so it does not
+ * need to reflect to find it out.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method.
+ * @param method
+ * the trigger method.
+ * @param arguments
+ * the arguments to be passed to the trigger method.
+ * @throws java.lang.IllegalArgumentException
+ * if <code>method</code> is not a member of <code>target</code>
+ * .
+ */
+ public ListenerMethod(Class<?> eventType, Object target, Method method,
+ Object[] arguments) throws java.lang.IllegalArgumentException {
+
+ // Check that the object is of correct type
+ if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
+ throw new java.lang.IllegalArgumentException("The method "
+ + method.getName()
+ + " cannot be used for the given target: "
+ + target.getClass().getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ this.arguments = arguments;
+ eventArgumentIndex = -1;
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from a trigger method name and it's
+ * arguments. Since the the index to the replaced parameter is not specified
+ * the event triggering this listener will not be passed to the trigger
+ * method.
+ * </p>
+ *
+ * <p>
+ * The actual trigger method is reflected from <code>target</code>, and
+ * <code>java.lang.IllegalArgumentException</code> is thrown unless exactly
+ * one match is found.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method.
+ * @param methodName
+ * the name of the trigger method. If the object does not contain
+ * the method or it contains more than one matching methods
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * @param arguments
+ * the arguments to be passed to the trigger method.
+ * @throws java.lang.IllegalArgumentException
+ * unless exactly one match <code>methodName</code> is found in
+ * <code>object</code>.
+ */
+ public ListenerMethod(Class<?> eventType, Object target, String methodName,
+ Object[] arguments) throws java.lang.IllegalArgumentException {
+
+ // Find the correct method
+ final Method[] methods = target.getClass().getMethods();
+ Method method = null;
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getName().equals(methodName)) {
+ method = methods[i];
+ }
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("Method " + methodName
+ + " not found in class " + target.getClass().getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ this.arguments = arguments;
+ eventArgumentIndex = -1;
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from a trigger method. Since the argument
+ * list is unspecified no parameters are passed to the trigger method when
+ * the listener is triggered.
+ * </p>
+ *
+ * <p>
+ * This constructor gets the trigger method as a parameter so it does not
+ * need to reflect to find it out.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method.
+ * @param method
+ * the trigger method.
+ * @throws java.lang.IllegalArgumentException
+ * if <code>method</code> is not a member of <code>object</code>
+ * .
+ */
+ public ListenerMethod(Class<?> eventType, Object target, Method method)
+ throws java.lang.IllegalArgumentException {
+
+ // Checks that the object is of correct type
+ if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
+ throw new java.lang.IllegalArgumentException("The method "
+ + method.getName()
+ + " cannot be used for the given target: "
+ + target.getClass().getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ eventArgumentIndex = -1;
+
+ final Class<?>[] params = method.getParameterTypes();
+
+ if (params.length == 0) {
+ arguments = new Object[0];
+ } else if (params.length == 1 && params[0].isAssignableFrom(eventType)) {
+ arguments = new Object[] { null };
+ eventArgumentIndex = 0;
+ } else {
+ throw new IllegalArgumentException(
+ "Method requires unknown parameters");
+ }
+ }
+
+ /**
+ * <p>
+ * Constructs a new event listener from a trigger method name. Since the
+ * argument list is unspecified no parameters are passed to the trigger
+ * method when the listener is triggered.
+ * </p>
+ *
+ * <p>
+ * The actual trigger method is reflected from <code>object</code>, and
+ * <code>java.lang.IllegalArgumentException</code> is thrown unless exactly
+ * one match is found.
+ * </p>
+ *
+ * @param eventType
+ * the event type that is listener listens to. All events of this
+ * kind (or its subclasses) result in calling the trigger method.
+ * @param target
+ * the object instance that contains the trigger method.
+ * @param methodName
+ * the name of the trigger method. If the object does not contain
+ * the method or it contains more than one matching methods
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * @throws java.lang.IllegalArgumentException
+ * unless exactly one match <code>methodName</code> is found in
+ * <code>target</code>.
+ */
+ public ListenerMethod(Class<?> eventType, Object target, String methodName)
+ throws java.lang.IllegalArgumentException {
+
+ // Finds the correct method
+ final Method[] methods = target.getClass().getMethods();
+ Method method = null;
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getName().equals(methodName)) {
+ method = methods[i];
+ }
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("Method " + methodName
+ + " not found in class " + target.getClass().getName());
+ }
+
+ this.eventType = eventType;
+ this.target = target;
+ this.method = method;
+ eventArgumentIndex = -1;
+
+ final Class<?>[] params = method.getParameterTypes();
+
+ if (params.length == 0) {
+ arguments = new Object[0];
+ } else if (params.length == 1 && params[0].isAssignableFrom(eventType)) {
+ arguments = new Object[] { null };
+ eventArgumentIndex = 0;
+ } else {
+ throw new IllegalArgumentException(
+ "Method requires unknown parameters");
+ }
+ }
+
+ /**
+ * Receives one event from the <code>EventRouter</code> and calls the
+ * trigger method if it matches with the criteria defined for the listener.
+ * Only the events of the same or subclass of the specified event class
+ * result in the trigger method to be called.
+ *
+ * @param event
+ * the fired event. Unless the trigger method's argument list and
+ * the index to the to be replaced argument is specified, this
+ * event will not be passed to the trigger method.
+ */
+ public void receiveEvent(EventObject event) {
+ // Only send events supported by the method
+ if (eventType.isAssignableFrom(event.getClass())) {
+ try {
+ if (eventArgumentIndex >= 0) {
+ if (eventArgumentIndex == 0 && arguments.length == 1) {
+ method.invoke(target, new Object[] { event });
+ } else {
+ final Object[] arg = new Object[arguments.length];
+ for (int i = 0; i < arg.length; i++) {
+ arg[i] = arguments[i];
+ }
+ arg[eventArgumentIndex] = event;
+ method.invoke(target, arg);
+ }
+ } else {
+ method.invoke(target, arguments);
+ }
+
+ } catch (final java.lang.IllegalAccessException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error - please report", e);
+ } catch (final java.lang.reflect.InvocationTargetException e) {
+ // An exception was thrown by the invocation target. Throw it
+ // forwards.
+ throw new MethodException("Invocation of method "
+ + method.getName() + " in "
+ + target.getClass().getName() + " failed.",
+ e.getTargetException());
+ }
+ }
+ }
+
+ /**
+ * Checks if the given object and event match with the ones stored in this
+ * listener.
+ *
+ * @param target
+ * the object to be matched against the object stored by this
+ * listener.
+ * @param eventType
+ * the type to be tested for equality against the type stored by
+ * this listener.
+ * @return <code>true</code> if <code>target</code> is the same object as
+ * the one stored in this object and <code>eventType</code> equals
+ * the event type stored in this object. *
+ */
+ public boolean matches(Class<?> eventType, Object target) {
+ return (this.target == target) && (eventType.equals(this.eventType));
+ }
+
+ /**
+ * Checks if the given object, event and method match with the ones stored
+ * in this listener.
+ *
+ * @param target
+ * the object to be matched against the object stored by this
+ * listener.
+ * @param eventType
+ * the type to be tested for equality against the type stored by
+ * this listener.
+ * @param method
+ * the method to be tested for equality against the method stored
+ * by this listener.
+ * @return <code>true</code> if <code>target</code> is the same object as
+ * the one stored in this object, <code>eventType</code> equals with
+ * the event type stored in this object and <code>method</code>
+ * equals with the method stored in this object
+ */
+ public boolean matches(Class<?> eventType, Object target, Method method) {
+ return (this.target == target)
+ && (eventType.equals(this.eventType) && method
+ .equals(this.method));
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+
+ hash = 31 * hash + eventArgumentIndex;
+ hash = 31 * hash + (eventType == null ? 0 : eventType.hashCode());
+ hash = 31 * hash + (target == null ? 0 : target.hashCode());
+ hash = 31 * hash + (method == null ? 0 : method.hashCode());
+
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ if (this == obj) {
+ return true;
+ }
+
+ // return false if obj is a subclass (do not use instanceof check)
+ if ((obj == null) || (obj.getClass() != getClass())) {
+ return false;
+ }
+
+ // obj is of same class, test it further
+ ListenerMethod t = (ListenerMethod) obj;
+
+ return eventArgumentIndex == t.eventArgumentIndex
+ && (eventType == t.eventType || (eventType != null && eventType
+ .equals(t.eventType)))
+ && (target == t.target || (target != null && target
+ .equals(t.target)))
+ && (method == t.method || (method != null && method
+ .equals(t.method)))
+ && (arguments == t.arguments || (Arrays.equals(arguments,
+ t.arguments)));
+ }
+
+ /**
+ * Exception that wraps an exception thrown by an invoked method. When
+ * <code>ListenerMethod</code> invokes the target method, it may throw
+ * arbitrary exception. The original exception is wrapped into
+ * MethodException instance and rethrown by the <code>ListenerMethod</code>.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class MethodException extends RuntimeException implements
+ Serializable {
+
+ private MethodException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ }
+
+ /**
+ * Compares the type of this ListenerMethod to the given type
+ *
+ * @param eventType
+ * The type to compare with
+ * @return true if this type of this ListenerMethod matches the given type,
+ * false otherwise
+ */
+ public boolean isType(Class<?> eventType) {
+ return this.eventType == eventType;
+ }
+
+ /**
+ * Compares the type of this ListenerMethod to the given type
+ *
+ * @param eventType
+ * The type to compare with
+ * @return true if this event type can be assigned to the given type, false
+ * otherwise
+ */
+ public boolean isOrExtendsType(Class<?> eventType) {
+ return eventType.isAssignableFrom(this.eventType);
+ }
+
+ /**
+ * Returns the target object which contains the trigger method.
+ *
+ * @return The target object
+ */
+ public Object getTarget() {
+ return target;
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(ListenerMethod.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/event/MethodEventSource.java b/server/src/com/vaadin/event/MethodEventSource.java
new file mode 100644
index 0000000000..fb2e7b029b
--- /dev/null
+++ b/server/src/com/vaadin/event/MethodEventSource.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+/**
+ * <p>
+ * Interface for classes supporting registration of methods as event receivers.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface MethodEventSource extends Serializable {
+
+ /**
+ * <p>
+ * Registers a new event listener with the specified activation method to
+ * listen events generated by this component. If the activation method does
+ * not have any arguments the event object will not be passed to it when
+ * it's called.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param object
+ * the object instance who owns the activation method.
+ * @param method
+ * the activation method.
+ * @throws java.lang.IllegalArgumentException
+ * unless <code>method</code> has exactly one match in
+ * <code>object</code>
+ */
+ public void addListener(Class<?> eventType, Object object, Method method);
+
+ /**
+ * <p>
+ * Registers a new listener with the specified activation method to listen
+ * events generated by this component. If the activation method does not
+ * have any arguments the event object will not be passed to it when it's
+ * called.
+ * </p>
+ *
+ * <p>
+ * This version of <code>addListener</code> gets the name of the activation
+ * method as a parameter. The actual method is reflected from
+ * <code>object</code>, and unless exactly one match is found,
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param object
+ * the object instance who owns the activation method.
+ * @param methodName
+ * the name of the activation method.
+ * @throws java.lang.IllegalArgumentException
+ * unless <code>method</code> has exactly one match in
+ * <code>object</code>
+ */
+ public void addListener(Class<?> eventType, Object object, String methodName);
+
+ /**
+ * Removes all registered listeners matching the given parameters. Since
+ * this method receives the event type and the listener object as
+ * parameters, it will unregister all <code>object</code>'s methods that are
+ * registered to listen to events of type <code>eventType</code> generated
+ * by this component.
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ */
+ public void removeListener(Class<?> eventType, Object target);
+
+ /**
+ * Removes one registered listener method. The given method owned by the
+ * given object will no longer be called when the specified events are
+ * generated by this component.
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type eventType with one or more methods.
+ * @param method
+ * the method owned by the target that's registered to listen to
+ * events of type eventType.
+ */
+ public void removeListener(Class<?> eventType, Object target, Method method);
+
+ /**
+ * <p>
+ * Removes one registered listener method. The given method owned by the
+ * given object will no longer be called when the specified events are
+ * generated by this component.
+ * </p>
+ *
+ * <p>
+ * This version of <code>removeListener</code> gets the name of the
+ * activation method as a parameter. The actual method is reflected from the
+ * target, and unless exactly one match is found,
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ * @param methodName
+ * the name of the method owned by <code>target</code> that's
+ * registered to listen to events of type <code>eventType</code>.
+ */
+ public void removeListener(Class<?> eventType, Object target,
+ String methodName);
+}
diff --git a/server/src/com/vaadin/event/MouseEvents.java b/server/src/com/vaadin/event/MouseEvents.java
new file mode 100644
index 0000000000..fafd44be89
--- /dev/null
+++ b/server/src/com/vaadin/event/MouseEvents.java
@@ -0,0 +1,234 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.Component;
+
+/**
+ * Interface that serves as a wrapper for mouse related events.
+ *
+ * @author Vaadin Ltd.
+ * @see ClickListener
+ * @version
+ * @VERSION@
+ * @since 6.2
+ */
+public interface MouseEvents {
+
+ /**
+ * Class for holding information about a mouse click event. A
+ * {@link ClickEvent} is fired when the user clicks on a
+ * <code>Component</code>.
+ *
+ * The information available for click events are terminal dependent.
+ * Correct values for all event details cannot be guaranteed.
+ *
+ * @author Vaadin Ltd.
+ * @see ClickListener
+ * @version
+ * @VERSION@
+ * @since 6.2
+ */
+ public class ClickEvent extends Component.Event {
+ public static final int BUTTON_LEFT = MouseEventDetails.BUTTON_LEFT;
+ public static final int BUTTON_MIDDLE = MouseEventDetails.BUTTON_MIDDLE;
+ public static final int BUTTON_RIGHT = MouseEventDetails.BUTTON_RIGHT;
+
+ private MouseEventDetails details;
+
+ public ClickEvent(Component source, MouseEventDetails mouseEventDetails) {
+ super(source);
+ details = mouseEventDetails;
+ }
+
+ /**
+ * Returns an identifier describing which mouse button the user pushed.
+ * Compare with {@link #BUTTON_LEFT},{@link #BUTTON_MIDDLE},
+ * {@link #BUTTON_RIGHT} to find out which butten it is.
+ *
+ * @return one of {@link #BUTTON_LEFT}, {@link #BUTTON_MIDDLE},
+ * {@link #BUTTON_RIGHT}.
+ */
+ public int getButton() {
+ return details.getButton();
+ }
+
+ /**
+ * Returns the mouse position (x coordinate) when the click took place.
+ * The position is relative to the browser client area.
+ *
+ * @return The mouse cursor x position
+ */
+ public int getClientX() {
+ return details.getClientX();
+ }
+
+ /**
+ * Returns the mouse position (y coordinate) when the click took place.
+ * The position is relative to the browser client area.
+ *
+ * @return The mouse cursor y position
+ */
+ public int getClientY() {
+ return details.getClientY();
+ }
+
+ /**
+ * Returns the relative mouse position (x coordinate) when the click
+ * took place. The position is relative to the clicked component.
+ *
+ * @return The mouse cursor x position relative to the clicked layout
+ * component or -1 if no x coordinate available
+ */
+ public int getRelativeX() {
+ return details.getRelativeX();
+ }
+
+ /**
+ * Returns the relative mouse position (y coordinate) when the click
+ * took place. The position is relative to the clicked component.
+ *
+ * @return The mouse cursor y position relative to the clicked layout
+ * component or -1 if no y coordinate available
+ */
+ public int getRelativeY() {
+ return details.getRelativeY();
+ }
+
+ /**
+ * Checks if the event is a double click event.
+ *
+ * @return true if the event is a double click event, false otherwise
+ */
+ public boolean isDoubleClick() {
+ return details.isDoubleClick();
+ }
+
+ /**
+ * Checks if the Alt key was down when the mouse event took place.
+ *
+ * @return true if Alt was down when the event occured, false otherwise
+ */
+ public boolean isAltKey() {
+ return details.isAltKey();
+ }
+
+ /**
+ * Checks if the Ctrl key was down when the mouse event took place.
+ *
+ * @return true if Ctrl was pressed when the event occured, false
+ * otherwise
+ */
+ public boolean isCtrlKey() {
+ return details.isCtrlKey();
+ }
+
+ /**
+ * Checks if the Meta key was down when the mouse event took place.
+ *
+ * @return true if Meta was pressed when the event occured, false
+ * otherwise
+ */
+ public boolean isMetaKey() {
+ return details.isMetaKey();
+ }
+
+ /**
+ * Checks if the Shift key was down when the mouse event took place.
+ *
+ * @return true if Shift was pressed when the event occured, false
+ * otherwise
+ */
+ public boolean isShiftKey() {
+ return details.isShiftKey();
+ }
+
+ /**
+ * Returns a human readable string representing which button has been
+ * pushed. This is meant for debug purposes only and the string returned
+ * could change. Use {@link #getButton()} to check which button was
+ * pressed.
+ *
+ * @since 6.3
+ * @return A string representation of which button was pushed.
+ */
+ public String getButtonName() {
+ return details.getButtonName();
+ }
+ }
+
+ /**
+ * Interface for listening for a {@link ClickEvent} fired by a
+ * {@link Component}.
+ *
+ * @see ClickEvent
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.2
+ */
+ public interface ClickListener extends ComponentEventListener {
+
+ public static final Method clickMethod = ReflectTools.findMethod(
+ ClickListener.class, "click", ClickEvent.class);
+
+ /**
+ * Called when a {@link Component} has been clicked. A reference to the
+ * component is given by {@link ClickEvent#getComponent()}.
+ *
+ * @param event
+ * An event containing information about the click.
+ */
+ public void click(ClickEvent event);
+ }
+
+ /**
+ * Class for holding additional event information for DoubleClick events.
+ * Fired when the user double-clicks on a <code>Component</code>.
+ *
+ * @see ClickEvent
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.2
+ */
+ public class DoubleClickEvent extends Component.Event {
+
+ public DoubleClickEvent(Component source) {
+ super(source);
+ }
+ }
+
+ /**
+ * Interface for listening for a {@link DoubleClickEvent} fired by a
+ * {@link Component}.
+ *
+ * @see DoubleClickEvent
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.2
+ */
+ public interface DoubleClickListener extends ComponentEventListener {
+
+ public static final Method doubleClickMethod = ReflectTools.findMethod(
+ DoubleClickListener.class, "doubleClick",
+ DoubleClickEvent.class);
+
+ /**
+ * Called when a {@link Component} has been double clicked. A reference
+ * to the component is given by {@link DoubleClickEvent#getComponent()}.
+ *
+ * @param event
+ * An event containing information about the double click.
+ */
+ public void doubleClick(DoubleClickEvent event);
+ }
+
+}
diff --git a/server/src/com/vaadin/event/ShortcutAction.java b/server/src/com/vaadin/event/ShortcutAction.java
new file mode 100644
index 0000000000..c42dd731c8
--- /dev/null
+++ b/server/src/com/vaadin/event/ShortcutAction.java
@@ -0,0 +1,373 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.vaadin.terminal.Resource;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.Panel;
+import com.vaadin.ui.Window;
+
+/**
+ * Shortcuts are a special type of {@link Action}s used to create keyboard
+ * shortcuts.
+ * <p>
+ * The ShortcutAction is triggered when the user presses a given key in
+ * combination with the (optional) given modifier keys.
+ * </p>
+ * <p>
+ * ShortcutActions can be global (by attaching to the {@link Window}), or
+ * attached to different parts of the UI so that a specific shortcut is only
+ * valid in part of the UI. For instance, one can attach shortcuts to a specific
+ * {@link Panel} - look for {@link ComponentContainer}s implementing
+ * {@link Handler Action.Handler} or {@link Notifier Action.Notifier}.
+ * </p>
+ * <p>
+ * ShortcutActions have a caption that may be used to display the shortcut
+ * visually. This allows the ShortcutAction to be used as a plain Action while
+ * still reacting to a keyboard shortcut. Note that this functionality is not
+ * very well supported yet, but it might still be a good idea to give a caption
+ * to the shortcut.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @since 4.0.1
+ */
+@SuppressWarnings("serial")
+public class ShortcutAction extends Action {
+
+ private final int keyCode;
+
+ private final int[] modifiers;
+
+ /**
+ * Creates a shortcut that reacts to the given {@link KeyCode} and
+ * (optionally) {@link ModifierKey}s. <br/>
+ * The shortcut might be shown in the UI (e.g context menu), in which case
+ * the caption will be used.
+ *
+ * @param caption
+ * used when displaying the shortcut visually
+ * @param kc
+ * KeyCode that the shortcut reacts to
+ * @param m
+ * optional modifier keys
+ */
+ public ShortcutAction(String caption, int kc, int[] m) {
+ super(caption);
+ keyCode = kc;
+ modifiers = m;
+ }
+
+ /**
+ * Creates a shortcut that reacts to the given {@link KeyCode} and
+ * (optionally) {@link ModifierKey}s. <br/>
+ * The shortcut might be shown in the UI (e.g context menu), in which case
+ * the caption and icon will be used.
+ *
+ * @param caption
+ * used when displaying the shortcut visually
+ * @param icon
+ * used when displaying the shortcut visually
+ * @param kc
+ * KeyCode that the shortcut reacts to
+ * @param m
+ * optional modifier keys
+ */
+ public ShortcutAction(String caption, Resource icon, int kc, int[] m) {
+ super(caption, icon);
+ keyCode = kc;
+ modifiers = m;
+ }
+
+ /**
+ * Used in the caption shorthand notation to indicate the ALT modifier.
+ */
+ public static final char SHORTHAND_CHAR_ALT = '&';
+ /**
+ * Used in the caption shorthand notation to indicate the SHIFT modifier.
+ */
+ public static final char SHORTHAND_CHAR_SHIFT = '_';
+ /**
+ * Used in the caption shorthand notation to indicate the CTRL modifier.
+ */
+ public static final char SHORTHAND_CHAR_CTRL = '^';
+
+ // regex-quote (escape) the characters
+ private static final String SHORTHAND_ALT = Pattern.quote(Character
+ .toString(SHORTHAND_CHAR_ALT));
+ private static final String SHORTHAND_SHIFT = Pattern.quote(Character
+ .toString(SHORTHAND_CHAR_SHIFT));
+ private static final String SHORTHAND_CTRL = Pattern.quote(Character
+ .toString(SHORTHAND_CHAR_CTRL));
+ // Used for replacing escaped chars, e.g && with &
+ private static final Pattern SHORTHAND_ESCAPE = Pattern.compile("("
+ + SHORTHAND_ALT + "?)" + SHORTHAND_ALT + "|(" + SHORTHAND_SHIFT
+ + "?)" + SHORTHAND_SHIFT + "|(" + SHORTHAND_CTRL + "?)"
+ + SHORTHAND_CTRL);
+ // Used for removing escaped chars, only leaving real shorthands
+ private static final Pattern SHORTHAND_REMOVE = Pattern.compile("(["
+ + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL
+ + "])\\1");
+ // Mnemonic char, optionally followed by another, and optionally a third
+ private static final Pattern SHORTHANDS = Pattern.compile("("
+ + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL
+ + ")(?!\\1)(?:(" + SHORTHAND_ALT + "|" + SHORTHAND_SHIFT + "|"
+ + SHORTHAND_CTRL + ")(?!\\1|\\2))?(?:(" + SHORTHAND_ALT + "|"
+ + SHORTHAND_SHIFT + "|" + SHORTHAND_CTRL + ")(?!\\1|\\2|\\3))?.");
+
+ /**
+ * Constructs a ShortcutAction using a shorthand notation to encode the
+ * keycode and modifiers in the caption.
+ * <p>
+ * Insert one or more modifier characters before the character to use as
+ * keycode. E.g <code>"&Save"</code> will make a shortcut responding to
+ * ALT-S, <code>"E^xit"</code> will respond to CTRL-X.<br/>
+ * Multiple modifiers can be used, e.g <code>"&^Delete"</code> will respond
+ * to CTRL-ALT-D (the order of the modifier characters is not important).
+ * </p>
+ * <p>
+ * The modifier characters will be removed from the caption. The modifier
+ * character is be escaped by itself: two consecutive characters are turned
+ * into the original character w/o the special meaning. E.g
+ * <code>"Save&&&close"</code> will respond to ALT-C, and the caption will
+ * say "Save&close".
+ * </p>
+ *
+ * @param shorthandCaption
+ * the caption in modifier shorthand
+ */
+ public ShortcutAction(String shorthandCaption) {
+ this(shorthandCaption, null);
+ }
+
+ /**
+ * Constructs a ShortcutAction using a shorthand notation to encode the
+ * keycode a in the caption.
+ * <p>
+ * This works the same way as {@link #ShortcutAction(String)}, with the
+ * exception that the modifiers given override those indicated in the
+ * caption. I.e use any of the modifier characters in the caption to
+ * indicate the keycode, but the modifier will be the given set.<br/>
+ * E.g
+ * <code>new ShortcutAction("Do &stuff", new int[]{ShortcutAction.ModifierKey.CTRL}));</code>
+ * will respond to CTRL-S.
+ * </p>
+ *
+ * @param shorthandCaption
+ * @param modifierKeys
+ */
+ public ShortcutAction(String shorthandCaption, int[] modifierKeys) {
+ // && -> & etc
+ super(SHORTHAND_ESCAPE.matcher(shorthandCaption).replaceAll("$1$2$3"));
+ // replace escaped chars with something that won't accidentally match
+ shorthandCaption = SHORTHAND_REMOVE.matcher(shorthandCaption)
+ .replaceAll("\u001A");
+ Matcher matcher = SHORTHANDS.matcher(shorthandCaption);
+ if (matcher.find()) {
+ String match = matcher.group();
+
+ // KeyCode from last char in match, uppercase
+ keyCode = Character.toUpperCase(matcher.group().charAt(
+ match.length() - 1));
+
+ // Given modifiers override this indicated in the caption
+ if (modifierKeys != null) {
+ modifiers = modifierKeys;
+ } else {
+ // Read modifiers from caption
+ int[] mod = new int[match.length() - 1];
+ for (int i = 0; i < mod.length; i++) {
+ int kc = match.charAt(i);
+ switch (kc) {
+ case SHORTHAND_CHAR_ALT:
+ mod[i] = ModifierKey.ALT;
+ break;
+ case SHORTHAND_CHAR_CTRL:
+ mod[i] = ModifierKey.CTRL;
+ break;
+ case SHORTHAND_CHAR_SHIFT:
+ mod[i] = ModifierKey.SHIFT;
+ break;
+ }
+ }
+ modifiers = mod;
+ }
+
+ } else {
+ keyCode = -1;
+ modifiers = modifierKeys;
+ }
+ }
+
+ /**
+ * Get the {@link KeyCode} that this shortcut reacts to (in combination with
+ * the {@link ModifierKey}s).
+ *
+ * @return keycode for this shortcut
+ */
+ public int getKeyCode() {
+ return keyCode;
+ }
+
+ /**
+ * Get the {@link ModifierKey}s required for the shortcut to react.
+ *
+ * @return modifier keys for this shortcut
+ */
+ public int[] getModifiers() {
+ return modifiers;
+ }
+
+ /**
+ * Key codes that can be used for shortcuts
+ *
+ */
+ public interface KeyCode extends Serializable {
+ public static final int ENTER = 13;
+
+ public static final int ESCAPE = 27;
+
+ public static final int PAGE_UP = 33;
+
+ public static final int PAGE_DOWN = 34;
+
+ public static final int TAB = 9;
+
+ public static final int ARROW_LEFT = 37;
+
+ public static final int ARROW_UP = 38;
+
+ public static final int ARROW_RIGHT = 39;
+
+ public static final int ARROW_DOWN = 40;
+
+ public static final int BACKSPACE = 8;
+
+ public static final int DELETE = 46;
+
+ public static final int INSERT = 45;
+
+ public static final int END = 35;
+
+ public static final int HOME = 36;
+
+ public static final int F1 = 112;
+
+ public static final int F2 = 113;
+
+ public static final int F3 = 114;
+
+ public static final int F4 = 115;
+
+ public static final int F5 = 116;
+
+ public static final int F6 = 117;
+
+ public static final int F7 = 118;
+
+ public static final int F8 = 119;
+
+ public static final int F9 = 120;
+
+ public static final int F10 = 121;
+
+ public static final int F11 = 122;
+
+ public static final int F12 = 123;
+
+ public static final int A = 65;
+
+ public static final int B = 66;
+
+ public static final int C = 67;
+
+ public static final int D = 68;
+
+ public static final int E = 69;
+
+ public static final int F = 70;
+
+ public static final int G = 71;
+
+ public static final int H = 72;
+
+ public static final int I = 73;
+
+ public static final int J = 74;
+
+ public static final int K = 75;
+
+ public static final int L = 76;
+
+ public static final int M = 77;
+
+ public static final int N = 78;
+
+ public static final int O = 79;
+
+ public static final int P = 80;
+
+ public static final int Q = 81;
+
+ public static final int R = 82;
+
+ public static final int S = 83;
+
+ public static final int T = 84;
+
+ public static final int U = 85;
+
+ public static final int V = 86;
+
+ public static final int W = 87;
+
+ public static final int X = 88;
+
+ public static final int Y = 89;
+
+ public static final int Z = 90;
+
+ public static final int NUM0 = 48;
+
+ public static final int NUM1 = 49;
+
+ public static final int NUM2 = 50;
+
+ public static final int NUM3 = 51;
+
+ public static final int NUM4 = 52;
+
+ public static final int NUM5 = 53;
+
+ public static final int NUM6 = 54;
+
+ public static final int NUM7 = 55;
+
+ public static final int NUM8 = 56;
+
+ public static final int NUM9 = 57;
+
+ public static final int SPACEBAR = 32;
+ }
+
+ /**
+ * Modifier key constants
+ *
+ */
+ public interface ModifierKey extends Serializable {
+ public static final int SHIFT = 16;
+
+ public static final int CTRL = 17;
+
+ public static final int ALT = 18;
+
+ public static final int META = 91;
+ }
+}
diff --git a/server/src/com/vaadin/event/ShortcutListener.java b/server/src/com/vaadin/event/ShortcutListener.java
new file mode 100644
index 0000000000..b760cfabe6
--- /dev/null
+++ b/server/src/com/vaadin/event/ShortcutListener.java
@@ -0,0 +1,33 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import com.vaadin.event.Action.Listener;
+import com.vaadin.terminal.Resource;
+
+public abstract class ShortcutListener extends ShortcutAction implements
+ Listener {
+
+ private static final long serialVersionUID = 1L;
+
+ public ShortcutListener(String caption, int keyCode, int... modifierKeys) {
+ super(caption, keyCode, modifierKeys);
+ }
+
+ public ShortcutListener(String shorthandCaption, int... modifierKeys) {
+ super(shorthandCaption, modifierKeys);
+ }
+
+ public ShortcutListener(String caption, Resource icon, int keyCode,
+ int... modifierKeys) {
+ super(caption, icon, keyCode, modifierKeys);
+ }
+
+ public ShortcutListener(String shorthandCaption) {
+ super(shorthandCaption);
+ }
+
+ @Override
+ abstract public void handleAction(Object sender, Object target);
+}
diff --git a/server/src/com/vaadin/event/Transferable.java b/server/src/com/vaadin/event/Transferable.java
new file mode 100644
index 0000000000..838d8ad7e2
--- /dev/null
+++ b/server/src/com/vaadin/event/Transferable.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.ui.Component;
+
+/**
+ * Transferable wraps the data that is to be imported into another component.
+ * Currently Transferable is only used for drag and drop.
+ *
+ * @since 6.3
+ */
+public interface Transferable extends Serializable {
+
+ /**
+ * Returns the data from Transferable by its data flavor (aka data type).
+ * Data types can be any string keys, but MIME types like "text/plain" are
+ * commonly used.
+ * <p>
+ * Note, implementations of {@link Transferable} often provide a better
+ * typed API for accessing data.
+ *
+ * @param dataFlavor
+ * the data flavor to be returned from Transferable
+ * @return the data stored in the Transferable or null if Transferable
+ * contains no data for given data flavour
+ */
+ public Object getData(String dataFlavor);
+
+ /**
+ * Stores data of given data flavor to Transferable. Possibly existing value
+ * of the same data flavor will be replaced.
+ *
+ * @param dataFlavor
+ * the data flavor
+ * @param value
+ * the new value of the data flavor
+ */
+ public void setData(String dataFlavor, Object value);
+
+ /**
+ * @return a collection of data flavors ( data types ) available in this
+ * Transferable
+ */
+ public Collection<String> getDataFlavors();
+
+ /**
+ * @return the component that created the Transferable or null if the source
+ * component is unknown
+ */
+ public Component getSourceComponent();
+
+}
diff --git a/server/src/com/vaadin/event/TransferableImpl.java b/server/src/com/vaadin/event/TransferableImpl.java
new file mode 100644
index 0000000000..4c973571f7
--- /dev/null
+++ b/server/src/com/vaadin/event/TransferableImpl.java
@@ -0,0 +1,47 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.ui.Component;
+
+/**
+ * TODO Javadoc!
+ *
+ * @since 6.3
+ */
+public class TransferableImpl implements Transferable {
+ private Map<String, Object> rawVariables = new HashMap<String, Object>();
+ private Component sourceComponent;
+
+ public TransferableImpl(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ this.sourceComponent = sourceComponent;
+ this.rawVariables = rawVariables;
+ }
+
+ @Override
+ public Component getSourceComponent() {
+ return sourceComponent;
+ }
+
+ @Override
+ public Object getData(String dataFlavor) {
+ return rawVariables.get(dataFlavor);
+ }
+
+ @Override
+ public void setData(String dataFlavor, Object value) {
+ rawVariables.put(dataFlavor, value);
+ }
+
+ @Override
+ public Collection<String> getDataFlavors() {
+ return rawVariables.keySet();
+ }
+
+}
diff --git a/server/src/com/vaadin/event/dd/DragAndDropEvent.java b/server/src/com/vaadin/event/dd/DragAndDropEvent.java
new file mode 100644
index 0000000000..b920d43469
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/DragAndDropEvent.java
@@ -0,0 +1,50 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.io.Serializable;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+
+/**
+ * DragAndDropEvent wraps information related to drag and drop operation. It is
+ * passed by terminal implementation for
+ * {@link DropHandler#drop(DragAndDropEvent)} and
+ * {@link AcceptCriterion#accept(DragAndDropEvent)} methods.
+ * <p>
+ * DragAndDropEvent instances contains both the dragged data in
+ * {@link Transferable} (generated by {@link DragSource} and details about the
+ * current drop event in {@link TargetDetails} (generated by {@link DropTarget}.
+ *
+ * @since 6.3
+ *
+ */
+public class DragAndDropEvent implements Serializable {
+ private Transferable transferable;
+ private TargetDetails dropTargetDetails;
+
+ public DragAndDropEvent(Transferable transferable,
+ TargetDetails dropTargetDetails) {
+ this.transferable = transferable;
+ this.dropTargetDetails = dropTargetDetails;
+ }
+
+ /**
+ * @return the Transferable instance representing the data dragged in this
+ * drag and drop event
+ */
+ public Transferable getTransferable() {
+ return transferable;
+ }
+
+ /**
+ * @return the TargetDetails containing drop target related details of drag
+ * and drop operation
+ */
+ public TargetDetails getTargetDetails() {
+ return dropTargetDetails;
+ }
+
+}
diff --git a/server/src/com/vaadin/event/dd/DragSource.java b/server/src/com/vaadin/event/dd/DragSource.java
new file mode 100644
index 0000000000..4daf0dcb18
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/DragSource.java
@@ -0,0 +1,52 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.util.Map;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Tree;
+
+/**
+ * DragSource is a {@link Component} that builds a {@link Transferable} for a
+ * drag and drop operation.
+ * <p>
+ * In Vaadin the drag and drop operation practically starts from client side
+ * component. The client side component initially defines the data that will be
+ * present in {@link Transferable} object on server side. If the server side
+ * counterpart of the component implements this interface, terminal
+ * implementation lets it create the {@link Transferable} instance from the raw
+ * client side "seed data". This way server side implementation may translate or
+ * extend the data that will be available for {@link DropHandler}.
+ *
+ * @since 6.3
+ *
+ */
+public interface DragSource extends Component {
+
+ /**
+ * DragSource may convert data added by client side component to meaningful
+ * values for server side developer or add other data based on it.
+ *
+ * <p>
+ * For example Tree converts item identifiers to generated string keys for
+ * the client side. Vaadin developer don't and can't know anything about
+ * these generated keys, only about item identifiers. When tree node is
+ * dragged client puts that key to {@link Transferable}s client side
+ * counterpart. In {@link Tree#getTransferable(Map)} the key is converted
+ * back to item identifier that the server side developer can use.
+ * <p>
+ *
+ * @since 6.3
+ * @param rawVariables
+ * the data that client side initially included in
+ * {@link Transferable}s client side counterpart.
+ * @return the {@link Transferable} instance that will be passed to
+ * {@link DropHandler} (and/or {@link AcceptCriterion})
+ */
+ public Transferable getTransferable(Map<String, Object> rawVariables);
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/DropHandler.java b/server/src/com/vaadin/event/dd/DropHandler.java
new file mode 100644
index 0000000000..7a15ea5b68
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/DropHandler.java
@@ -0,0 +1,61 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.io.Serializable;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.acceptcriteria.AcceptAll;
+import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+
+/**
+ * DropHandlers contain the actual business logic for drag and drop operations.
+ * <p>
+ * The {@link #drop(DragAndDropEvent)} method is used to receive the transferred
+ * data and the {@link #getAcceptCriterion()} method contains the (possibly
+ * client side verifiable) criterion whether the dragged data will be handled at
+ * all.
+ *
+ * @since 6.3
+ *
+ */
+public interface DropHandler extends Serializable {
+
+ /**
+ * Drop method is called when the end user has finished the drag operation
+ * on a {@link DropTarget} and {@link DragAndDropEvent} has passed
+ * {@link AcceptCriterion} defined by {@link #getAcceptCriterion()} method.
+ * The actual business logic of drag and drop operation is implemented into
+ * this method.
+ *
+ * @param event
+ * the event related to this drop
+ */
+ public void drop(DragAndDropEvent event);
+
+ /**
+ * Returns the {@link AcceptCriterion} used to evaluate whether the
+ * {@link Transferable} will be handed over to
+ * {@link DropHandler#drop(DragAndDropEvent)} method. If client side can't
+ * verify the {@link AcceptCriterion}, the same criteria may be tested also
+ * prior to actual drop - during the drag operation.
+ * <p>
+ * Based on information from {@link AcceptCriterion} components may display
+ * some hints for the end user whether the drop will be accepted or not.
+ * <p>
+ * Vaadin contains a variety of criteria built in that can be composed to
+ * more complex criterion. If the build in criteria are not enough,
+ * developer can use a {@link ServerSideCriterion} or build own custom
+ * criterion with client side counterpart.
+ * <p>
+ * If developer wants to handle everything in the
+ * {@link #drop(DragAndDropEvent)} method, {@link AcceptAll} instance can be
+ * returned.
+ *
+ * @return the {@link AcceptCriterion}
+ */
+ public AcceptCriterion getAcceptCriterion();
+
+}
diff --git a/server/src/com/vaadin/event/dd/DropTarget.java b/server/src/com/vaadin/event/dd/DropTarget.java
new file mode 100644
index 0000000000..c18aa60b19
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/DropTarget.java
@@ -0,0 +1,42 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.util.Map;
+
+import com.vaadin.ui.Component;
+
+/**
+ * DropTarget is an interface for components supporting drop operations. A
+ * component that wants to receive drop events should implement this interface
+ * and provide a {@link DropHandler} which will handle the actual drop event.
+ *
+ * @since 6.3
+ */
+public interface DropTarget extends Component {
+
+ /**
+ * @return the drop hanler that will receive the dragged data or null if
+ * drops are not currently accepted
+ */
+ public DropHandler getDropHandler();
+
+ /**
+ * Called before the {@link DragAndDropEvent} is passed to
+ * {@link DropHandler}. Implementation may for example translate the drop
+ * target details provided by the client side (drop target) to meaningful
+ * server side values. If null is returned the terminal implementation will
+ * automatically create a {@link TargetDetails} with raw client side data.
+ *
+ * @see DragSource#getTransferable(Map)
+ *
+ * @param clientVariables
+ * data passed from the DropTargets client side counterpart.
+ * @return A DropTargetDetails object with the translated data or null to
+ * use a default implementation.
+ */
+ public TargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables);
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/TargetDetails.java b/server/src/com/vaadin/event/dd/TargetDetails.java
new file mode 100644
index 0000000000..a352fbec60
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/TargetDetails.java
@@ -0,0 +1,37 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Tree.TreeTargetDetails;
+
+/**
+ * TargetDetails wraps drop target related information about
+ * {@link DragAndDropEvent}.
+ * <p>
+ * When a TargetDetails object is used in {@link DropHandler} it is often
+ * preferable to cast the TargetDetails to an implementation provided by
+ * DropTarget like {@link TreeTargetDetails}. They often provide a better typed,
+ * drop target specific API.
+ *
+ * @since 6.3
+ *
+ */
+public interface TargetDetails extends Serializable {
+
+ /**
+ * Gets target data associated with the given string key
+ *
+ * @param key
+ * @return The data associated with the key
+ */
+ public Object getData(String key);
+
+ /**
+ * @return the drop target on which the {@link DragAndDropEvent} happened.
+ */
+ public DropTarget getTarget();
+
+}
diff --git a/server/src/com/vaadin/event/dd/TargetDetailsImpl.java b/server/src/com/vaadin/event/dd/TargetDetailsImpl.java
new file mode 100644
index 0000000000..4a459777ed
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/TargetDetailsImpl.java
@@ -0,0 +1,46 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A HashMap backed implementation of {@link TargetDetails} for terminal
+ * implementation and for extension.
+ *
+ * @since 6.3
+ *
+ */
+@SuppressWarnings("serial")
+public class TargetDetailsImpl implements TargetDetails {
+
+ private HashMap<String, Object> data = new HashMap<String, Object>();
+ private DropTarget dropTarget;
+
+ protected TargetDetailsImpl(Map<String, Object> rawDropData) {
+ data.putAll(rawDropData);
+ }
+
+ public TargetDetailsImpl(Map<String, Object> rawDropData,
+ DropTarget dropTarget) {
+ this(rawDropData);
+ this.dropTarget = dropTarget;
+ }
+
+ @Override
+ public Object getData(String key) {
+ return data.get(key);
+ }
+
+ public Object setData(String key, Object value) {
+ return data.put(key, value);
+ }
+
+ @Override
+ public DropTarget getTarget() {
+ return dropTarget;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java
new file mode 100644
index 0000000000..1457ea9df3
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.dd.DragAndDropEvent;
+
+/**
+ * Criterion that accepts all drops anywhere on the component.
+ * <p>
+ * Note! Class is singleton, use {@link #get()} method to get the instance.
+ *
+ *
+ * @since 6.3
+ *
+ */
+public final class AcceptAll extends ClientSideCriterion {
+
+ private static final long serialVersionUID = 7406683402153141461L;
+ private static AcceptCriterion singleton = new AcceptAll();
+
+ private AcceptAll() {
+ }
+
+ public static AcceptCriterion get() {
+ return singleton;
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ return true;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java
new file mode 100644
index 0000000000..c0f04d362f
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java
@@ -0,0 +1,75 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import java.io.Serializable;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * Criterion that can be used create policy to accept/discard dragged content
+ * (presented by {@link Transferable}).
+ *
+ * The drag and drop mechanism will verify the criteria returned by
+ * {@link DropHandler#getAcceptCriterion()} before calling
+ * {@link DropHandler#drop(DragAndDropEvent)}.
+ *
+ * The criteria can be evaluated either on the client (browser - see
+ * {@link ClientSideCriterion}) or on the server (see
+ * {@link ServerSideCriterion}). If no constraints are needed, an
+ * {@link AcceptAll} can be used.
+ *
+ * In addition to accepting or rejecting a possible drop, criteria can provide
+ * additional hints for client side painting.
+ *
+ * @see DropHandler
+ * @see ClientSideCriterion
+ * @see ServerSideCriterion
+ *
+ * @since 6.3
+ */
+public interface AcceptCriterion extends Serializable {
+
+ /**
+ * Returns whether the criteria can be checked on the client or whether a
+ * server request is needed to check the criteria.
+ *
+ * This requirement may depend on the state of the criterion (e.g. logical
+ * operations between criteria), so this cannot be based on a marker
+ * interface.
+ */
+ public boolean isClientSideVerifiable();
+
+ public void paint(PaintTarget target) throws PaintException;
+
+ /**
+ * This needs to be implemented iff criterion does some lazy server side
+ * initialization. The UIDL painted in this method will be passed to client
+ * side drop handler implementation. Implementation can assume that
+ * {@link #accept(DragAndDropEvent)} is called before this method.
+ *
+ * @param target
+ * @throws PaintException
+ */
+ public void paintResponse(PaintTarget target) throws PaintException;
+
+ /**
+ * Validates the data in event to be appropriate for the
+ * {@link DropHandler#drop(DragAndDropEvent)} method.
+ * <p>
+ * Note that even if your criterion is validated on client side, you should
+ * always validate the data on server side too.
+ *
+ * @param dragEvent
+ * @return
+ */
+ public boolean accept(DragAndDropEvent dragEvent);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/And.java b/server/src/com/vaadin/event/dd/acceptcriteria/And.java
new file mode 100644
index 0000000000..4122d67160
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/And.java
@@ -0,0 +1,54 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * A compound criterion that accepts the drag if all of its criteria accepts the
+ * drag.
+ *
+ * @see Or
+ *
+ * @since 6.3
+ *
+ */
+public class And extends ClientSideCriterion {
+
+ private static final long serialVersionUID = -5242574480825471748L;
+ protected ClientSideCriterion[] criteria;
+
+ /**
+ *
+ * @param criteria
+ * criteria of which the And criterion will be composed
+ */
+ public And(ClientSideCriterion... criteria) {
+ this.criteria = criteria;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ for (ClientSideCriterion crit : criteria) {
+ crit.paint(target);
+ }
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ for (ClientSideCriterion crit : criteria) {
+ if (!crit.accept(dragEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java
new file mode 100644
index 0000000000..7d2c42ecb0
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java
@@ -0,0 +1,61 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * Parent class for criteria that can be completely validated on client side.
+ * All classes that provide criteria that can be completely validated on client
+ * side should extend this class.
+ *
+ * It is recommended that subclasses of ClientSideCriterion re-validate the
+ * condition on the server side in
+ * {@link AcceptCriterion#accept(com.vaadin.event.dd.DragAndDropEvent)} after
+ * the client side validation has accepted a transfer.
+ *
+ * @since 6.3
+ */
+public abstract class ClientSideCriterion implements Serializable,
+ AcceptCriterion {
+
+ /*
+ * All criteria that extend this must be completely validatable on client
+ * side.
+ *
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#isClientSideVerifiable
+ * ()
+ */
+ @Override
+ public final boolean isClientSideVerifiable() {
+ return true;
+ }
+
+ @Override
+ public void paint(PaintTarget target) throws PaintException {
+ target.startTag("-ac");
+ target.addAttribute("name", getIdentifier());
+ paintContent(target);
+ target.endTag("-ac");
+ }
+
+ protected void paintContent(PaintTarget target) throws PaintException {
+ }
+
+ protected String getIdentifier() {
+ return getClass().getCanonicalName();
+ }
+
+ @Override
+ public final void paintResponse(PaintTarget target) throws PaintException {
+ // NOP, nothing to do as this is client side verified criterion
+ }
+
+}
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java b/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java
new file mode 100644
index 0000000000..4c52698a4a
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java
@@ -0,0 +1,53 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * A Criterion that checks whether {@link Transferable} contains given data
+ * flavor. The developer might for example accept the incoming data only if it
+ * contains "Url" or "Text".
+ *
+ * @since 6.3
+ */
+public class ContainsDataFlavor extends ClientSideCriterion {
+
+ private String dataFlavorId;
+
+ /**
+ * Constructs a new instance of {@link ContainsDataFlavor}.
+ *
+ * @param dataFlawor
+ * the type of data that will be checked from
+ * {@link Transferable}
+ */
+ public ContainsDataFlavor(String dataFlawor) {
+ dataFlavorId = dataFlawor;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ target.addAttribute("p", dataFlavorId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ return dragEvent.getTransferable().getDataFlavors()
+ .contains(dataFlavorId);
+ }
+
+ @Override
+ protected String getIdentifier() {
+ // extending classes use client side implementation from this class
+ return ContainsDataFlavor.class.getCanonicalName();
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/Not.java b/server/src/com/vaadin/event/dd/acceptcriteria/Not.java
new file mode 100644
index 0000000000..1ed40a324d
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/Not.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * Criterion that wraps another criterion and inverts its return value.
+ *
+ * @since 6.3
+ *
+ */
+public class Not extends ClientSideCriterion {
+
+ private static final long serialVersionUID = 1131422338558613244L;
+ private AcceptCriterion acceptCriterion;
+
+ public Not(ClientSideCriterion acceptCriterion) {
+ this.acceptCriterion = acceptCriterion;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ acceptCriterion.paint(target);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ return !acceptCriterion.accept(dragEvent);
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/Or.java b/server/src/com/vaadin/event/dd/acceptcriteria/Or.java
new file mode 100644
index 0000000000..6ad45c54af
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/Or.java
@@ -0,0 +1,52 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * A compound criterion that accepts the drag if any of its criterion accepts
+ * it.
+ *
+ * @see And
+ *
+ * @since 6.3
+ *
+ */
+public class Or extends ClientSideCriterion {
+ private static final long serialVersionUID = 1L;
+ private AcceptCriterion criteria[];
+
+ /**
+ * @param criteria
+ * the criteria of which the Or criteria will be composed
+ */
+ public Or(ClientSideCriterion... criteria) {
+ this.criteria = criteria;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ for (AcceptCriterion crit : criteria) {
+ crit.paint(target);
+ }
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ for (AcceptCriterion crit : criteria) {
+ if (crit.accept(dragEvent)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java b/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java
new file mode 100644
index 0000000000..47f06d434c
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import java.io.Serializable;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * Parent class for criteria which are verified on the server side during a drag
+ * operation to accept/discard dragged content (presented by
+ * {@link Transferable}).
+ * <p>
+ * Subclasses should implement the
+ * {@link AcceptCriterion#accept(com.vaadin.event.dd.DragAndDropEvent)} method.
+ * <p>
+ * As all server side state can be used to make a decision, this is more
+ * flexible than {@link ClientSideCriterion}. However, this does require
+ * additional requests from the browser to the server during a drag operation.
+ *
+ * @see AcceptCriterion
+ * @see ClientSideCriterion
+ *
+ * @since 6.3
+ */
+public abstract class ServerSideCriterion implements Serializable,
+ AcceptCriterion {
+
+ private static final long serialVersionUID = 2128510128911628902L;
+
+ @Override
+ public final boolean isClientSideVerifiable() {
+ return false;
+ }
+
+ @Override
+ public void paint(PaintTarget target) throws PaintException {
+ target.startTag("-ac");
+ target.addAttribute("name", getIdentifier());
+ paintContent(target);
+ target.endTag("-ac");
+ }
+
+ public void paintContent(PaintTarget target) {
+ }
+
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ }
+
+ protected String getIdentifier() {
+ return ServerSideCriterion.class.getCanonicalName();
+ }
+}
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java
new file mode 100644
index 0000000000..d4fd20c952
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java
@@ -0,0 +1,67 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.event.TransferableImpl;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.ui.Component;
+
+/**
+ * Client side criteria that checks if the drag source is one of the given
+ * components.
+ *
+ * @since 6.3
+ */
+@SuppressWarnings("serial")
+public class SourceIs extends ClientSideCriterion {
+
+ private Component[] components;
+
+ public SourceIs(Component... component) {
+ components = component;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ int paintedComponents = 0;
+ for (int i = 0; i < components.length; i++) {
+ Component c = components[i];
+ if (c.getApplication() != null) {
+ target.addAttribute("component" + paintedComponents++, c);
+ } else {
+ Logger.getLogger(SourceIs.class.getName())
+ .log(Level.WARNING,
+ "SourceIs component {0} at index {1} is not attached to the component hierachy and will thus be ignored",
+ new Object[] { c.getClass().getName(),
+ Integer.valueOf(i) });
+ }
+ }
+ target.addAttribute("c", paintedComponents);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ if (dragEvent.getTransferable() instanceof TransferableImpl) {
+ Component sourceComponent = ((TransferableImpl) dragEvent
+ .getTransferable()).getSourceComponent();
+ for (Component c : components) {
+ if (c == sourceComponent) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java
new file mode 100644
index 0000000000..a644b858e2
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java
@@ -0,0 +1,51 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.TransferableImpl;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Table;
+import com.vaadin.ui.Tree;
+
+/**
+ *
+ * A criterion that ensures the drag source is the same as drop target. Eg.
+ * {@link Tree} or {@link Table} could support only re-ordering of items, but no
+ * {@link Transferable}s coming outside.
+ * <p>
+ * Note! Class is singleton, use {@link #get()} method to get the instance.
+ *
+ * @since 6.3
+ *
+ */
+public class SourceIsTarget extends ClientSideCriterion {
+
+ private static final long serialVersionUID = -451399314705532584L;
+ private static SourceIsTarget instance = new SourceIsTarget();
+
+ private SourceIsTarget() {
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ if (dragEvent.getTransferable() instanceof TransferableImpl) {
+ Component sourceComponent = ((TransferableImpl) dragEvent
+ .getTransferable()).getSourceComponent();
+ DropTarget target = dragEvent.getTargetDetails().getTarget();
+ return sourceComponent == target;
+ }
+ return false;
+ }
+
+ public static synchronized SourceIsTarget get() {
+ return instance;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java b/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java
new file mode 100644
index 0000000000..5df8f3f618
--- /dev/null
+++ b/server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java
@@ -0,0 +1,72 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+/**
+ *
+ */
+package com.vaadin.event.dd.acceptcriteria;
+
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * Criterion for checking if drop target details contains the specific property
+ * with the specific value. Currently only String values are supported.
+ *
+ * @since 6.3
+ *
+ * TODO add support for other basic data types that we support in UIDL.
+ *
+ */
+public class TargetDetailIs extends ClientSideCriterion {
+
+ private static final long serialVersionUID = 763165450054331246L;
+ private String propertyName;
+ private Object value;
+
+ /**
+ * Constructs a criterion which ensures that the value there is a value in
+ * {@link TargetDetails} that equals the reference value.
+ *
+ * @param dataFlavor
+ * the type of data to be checked
+ * @param value
+ * the reference value to which the drop target detail will be
+ * compared
+ */
+ public TargetDetailIs(String dataFlavor, String value) {
+ propertyName = dataFlavor;
+ this.value = value;
+ }
+
+ public TargetDetailIs(String dataFlavor, Boolean true1) {
+ propertyName = dataFlavor;
+ value = true1;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ target.addAttribute("p", propertyName);
+ if (value instanceof Boolean) {
+ target.addAttribute("v", ((Boolean) value).booleanValue());
+ target.addAttribute("t", "b");
+ } else if (value instanceof String) {
+ target.addAttribute("v", (String) value);
+ }
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ Object data = dragEvent.getTargetDetails().getData(propertyName);
+ return value.equals(data);
+ }
+
+ @Override
+ protected String getIdentifier() {
+ // sub classes by default use VDropDetailEquals a client implementation
+ return TargetDetailIs.class.getCanonicalName();
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/event/package.html b/server/src/com/vaadin/event/package.html
new file mode 100644
index 0000000000..2e7e17b892
--- /dev/null
+++ b/server/src/com/vaadin/event/package.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides classes and interfaces for the inheritable event
+model. The model supports inheritable events and a flexible way of
+registering and unregistering event listeners. It's a fundamental building
+block of Vaadin, and as it is included in
+{@link com.vaadin.ui.AbstractComponent}, all UI components
+automatically support it.</p>
+
+<h2>Package Specification</h2>
+
+<p>The core of the event model is the inheritable event class
+hierarchy, and the {@link com.vaadin.event.EventRouter EventRouter}
+which provide a simple, ubiquitous mechanism to transport events to all
+interested parties.</p>
+
+<p>The power of the event inheritance arises from the possibility of
+receiving not only the events of the registered type, <i>but also the
+ones which are inherited from it</i>. For example, let's assume that there
+are the events <code>GeneralEvent</code> and <code>SpecializedEvent</code>
+so that the latter inherits the former. Furthermore we have an object
+<code>A</code> which registers to receive <code>GeneralEvent</code> type
+events from the object <code>B</code>. <code>A</code> would of course
+receive all <code>GeneralEvent</code>s generated by <code>B</code>, but in
+addition to this, <code>A</code> would also receive all
+<code>SpecializedEvent</code>s generated by <code>B</code>. However, if
+<code>B</code> generates some other events that do not have
+<code>GeneralEvent</code> as an ancestor, <code>A</code> would not receive
+them unless it registers to listen for them, too.</p>
+
+<p>The interface to attaching and detaching listeners to and from an object
+works with methods. One specifies the event that should trigger the listener,
+the trigger method that should be called when a suitable event occurs and the
+object owning the method. From these a new listener is constructed and added
+to the event router of the specified component.</p>
+
+<p>The interface is defined in
+{@link com.vaadin.event.MethodEventSource MethodEventSource}, and a
+straightforward implementation of it is defined in
+{@link com.vaadin.event.EventRouter EventRouter} which also includes
+a method to actually fire the events.</p>
+
+<p>All fired events are passed to all registered listeners, which are of
+type {@link com.vaadin.event.ListenerMethod ListenerMethod}. The
+listener then checks if the event type matches with the specified event
+type and calls the specified trigger method if it does.</p>
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/external/json/JSONArray.java b/server/src/com/vaadin/external/json/JSONArray.java
new file mode 100644
index 0000000000..2307749ffc
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONArray.java
@@ -0,0 +1,963 @@
+package com.vaadin.external.json;
+
+/*
+ Copyright (c) 2002 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A JSONArray is an ordered sequence of values. Its external text form is a
+ * string wrapped in square brackets with commas separating the values. The
+ * internal form is an object having <code>get</code> and <code>opt</code>
+ * methods for accessing the values by index, and <code>put</code> methods for
+ * adding or replacing values. The values can be any of these types:
+ * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>,
+ * <code>Number</code>, <code>String</code>, or the
+ * <code>JSONObject.NULL object</code>.
+ * <p>
+ * The constructor can convert a JSON text into a Java object. The
+ * <code>toString</code> method converts to JSON text.
+ * <p>
+ * A <code>get</code> method returns a value if one can be found, and throws an
+ * exception if one cannot be found. An <code>opt</code> method returns a
+ * default value instead of throwing an exception, and so is useful for
+ * obtaining optional values.
+ * <p>
+ * The generic <code>get()</code> and <code>opt()</code> methods return an
+ * object which you can cast or query for type. There are also typed
+ * <code>get</code> and <code>opt</code> methods that do type checking and type
+ * coercion for you.
+ * <p>
+ * The texts produced by the <code>toString</code> methods strictly conform to
+ * JSON syntax rules. The constructors are more forgiving in the texts they will
+ * accept:
+ * <ul>
+ * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
+ * before the closing bracket.</li>
+ * <li>The <code>null</code> value will be inserted when there is <code>,</code>
+ * &nbsp;<small>(comma)</small> elision.</li>
+ * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
+ * quote)</small>.</li>
+ * <li>Strings do not need to be quoted at all if they do not begin with a quote
+ * or single quote, and if they do not contain leading or trailing spaces, and
+ * if they do not contain any of these characters:
+ * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and
+ * if they are not the reserved words <code>true</code>, <code>false</code>, or
+ * <code>null</code>.</li>
+ * <li>Values can be separated by <code>;</code> <small>(semicolon)</small> as
+ * well as by <code>,</code> <small>(comma)</small>.</li>
+ * <li>Numbers may have the <code>0x-</code> <small>(hex)</small> prefix.</li>
+ * </ul>
+ *
+ * @author JSON.org
+ * @version 2011-08-25
+ */
+public class JSONArray implements Serializable {
+
+ /**
+ * The arrayList where the JSONArray's properties are kept.
+ */
+ private ArrayList myArrayList;
+
+ /**
+ * Construct an empty JSONArray.
+ */
+ public JSONArray() {
+ myArrayList = new ArrayList();
+ }
+
+ /**
+ * Construct a JSONArray from a JSONTokener.
+ *
+ * @param x
+ * A JSONTokener
+ * @throws JSONException
+ * If there is a syntax error.
+ */
+ public JSONArray(JSONTokener x) throws JSONException {
+ this();
+ if (x.nextClean() != '[') {
+ throw x.syntaxError("A JSONArray text must start with '['");
+ }
+ if (x.nextClean() != ']') {
+ x.back();
+ for (;;) {
+ if (x.nextClean() == ',') {
+ x.back();
+ myArrayList.add(JSONObject.NULL);
+ } else {
+ x.back();
+ myArrayList.add(x.nextValue());
+ }
+ switch (x.nextClean()) {
+ case ';':
+ case ',':
+ if (x.nextClean() == ']') {
+ return;
+ }
+ x.back();
+ break;
+ case ']':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or ']'");
+ }
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONArray from a source JSON text.
+ *
+ * @param source
+ * A string that begins with <code>[</code>&nbsp;<small>(left
+ * bracket)</small> and ends with <code>]</code>
+ * &nbsp;<small>(right bracket)</small>.
+ * @throws JSONException
+ * If there is a syntax error.
+ */
+ public JSONArray(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+ /**
+ * Construct a JSONArray from a Collection.
+ *
+ * @param collection
+ * A Collection.
+ */
+ public JSONArray(Collection collection) {
+ myArrayList = new ArrayList();
+ if (collection != null) {
+ Iterator iter = collection.iterator();
+ while (iter.hasNext()) {
+ myArrayList.add(JSONObject.wrap(iter.next()));
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONArray from an array
+ *
+ * @throws JSONException
+ * If not an array.
+ */
+ public JSONArray(Object array) throws JSONException {
+ this();
+ if (array.getClass().isArray()) {
+ int length = Array.getLength(array);
+ for (int i = 0; i < length; i += 1) {
+ this.put(JSONObject.wrap(Array.get(array, i)));
+ }
+ } else {
+ throw new JSONException(
+ "JSONArray initial value should be a string or collection or array.");
+ }
+ }
+
+ /**
+ * Get the object value associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return An object value.
+ * @throws JSONException
+ * If there is no value for the index.
+ */
+ public Object get(int index) throws JSONException {
+ Object object = opt(index);
+ if (object == null) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ return object;
+ }
+
+ /**
+ * Get the boolean value associated with an index. The string values "true"
+ * and "false" are converted to boolean.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The truth.
+ * @throws JSONException
+ * If there is no value for the index or if the value is not
+ * convertible to boolean.
+ */
+ public boolean getBoolean(int index) throws JSONException {
+ Object object = get(index);
+ if (object.equals(Boolean.FALSE)
+ || (object instanceof String && ((String) object)
+ .equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE)
+ || (object instanceof String && ((String) object)
+ .equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a boolean.");
+ }
+
+ /**
+ * Get the double value associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException
+ * If the key is not found or if the value cannot be converted
+ * to a number.
+ */
+ public double getDouble(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ? ((Number) object).doubleValue()
+ : Double.parseDouble((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the int value associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException
+ * If the key is not found or if the value is not a number.
+ */
+ public int getInt(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ? ((Number) object).intValue()
+ : Integer.parseInt((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the JSONArray associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return A JSONArray value.
+ * @throws JSONException
+ * If there is no value for the index. or if the value is not a
+ * JSONArray
+ */
+ public JSONArray getJSONArray(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a JSONArray.");
+ }
+
+ /**
+ * Get the JSONObject associated with an index.
+ *
+ * @param index
+ * subscript
+ * @return A JSONObject value.
+ * @throws JSONException
+ * If there is no value for the index or if the value is not a
+ * JSONObject
+ */
+ public JSONObject getJSONObject(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a JSONObject.");
+ }
+
+ /**
+ * Get the long value associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException
+ * If the key is not found or if the value cannot be converted
+ * to a number.
+ */
+ public long getLong(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ? ((Number) object).longValue()
+ : Long.parseLong((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the string associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return A string value.
+ * @throws JSONException
+ * If there is no string value for the index.
+ */
+ public String getString(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof String) {
+ return (String) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] not a string.");
+ }
+
+ /**
+ * Determine if the value is null.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return true if the value at the index is null, or if there is no value.
+ */
+ public boolean isNull(int index) {
+ return JSONObject.NULL.equals(opt(index));
+ }
+
+ /**
+ * Make a string from the contents of this JSONArray. The
+ * <code>separator</code> string is inserted between each element. Warning:
+ * This method assumes that the data structure is acyclical.
+ *
+ * @param separator
+ * A string that will be inserted between the elements.
+ * @return a string.
+ * @throws JSONException
+ * If the array contains an invalid number.
+ */
+ public String join(String separator) throws JSONException {
+ int len = length();
+ StringBuffer sb = new StringBuffer();
+
+ for (int i = 0; i < len; i += 1) {
+ if (i > 0) {
+ sb.append(separator);
+ }
+ sb.append(JSONObject.valueToString(myArrayList.get(i)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the number of elements in the JSONArray, included nulls.
+ *
+ * @return The length (or size).
+ */
+ public int length() {
+ return myArrayList.size();
+ }
+
+ /**
+ * Get the optional object value associated with an index.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return An object value, or null if there is no object at that index.
+ */
+ public Object opt(int index) {
+ return (index < 0 || index >= length()) ? null : myArrayList.get(index);
+ }
+
+ /**
+ * Get the optional boolean value associated with an index. It returns false
+ * if there is no value at that index, or if the value is not Boolean.TRUE
+ * or the String "true".
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index) {
+ return optBoolean(index, false);
+ }
+
+ /**
+ * Get the optional boolean value associated with an index. It returns the
+ * defaultValue if there is no value at that index or if it is not a Boolean
+ * or the String "true" or "false" (case insensitive).
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @param defaultValue
+ * A boolean default.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index, boolean defaultValue) {
+ try {
+ return getBoolean(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional double value associated with an index. NaN is returned
+ * if there is no value for the index, or if the value is not a number and
+ * cannot be converted to a number.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public double optDouble(int index) {
+ return optDouble(index, Double.NaN);
+ }
+
+ /**
+ * Get the optional double value associated with an index. The defaultValue
+ * is returned if there is no value for the index, or if the value is not a
+ * number and cannot be converted to a number.
+ *
+ * @param index
+ * subscript
+ * @param defaultValue
+ * The default value.
+ * @return The value.
+ */
+ public double optDouble(int index, double defaultValue) {
+ try {
+ return getDouble(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional int value associated with an index. Zero is returned if
+ * there is no value for the index, or if the value is not a number and
+ * cannot be converted to a number.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public int optInt(int index) {
+ return optInt(index, 0);
+ }
+
+ /**
+ * Get the optional int value associated with an index. The defaultValue is
+ * returned if there is no value for the index, or if the value is not a
+ * number and cannot be converted to a number.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @param defaultValue
+ * The default value.
+ * @return The value.
+ */
+ public int optInt(int index, int defaultValue) {
+ try {
+ return getInt(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional JSONArray associated with an index.
+ *
+ * @param index
+ * subscript
+ * @return A JSONArray value, or null if the index has no value, or if the
+ * value is not a JSONArray.
+ */
+ public JSONArray optJSONArray(int index) {
+ Object o = opt(index);
+ return o instanceof JSONArray ? (JSONArray) o : null;
+ }
+
+ /**
+ * Get the optional JSONObject associated with an index. Null is returned if
+ * the key is not found, or null if the index has no value, or if the value
+ * is not a JSONObject.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return A JSONObject value.
+ */
+ public JSONObject optJSONObject(int index) {
+ Object o = opt(index);
+ return o instanceof JSONObject ? (JSONObject) o : null;
+ }
+
+ /**
+ * Get the optional long value associated with an index. Zero is returned if
+ * there is no value for the index, or if the value is not a number and
+ * cannot be converted to a number.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public long optLong(int index) {
+ return optLong(index, 0);
+ }
+
+ /**
+ * Get the optional long value associated with an index. The defaultValue is
+ * returned if there is no value for the index, or if the value is not a
+ * number and cannot be converted to a number.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @param defaultValue
+ * The default value.
+ * @return The value.
+ */
+ public long optLong(int index, long defaultValue) {
+ try {
+ return getLong(index);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional string value associated with an index. It returns an
+ * empty string if there is no value at that index. If the value is not a
+ * string and is not null, then it is coverted to a string.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @return A String value.
+ */
+ public String optString(int index) {
+ return optString(index, "");
+ }
+
+ /**
+ * Get the optional string associated with an index. The defaultValue is
+ * returned if the key is not found.
+ *
+ * @param index
+ * The index must be between 0 and length() - 1.
+ * @param defaultValue
+ * The default value.
+ * @return A String value.
+ */
+ public String optString(int index, String defaultValue) {
+ Object object = opt(index);
+ return JSONObject.NULL.equals(object) ? object.toString()
+ : defaultValue;
+ }
+
+ /**
+ * Append a boolean value. This increases the array's length by one.
+ *
+ * @param value
+ * A boolean value.
+ * @return this.
+ */
+ public JSONArray put(boolean value) {
+ put(value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONArray which
+ * is produced from a Collection.
+ *
+ * @param value
+ * A Collection value.
+ * @return this.
+ */
+ public JSONArray put(Collection value) {
+ put(new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Append a double value. This increases the array's length by one.
+ *
+ * @param value
+ * A double value.
+ * @throws JSONException
+ * if the value is not finite.
+ * @return this.
+ */
+ public JSONArray put(double value) throws JSONException {
+ Double d = new Double(value);
+ JSONObject.testValidity(d);
+ put(d);
+ return this;
+ }
+
+ /**
+ * Append an int value. This increases the array's length by one.
+ *
+ * @param value
+ * An int value.
+ * @return this.
+ */
+ public JSONArray put(int value) {
+ put(new Integer(value));
+ return this;
+ }
+
+ /**
+ * Append an long value. This increases the array's length by one.
+ *
+ * @param value
+ * A long value.
+ * @return this.
+ */
+ public JSONArray put(long value) {
+ put(new Long(value));
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONObject which
+ * is produced from a Map.
+ *
+ * @param value
+ * A Map value.
+ * @return this.
+ */
+ public JSONArray put(Map value) {
+ put(new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Append an object value. This increases the array's length by one.
+ *
+ * @param value
+ * An object value. The value should be a Boolean, Double,
+ * Integer, JSONArray, JSONObject, Long, or String, or the
+ * JSONObject.NULL object.
+ * @return this.
+ */
+ public JSONArray put(Object value) {
+ myArrayList.add(value);
+ return this;
+ }
+
+ /**
+ * Put or replace a boolean value in the JSONArray. If the index is greater
+ * than the length of the JSONArray, then null elements will be added as
+ * necessary to pad it out.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * A boolean value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative.
+ */
+ public JSONArray put(int index, boolean value) throws JSONException {
+ put(index, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONArray which
+ * is produced from a Collection.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * A Collection value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative or if the value is not finite.
+ */
+ public JSONArray put(int index, Collection value) throws JSONException {
+ put(index, new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Put or replace a double value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad it
+ * out.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * A double value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative or if the value is not finite.
+ */
+ public JSONArray put(int index, double value) throws JSONException {
+ put(index, new Double(value));
+ return this;
+ }
+
+ /**
+ * Put or replace an int value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad it
+ * out.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * An int value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative.
+ */
+ public JSONArray put(int index, int value) throws JSONException {
+ put(index, new Integer(value));
+ return this;
+ }
+
+ /**
+ * Put or replace a long value. If the index is greater than the length of
+ * the JSONArray, then null elements will be added as necessary to pad it
+ * out.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * A long value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative.
+ */
+ public JSONArray put(int index, long value) throws JSONException {
+ put(index, new Long(value));
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONObject that
+ * is produced from a Map.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * The Map value.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative or if the the value is an invalid
+ * number.
+ */
+ public JSONArray put(int index, Map value) throws JSONException {
+ put(index, new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Put or replace an object value in the JSONArray. If the index is greater
+ * than the length of the JSONArray, then null elements will be added as
+ * necessary to pad it out.
+ *
+ * @param index
+ * The subscript.
+ * @param value
+ * The value to put into the array. The value should be a
+ * Boolean, Double, Integer, JSONArray, JSONObject, Long, or
+ * String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException
+ * If the index is negative or if the the value is an invalid
+ * number.
+ */
+ public JSONArray put(int index, Object value) throws JSONException {
+ JSONObject.testValidity(value);
+ if (index < 0) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ if (index < length()) {
+ myArrayList.set(index, value);
+ } else {
+ while (index != length()) {
+ put(JSONObject.NULL);
+ }
+ put(value);
+ }
+ return this;
+ }
+
+ /**
+ * Remove an index and close the hole.
+ *
+ * @param index
+ * The index of the element to be removed.
+ * @return The value that was associated with the index, or null if there
+ * was no value.
+ */
+ public Object remove(int index) {
+ Object o = opt(index);
+ myArrayList.remove(index);
+ return o;
+ }
+
+ /**
+ * Produce a JSONObject by combining a JSONArray of names with the values of
+ * this JSONArray.
+ *
+ * @param names
+ * A JSONArray containing a list of key strings. These will be
+ * paired with the values.
+ * @return A JSONObject, or null if there are no names or if this JSONArray
+ * has no values.
+ * @throws JSONException
+ * If any of the names are null.
+ */
+ public JSONObject toJSONObject(JSONArray names) throws JSONException {
+ if (names == null || names.length() == 0 || length() == 0) {
+ return null;
+ }
+ JSONObject jo = new JSONObject();
+ for (int i = 0; i < names.length(); i += 1) {
+ jo.put(names.getString(i), opt(i));
+ }
+ return jo;
+ }
+
+ /**
+ * Make a JSON text of this JSONArray. For compactness, no unnecessary
+ * whitespace is added. If it is not possible to produce a syntactically
+ * correct JSON text then null will be returned instead. This could occur if
+ * the array contains an invalid number.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, transmittable representation of the
+ * array.
+ */
+ @Override
+ public String toString() {
+ try {
+ return '[' + join(",") + ']';
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONArray. Warning: This method
+ * assumes that the data structure is acyclical.
+ *
+ * @param indentFactor
+ * The number of spaces to add to each level of indentation.
+ * @return a printable, displayable, transmittable representation of the
+ * object, beginning with <code>[</code>&nbsp;<small>(left
+ * bracket)</small> and ending with <code>]</code>
+ * &nbsp;<small>(right bracket)</small>.
+ * @throws JSONException
+ */
+ public String toString(int indentFactor) throws JSONException {
+ return toString(indentFactor, 0);
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONArray. Warning: This method
+ * assumes that the data structure is acyclical.
+ *
+ * @param indentFactor
+ * The number of spaces to add to each level of indentation.
+ * @param indent
+ * The indention of the top level.
+ * @return a printable, displayable, transmittable representation of the
+ * array.
+ * @throws JSONException
+ */
+ String toString(int indentFactor, int indent) throws JSONException {
+ int len = length();
+ if (len == 0) {
+ return "[]";
+ }
+ int i;
+ StringBuffer sb = new StringBuffer("[");
+ if (len == 1) {
+ sb.append(JSONObject.valueToString(myArrayList.get(0),
+ indentFactor, indent));
+ } else {
+ int newindent = indent + indentFactor;
+ sb.append('\n');
+ for (i = 0; i < len; i += 1) {
+ if (i > 0) {
+ sb.append(",\n");
+ }
+ for (int j = 0; j < newindent; j += 1) {
+ sb.append(' ');
+ }
+ sb.append(JSONObject.valueToString(myArrayList.get(i),
+ indentFactor, newindent));
+ }
+ sb.append('\n');
+ for (i = 0; i < indent; i += 1) {
+ sb.append(' ');
+ }
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ /**
+ * Write the contents of the JSONArray as JSON text to a writer. For
+ * compactness, no whitespace is added.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ try {
+ boolean b = false;
+ int len = length();
+
+ writer.write('[');
+
+ for (int i = 0; i < len; i += 1) {
+ if (b) {
+ writer.write(',');
+ }
+ Object v = myArrayList.get(i);
+ if (v instanceof JSONObject) {
+ ((JSONObject) v).write(writer);
+ } else if (v instanceof JSONArray) {
+ ((JSONArray) v).write(writer);
+ } else {
+ writer.write(JSONObject.valueToString(v));
+ }
+ b = true;
+ }
+ writer.write(']');
+ return writer;
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/external/json/JSONException.java b/server/src/com/vaadin/external/json/JSONException.java
new file mode 100644
index 0000000000..895ffcb457
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONException.java
@@ -0,0 +1,32 @@
+package com.vaadin.external.json;
+
+/**
+ * The JSONException is thrown by the JSON.org classes when things are amiss.
+ *
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class JSONException extends Exception {
+ private static final long serialVersionUID = 0;
+ private Throwable cause;
+
+ /**
+ * Constructs a JSONException with an explanatory message.
+ *
+ * @param message
+ * Detail about the reason for the exception.
+ */
+ public JSONException(String message) {
+ super(message);
+ }
+
+ public JSONException(Throwable cause) {
+ super(cause.getMessage());
+ this.cause = cause;
+ }
+
+ @Override
+ public Throwable getCause() {
+ return this.cause;
+ }
+}
diff --git a/server/src/com/vaadin/external/json/JSONObject.java b/server/src/com/vaadin/external/json/JSONObject.java
new file mode 100644
index 0000000000..ba772933be
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONObject.java
@@ -0,0 +1,1693 @@
+package com.vaadin.external.json;
+
+/*
+ Copyright (c) 2002 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+/**
+ * A JSONObject is an unordered collection of name/value pairs. Its external
+ * form is a string wrapped in curly braces with colons between the names and
+ * values, and commas between the values and names. The internal form is an
+ * object having <code>get</code> and <code>opt</code> methods for accessing the
+ * values by name, and <code>put</code> methods for adding or replacing values
+ * by name. The values can be any of these types: <code>Boolean</code>,
+ * <code>JSONArray</code>, <code>JSONObject</code>, <code>Number</code>,
+ * <code>String</code>, or the <code>JSONObject.NULL</code> object. A JSONObject
+ * constructor can be used to convert an external form JSON text into an
+ * internal form whose values can be retrieved with the <code>get</code> and
+ * <code>opt</code> methods, or to convert values into a JSON text using the
+ * <code>put</code> and <code>toString</code> methods. A <code>get</code> method
+ * returns a value if one can be found, and throws an exception if one cannot be
+ * found. An <code>opt</code> method returns a default value instead of throwing
+ * an exception, and so is useful for obtaining optional values.
+ * <p>
+ * The generic <code>get()</code> and <code>opt()</code> methods return an
+ * object, which you can cast or query for type. There are also typed
+ * <code>get</code> and <code>opt</code> methods that do type checking and type
+ * coercion for you. The opt methods differ from the get methods in that they do
+ * not throw. Instead, they return a specified value, such as null.
+ * <p>
+ * The <code>put</code> methods add or replace values in an object. For example,
+ *
+ * <pre>
+ * myString = new JSONObject().put(&quot;JSON&quot;, &quot;Hello, World!&quot;).toString();
+ * </pre>
+ *
+ * produces the string <code>{"JSON": "Hello, World"}</code>.
+ * <p>
+ * The texts produced by the <code>toString</code> methods strictly conform to
+ * the JSON syntax rules. The constructors are more forgiving in the texts they
+ * will accept:
+ * <ul>
+ * <li>An extra <code>,</code>&nbsp;<small>(comma)</small> may appear just
+ * before the closing brace.</li>
+ * <li>Strings may be quoted with <code>'</code>&nbsp;<small>(single
+ * quote)</small>.</li>
+ * <li>Strings do not need to be quoted at all if they do not begin with a quote
+ * or single quote, and if they do not contain leading or trailing spaces, and
+ * if they do not contain any of these characters:
+ * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers and
+ * if they are not the reserved words <code>true</code>, <code>false</code>, or
+ * <code>null</code>.</li>
+ * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as by
+ * <code>:</code>.</li>
+ * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as
+ * well as by <code>,</code> <small>(comma)</small>.</li>
+ * <li>Numbers may have the <code>0x-</code> <small>(hex)</small> prefix.</li>
+ * </ul>
+ *
+ * @author JSON.org
+ * @version 2011-10-16
+ */
+public class JSONObject implements Serializable {
+
+ /**
+ * JSONObject.NULL is equivalent to the value that JavaScript calls null,
+ * whilst Java's null is equivalent to the value that JavaScript calls
+ * undefined.
+ */
+ private static final class Null implements Serializable {
+
+ /**
+ * There is only intended to be a single instance of the NULL object, so
+ * the clone method returns itself.
+ *
+ * @return NULL.
+ */
+ @Override
+ protected final Object clone() {
+ return this;
+ }
+
+ /**
+ * A Null object is equal to the null value and to itself.
+ *
+ * @param object
+ * An object to test for nullness.
+ * @return true if the object parameter is the JSONObject.NULL object or
+ * null.
+ */
+ @Override
+ public boolean equals(Object object) {
+ return object == null || object == this;
+ }
+
+ /**
+ * Get the "null" string value.
+ *
+ * @return The string "null".
+ */
+ @Override
+ public String toString() {
+ return "null";
+ }
+ }
+
+ /**
+ * The map where the JSONObject's properties are kept.
+ */
+ private Map map;
+
+ /**
+ * It is sometimes more convenient and less ambiguous to have a
+ * <code>NULL</code> object than to use Java's <code>null</code> value.
+ * <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>.
+ * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>.
+ */
+ public static final Object NULL = new Null();
+
+ /**
+ * Construct an empty JSONObject.
+ */
+ public JSONObject() {
+ map = new HashMap();
+ }
+
+ /**
+ * Construct a JSONObject from a subset of another JSONObject. An array of
+ * strings is used to identify the keys that should be copied. Missing keys
+ * are ignored.
+ *
+ * @param jo
+ * A JSONObject.
+ * @param names
+ * An array of strings.
+ * @throws JSONException
+ * @exception JSONException
+ * If a value is a non-finite number or if a name is
+ * duplicated.
+ */
+ public JSONObject(JSONObject jo, String[] names) {
+ this();
+ for (int i = 0; i < names.length; i += 1) {
+ try {
+ putOnce(names[i], jo.opt(names[i]));
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a JSONTokener.
+ *
+ * @param x
+ * A JSONTokener object containing the source string.
+ * @throws JSONException
+ * If there is a syntax error in the source string or a
+ * duplicated key.
+ */
+ public JSONObject(JSONTokener x) throws JSONException {
+ this();
+ char c;
+ String key;
+
+ if (x.nextClean() != '{') {
+ throw x.syntaxError("A JSONObject text must begin with '{'");
+ }
+ for (;;) {
+ c = x.nextClean();
+ switch (c) {
+ case 0:
+ throw x.syntaxError("A JSONObject text must end with '}'");
+ case '}':
+ return;
+ default:
+ x.back();
+ key = x.nextValue().toString();
+ }
+
+ // The key is followed by ':'. We will also tolerate '=' or '=>'.
+
+ c = x.nextClean();
+ if (c == '=') {
+ if (x.next() != '>') {
+ x.back();
+ }
+ } else if (c != ':') {
+ throw x.syntaxError("Expected a ':' after a key");
+ }
+ putOnce(key, x.nextValue());
+
+ // Pairs are separated by ','. We will also tolerate ';'.
+
+ switch (x.nextClean()) {
+ case ';':
+ case ',':
+ if (x.nextClean() == '}') {
+ return;
+ }
+ x.back();
+ break;
+ case '}':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or '}'");
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a Map.
+ *
+ * @param map
+ * A map object that can be used to initialize the contents of
+ * the JSONObject.
+ * @throws JSONException
+ */
+ public JSONObject(Map map) {
+ this.map = new HashMap();
+ if (map != null) {
+ Iterator i = map.entrySet().iterator();
+ while (i.hasNext()) {
+ Map.Entry e = (Map.Entry) i.next();
+ Object value = e.getValue();
+ if (value != null) {
+ this.map.put(e.getKey(), wrap(value));
+ }
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from an Object using bean getters. It reflects on
+ * all of the public methods of the object. For each of the methods with no
+ * parameters and a name starting with <code>"get"</code> or
+ * <code>"is"</code> followed by an uppercase letter, the method is invoked,
+ * and a key and the value returned from the getter method are put into the
+ * new JSONObject.
+ *
+ * The key is formed by removing the <code>"get"</code> or <code>"is"</code>
+ * prefix. If the second remaining character is not upper case, then the
+ * first character is converted to lower case.
+ *
+ * For example, if an object has a method named <code>"getName"</code>, and
+ * if the result of calling <code>object.getName()</code> is
+ * <code>"Larry Fine"</code>, then the JSONObject will contain
+ * <code>"name": "Larry Fine"</code>.
+ *
+ * @param bean
+ * An object that has getter methods that should be used to make
+ * a JSONObject.
+ */
+ public JSONObject(Object bean) {
+ this();
+ populateMap(bean);
+ }
+
+ /**
+ * Construct a JSONObject from an Object, using reflection to find the
+ * public members. The resulting JSONObject's keys will be the strings from
+ * the names array, and the values will be the field values associated with
+ * those keys in the object. If a key is not found or not visible, then it
+ * will not be copied into the new JSONObject.
+ *
+ * @param object
+ * An object that has fields that should be used to make a
+ * JSONObject.
+ * @param names
+ * An array of strings, the names of the fields to be obtained
+ * from the object.
+ */
+ public JSONObject(Object object, String names[]) {
+ this();
+ Class c = object.getClass();
+ for (int i = 0; i < names.length; i += 1) {
+ String name = names[i];
+ try {
+ putOpt(name, c.getField(name).get(object));
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a source JSON text string. This is the most
+ * commonly used JSONObject constructor.
+ *
+ * @param source
+ * A string beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>
+ * &nbsp;<small>(right brace)</small>.
+ * @exception JSONException
+ * If there is a syntax error in the source string or a
+ * duplicated key.
+ */
+ public JSONObject(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+ /**
+ * Construct a JSONObject from a ResourceBundle.
+ *
+ * @param baseName
+ * The ResourceBundle base name.
+ * @param locale
+ * The Locale to load the ResourceBundle for.
+ * @throws JSONException
+ * If any JSONExceptions are detected.
+ */
+ public JSONObject(String baseName, Locale locale) throws JSONException {
+ this();
+ ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale,
+ Thread.currentThread().getContextClassLoader());
+
+ // Iterate through the keys in the bundle.
+
+ Enumeration keys = bundle.getKeys();
+ while (keys.hasMoreElements()) {
+ Object key = keys.nextElement();
+ if (key instanceof String) {
+
+ // Go through the path, ensuring that there is a nested
+ // JSONObject for each
+ // segment except the last. Add the value using the last
+ // segment's name into
+ // the deepest nested JSONObject.
+
+ String[] path = ((String) key).split("\\.");
+ int last = path.length - 1;
+ JSONObject target = this;
+ for (int i = 0; i < last; i += 1) {
+ String segment = path[i];
+ JSONObject nextTarget = target.optJSONObject(segment);
+ if (nextTarget == null) {
+ nextTarget = new JSONObject();
+ target.put(segment, nextTarget);
+ }
+ target = nextTarget;
+ }
+ target.put(path[last], bundle.getString((String) key));
+ }
+ }
+ }
+
+ /**
+ * Accumulate values under a key. It is similar to the put method except
+ * that if there is already an object stored under the key then a JSONArray
+ * is stored under the key to hold all of the accumulated values. If there
+ * is already a JSONArray, then the new value is appended to it. In
+ * contrast, the put method replaces the previous value.
+ *
+ * If only one value is accumulated that is not a JSONArray, then the result
+ * will be the same as using put. But if multiple values are accumulated,
+ * then the result will be like append.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException
+ * If the value is an invalid number or if the key is null.
+ */
+ public JSONObject accumulate(String key, Object value) throws JSONException {
+ testValidity(value);
+ Object object = opt(key);
+ if (object == null) {
+ put(key, value instanceof JSONArray ? new JSONArray().put(value)
+ : value);
+ } else if (object instanceof JSONArray) {
+ ((JSONArray) object).put(value);
+ } else {
+ put(key, new JSONArray().put(object).put(value));
+ }
+ return this;
+ }
+
+ /**
+ * Append values to the array under a key. If the key does not exist in the
+ * JSONObject, then the key is put in the JSONObject with its value being a
+ * JSONArray containing the value parameter. If the key was already
+ * associated with a JSONArray, then the value parameter is appended to it.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException
+ * If the key is null or if the current value associated with
+ * the key is not a JSONArray.
+ */
+ public JSONObject append(String key, Object value) throws JSONException {
+ testValidity(value);
+ Object object = opt(key);
+ if (object == null) {
+ put(key, new JSONArray().put(value));
+ } else if (object instanceof JSONArray) {
+ put(key, ((JSONArray) object).put(value));
+ } else {
+ throw new JSONException("JSONObject[" + key
+ + "] is not a JSONArray.");
+ }
+ return this;
+ }
+
+ /**
+ * Produce a string from a double. The string "null" will be returned if the
+ * number is not finite.
+ *
+ * @param d
+ * A double.
+ * @return A String.
+ */
+ public static String doubleToString(double d) {
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ return "null";
+ }
+
+ // Shave off trailing zeros and decimal point, if possible.
+
+ String string = Double.toString(d);
+ if (string.indexOf('.') > 0 && string.indexOf('e') < 0
+ && string.indexOf('E') < 0) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Get the value object associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return The object associated with the key.
+ * @throws JSONException
+ * if the key is not found.
+ */
+ public Object get(String key) throws JSONException {
+ if (key == null) {
+ throw new JSONException("Null key.");
+ }
+ Object object = opt(key);
+ if (object == null) {
+ throw new JSONException("JSONObject[" + quote(key) + "] not found.");
+ }
+ return object;
+ }
+
+ /**
+ * Get the boolean value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return The truth.
+ * @throws JSONException
+ * if the value is not a Boolean or the String "true" or
+ * "false".
+ */
+ public boolean getBoolean(String key) throws JSONException {
+ Object object = get(key);
+ if (object.equals(Boolean.FALSE)
+ || (object instanceof String && ((String) object)
+ .equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE)
+ || (object instanceof String && ((String) object)
+ .equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not a Boolean.");
+ }
+
+ /**
+ * Get the double value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return The numeric value.
+ * @throws JSONException
+ * if the key is not found or if the value is not a Number
+ * object and cannot be converted to a number.
+ */
+ public double getDouble(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ? ((Number) object).doubleValue()
+ : Double.parseDouble((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the int value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return The integer value.
+ * @throws JSONException
+ * if the key is not found or if the value cannot be converted
+ * to an integer.
+ */
+ public int getInt(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ? ((Number) object).intValue()
+ : Integer.parseInt((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not an int.");
+ }
+ }
+
+ /**
+ * Get the JSONArray value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return A JSONArray which is the value.
+ * @throws JSONException
+ * if the key is not found or if the value is not a JSONArray.
+ */
+ public JSONArray getJSONArray(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not a JSONArray.");
+ }
+
+ /**
+ * Get the JSONObject value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return A JSONObject which is the value.
+ * @throws JSONException
+ * if the key is not found or if the value is not a JSONObject.
+ */
+ public JSONObject getJSONObject(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not a JSONObject.");
+ }
+
+ /**
+ * Get the long value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return The long value.
+ * @throws JSONException
+ * if the key is not found or if the value cannot be converted
+ * to a long.
+ */
+ public long getLong(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ? ((Number) object).longValue()
+ : Long.parseLong((String) object);
+ } catch (Exception e) {
+ throw new JSONException("JSONObject[" + quote(key)
+ + "] is not a long.");
+ }
+ }
+
+ /**
+ * Get an array of field names from a JSONObject.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(JSONObject jo) {
+ int length = jo.length();
+ if (length == 0) {
+ return null;
+ }
+ Iterator iterator = jo.keys();
+ String[] names = new String[length];
+ int i = 0;
+ while (iterator.hasNext()) {
+ names[i] = (String) iterator.next();
+ i += 1;
+ }
+ return names;
+ }
+
+ /**
+ * Get an array of field names from an Object.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(Object object) {
+ if (object == null) {
+ return null;
+ }
+ Class klass = object.getClass();
+ Field[] fields = klass.getFields();
+ int length = fields.length;
+ if (length == 0) {
+ return null;
+ }
+ String[] names = new String[length];
+ for (int i = 0; i < length; i += 1) {
+ names[i] = fields[i].getName();
+ }
+ return names;
+ }
+
+ /**
+ * Get the string associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return A string which is the value.
+ * @throws JSONException
+ * if there is no string value for the key.
+ */
+ public String getString(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof String) {
+ return (String) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) + "] not a string.");
+ }
+
+ /**
+ * Determine if the JSONObject contains a specific key.
+ *
+ * @param key
+ * A key string.
+ * @return true if the key exists in the JSONObject.
+ */
+ public boolean has(String key) {
+ return map.containsKey(key);
+ }
+
+ /**
+ * Increment a property of a JSONObject. If there is no such property,
+ * create one with a value of 1. If there is such a property, and if it is
+ * an Integer, Long, Double, or Float, then add one to it.
+ *
+ * @param key
+ * A key string.
+ * @return this.
+ * @throws JSONException
+ * If there is already a property with this name that is not an
+ * Integer, Long, Double, or Float.
+ */
+ public JSONObject increment(String key) throws JSONException {
+ Object value = opt(key);
+ if (value == null) {
+ put(key, 1);
+ } else if (value instanceof Integer) {
+ put(key, ((Integer) value).intValue() + 1);
+ } else if (value instanceof Long) {
+ put(key, ((Long) value).longValue() + 1);
+ } else if (value instanceof Double) {
+ put(key, ((Double) value).doubleValue() + 1);
+ } else if (value instanceof Float) {
+ put(key, ((Float) value).floatValue() + 1);
+ } else {
+ throw new JSONException("Unable to increment [" + quote(key) + "].");
+ }
+ return this;
+ }
+
+ /**
+ * Determine if the value associated with the key is null or if there is no
+ * value.
+ *
+ * @param key
+ * A key string.
+ * @return true if there is no value associated with the key or if the value
+ * is the JSONObject.NULL object.
+ */
+ public boolean isNull(String key) {
+ return JSONObject.NULL.equals(opt(key));
+ }
+
+ /**
+ * Get an enumeration of the keys of the JSONObject.
+ *
+ * @return An iterator of the keys.
+ */
+ public Iterator keys() {
+ return map.keySet().iterator();
+ }
+
+ /**
+ * Get the number of keys stored in the JSONObject.
+ *
+ * @return The number of keys in the JSONObject.
+ */
+ public int length() {
+ return map.size();
+ }
+
+ /**
+ * Produce a JSONArray containing the names of the elements of this
+ * JSONObject.
+ *
+ * @return A JSONArray containing the key strings, or null if the JSONObject
+ * is empty.
+ */
+ public JSONArray names() {
+ JSONArray ja = new JSONArray();
+ Iterator keys = keys();
+ while (keys.hasNext()) {
+ ja.put(keys.next());
+ }
+ return ja.length() == 0 ? null : ja;
+ }
+
+ /**
+ * Produce a string from a Number.
+ *
+ * @param number
+ * A Number
+ * @return A String.
+ * @throws JSONException
+ * If n is a non-finite number.
+ */
+ public static String numberToString(Number number) throws JSONException {
+ if (number == null) {
+ throw new JSONException("Null pointer");
+ }
+ testValidity(number);
+
+ // Shave off trailing zeros and decimal point, if possible.
+
+ String string = number.toString();
+ if (string.indexOf('.') > 0 && string.indexOf('e') < 0
+ && string.indexOf('E') < 0) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Get an optional value associated with a key.
+ *
+ * @param key
+ * A key string.
+ * @return An object which is the value, or null if there is no value.
+ */
+ public Object opt(String key) {
+ return key == null ? null : map.get(key);
+ }
+
+ /**
+ * Get an optional boolean associated with a key. It returns false if there
+ * is no such key, or if the value is not Boolean.TRUE or the String "true".
+ *
+ * @param key
+ * A key string.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key) {
+ return optBoolean(key, false);
+ }
+
+ /**
+ * Get an optional boolean associated with a key. It returns the
+ * defaultValue if there is no such key, or if it is not a Boolean or the
+ * String "true" or "false" (case insensitive).
+ *
+ * @param key
+ * A key string.
+ * @param defaultValue
+ * The default.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key, boolean defaultValue) {
+ try {
+ return getBoolean(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional double associated with a key, or NaN if there is no such
+ * key or if its value is not a number. If the value is a string, an attempt
+ * will be made to evaluate it as a number.
+ *
+ * @param key
+ * A string which is the key.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key) {
+ return optDouble(key, Double.NaN);
+ }
+
+ /**
+ * Get an optional double associated with a key, or the defaultValue if
+ * there is no such key or if its value is not a number. If the value is a
+ * string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key
+ * A key string.
+ * @param defaultValue
+ * The default.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key, double defaultValue) {
+ try {
+ return getDouble(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional int value associated with a key, or zero if there is no
+ * such key or if the value is not a number. If the value is a string, an
+ * attempt will be made to evaluate it as a number.
+ *
+ * @param key
+ * A key string.
+ * @return An object which is the value.
+ */
+ public int optInt(String key) {
+ return optInt(key, 0);
+ }
+
+ /**
+ * Get an optional int value associated with a key, or the default if there
+ * is no such key or if the value is not a number. If the value is a string,
+ * an attempt will be made to evaluate it as a number.
+ *
+ * @param key
+ * A key string.
+ * @param defaultValue
+ * The default.
+ * @return An object which is the value.
+ */
+ public int optInt(String key, int defaultValue) {
+ try {
+ return getInt(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional JSONArray associated with a key. It returns null if there
+ * is no such key, or if its value is not a JSONArray.
+ *
+ * @param key
+ * A key string.
+ * @return A JSONArray which is the value.
+ */
+ public JSONArray optJSONArray(String key) {
+ Object o = opt(key);
+ return o instanceof JSONArray ? (JSONArray) o : null;
+ }
+
+ /**
+ * Get an optional JSONObject associated with a key. It returns null if
+ * there is no such key, or if its value is not a JSONObject.
+ *
+ * @param key
+ * A key string.
+ * @return A JSONObject which is the value.
+ */
+ public JSONObject optJSONObject(String key) {
+ Object object = opt(key);
+ return object instanceof JSONObject ? (JSONObject) object : null;
+ }
+
+ /**
+ * Get an optional long value associated with a key, or zero if there is no
+ * such key or if the value is not a number. If the value is a string, an
+ * attempt will be made to evaluate it as a number.
+ *
+ * @param key
+ * A key string.
+ * @return An object which is the value.
+ */
+ public long optLong(String key) {
+ return optLong(key, 0);
+ }
+
+ /**
+ * Get an optional long value associated with a key, or the default if there
+ * is no such key or if the value is not a number. If the value is a string,
+ * an attempt will be made to evaluate it as a number.
+ *
+ * @param key
+ * A key string.
+ * @param defaultValue
+ * The default.
+ * @return An object which is the value.
+ */
+ public long optLong(String key, long defaultValue) {
+ try {
+ return getLong(key);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional string associated with a key. It returns an empty string
+ * if there is no such key. If the value is not a string and is not null,
+ * then it is converted to a string.
+ *
+ * @param key
+ * A key string.
+ * @return A string which is the value.
+ */
+ public String optString(String key) {
+ return optString(key, "");
+ }
+
+ /**
+ * Get an optional string associated with a key. It returns the defaultValue
+ * if there is no such key.
+ *
+ * @param key
+ * A key string.
+ * @param defaultValue
+ * The default.
+ * @return A string which is the value.
+ */
+ public String optString(String key, String defaultValue) {
+ Object object = opt(key);
+ return NULL.equals(object) ? defaultValue : object.toString();
+ }
+
+ private void populateMap(Object bean) {
+ Class klass = bean.getClass();
+
+ // If klass is a System class then set includeSuperClass to false.
+
+ boolean includeSuperClass = klass.getClassLoader() != null;
+
+ Method[] methods = (includeSuperClass) ? klass.getMethods() : klass
+ .getDeclaredMethods();
+ for (int i = 0; i < methods.length; i += 1) {
+ try {
+ Method method = methods[i];
+ if (Modifier.isPublic(method.getModifiers())) {
+ String name = method.getName();
+ String key = "";
+ if (name.startsWith("get")) {
+ if (name.equals("getClass")
+ || name.equals("getDeclaringClass")) {
+ key = "";
+ } else {
+ key = name.substring(3);
+ }
+ } else if (name.startsWith("is")) {
+ key = name.substring(2);
+ }
+ if (key.length() > 0
+ && Character.isUpperCase(key.charAt(0))
+ && method.getParameterTypes().length == 0) {
+ if (key.length() == 1) {
+ key = key.toLowerCase();
+ } else if (!Character.isUpperCase(key.charAt(1))) {
+ key = key.substring(0, 1).toLowerCase()
+ + key.substring(1);
+ }
+
+ Object result = method.invoke(bean, (Object[]) null);
+ if (result != null) {
+ map.put(key, wrap(result));
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+ /**
+ * Put a key/boolean pair in the JSONObject.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * A boolean which is the value.
+ * @return this.
+ * @throws JSONException
+ * If the key is null.
+ */
+ public JSONObject put(String key, boolean value) throws JSONException {
+ put(key, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a
+ * JSONArray which is produced from a Collection.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * A Collection value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Collection value) throws JSONException {
+ put(key, new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Put a key/double pair in the JSONObject.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * A double which is the value.
+ * @return this.
+ * @throws JSONException
+ * If the key is null or if the number is invalid.
+ */
+ public JSONObject put(String key, double value) throws JSONException {
+ put(key, new Double(value));
+ return this;
+ }
+
+ /**
+ * Put a key/int pair in the JSONObject.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * An int which is the value.
+ * @return this.
+ * @throws JSONException
+ * If the key is null.
+ */
+ public JSONObject put(String key, int value) throws JSONException {
+ put(key, new Integer(value));
+ return this;
+ }
+
+ /**
+ * Put a key/long pair in the JSONObject.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * A long which is the value.
+ * @return this.
+ * @throws JSONException
+ * If the key is null.
+ */
+ public JSONObject put(String key, long value) throws JSONException {
+ put(key, new Long(value));
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a
+ * JSONObject which is produced from a Map.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * A Map value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Map value) throws JSONException {
+ put(key, new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject. If the value is null, then the
+ * key will be removed from the JSONObject if it is present.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * An object which is the value. It should be of one of these
+ * types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
+ * String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException
+ * If the value is non-finite number or if the key is null.
+ */
+ public JSONObject put(String key, Object value) throws JSONException {
+ if (key == null) {
+ throw new JSONException("Null key.");
+ }
+ if (value != null) {
+ testValidity(value);
+ map.put(key, value);
+ } else {
+ remove(key);
+ }
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the key and the value
+ * are both non-null, and only if there is not already a member with that
+ * name.
+ *
+ * @param key
+ * @param value
+ * @return his.
+ * @throws JSONException
+ * if the key is a duplicate
+ */
+ public JSONObject putOnce(String key, Object value) throws JSONException {
+ if (key != null && value != null) {
+ if (opt(key) != null) {
+ throw new JSONException("Duplicate key \"" + key + "\"");
+ }
+ put(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the key and the value
+ * are both non-null.
+ *
+ * @param key
+ * A key string.
+ * @param value
+ * An object which is the value. It should be of one of these
+ * types: Boolean, Double, Integer, JSONArray, JSONObject, Long,
+ * String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException
+ * If the value is a non-finite number.
+ */
+ public JSONObject putOpt(String key, Object value) throws JSONException {
+ if (key != null && value != null) {
+ put(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * Produce a string in double quotes with backslash sequences in all the
+ * right places. A backslash will be inserted within </, producing <\/,
+ * allowing JSON text to be delivered in HTML. In JSON text, a string cannot
+ * contain a control character or an unescaped quote or backslash.
+ *
+ * @param string
+ * A String
+ * @return A String correctly formatted for insertion in a JSON text.
+ */
+ public static String quote(String string) {
+ if (string == null || string.length() == 0) {
+ return "\"\"";
+ }
+
+ char b;
+ char c = 0;
+ String hhhh;
+ int i;
+ int len = string.length();
+ StringBuffer sb = new StringBuffer(len + 4);
+
+ sb.append('"');
+ for (i = 0; i < len; i += 1) {
+ b = c;
+ c = string.charAt(i);
+ switch (c) {
+ case '\\':
+ case '"':
+ sb.append('\\');
+ sb.append(c);
+ break;
+ case '/':
+ if (b == '<') {
+ sb.append('\\');
+ }
+ sb.append(c);
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ default:
+ if (c < ' ' || (c >= '\u0080' && c < '\u00a0')
+ || (c >= '\u2000' && c < '\u2100')) {
+ hhhh = "000" + Integer.toHexString(c);
+ sb.append("\\u" + hhhh.substring(hhhh.length() - 4));
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+ sb.append('"');
+ return sb.toString();
+ }
+
+ /**
+ * Remove a name and its value, if present.
+ *
+ * @param key
+ * The name to be removed.
+ * @return The value that was associated with the name, or null if there was
+ * no value.
+ */
+ public Object remove(String key) {
+ return map.remove(key);
+ }
+
+ /**
+ * Try to convert a string into a number, boolean, or null. If the string
+ * can't be converted, return the string.
+ *
+ * @param string
+ * A String.
+ * @return A simple JSON value.
+ */
+ public static Object stringToValue(String string) {
+ Double d;
+ if (string.equals("")) {
+ return string;
+ }
+ if (string.equalsIgnoreCase("true")) {
+ return Boolean.TRUE;
+ }
+ if (string.equalsIgnoreCase("false")) {
+ return Boolean.FALSE;
+ }
+ if (string.equalsIgnoreCase("null")) {
+ return JSONObject.NULL;
+ }
+
+ /*
+ * If it might be a number, try converting it. We support the
+ * non-standard 0x- convention. If a number cannot be produced, then the
+ * value will just be a string. Note that the 0x-, plus, and implied
+ * string conventions are non-standard. A JSON parser may accept
+ * non-JSON forms as long as it accepts all correct JSON forms.
+ */
+
+ char b = string.charAt(0);
+ if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') {
+ if (b == '0' && string.length() > 2
+ && (string.charAt(1) == 'x' || string.charAt(1) == 'X')) {
+ try {
+ return new Integer(
+ Integer.parseInt(string.substring(2), 16));
+ } catch (Exception ignore) {
+ }
+ }
+ try {
+ if (string.indexOf('.') > -1 || string.indexOf('e') > -1
+ || string.indexOf('E') > -1) {
+ d = Double.valueOf(string);
+ if (!d.isInfinite() && !d.isNaN()) {
+ return d;
+ }
+ } else {
+ Long myLong = new Long(string);
+ if (myLong.longValue() == myLong.intValue()) {
+ return new Integer(myLong.intValue());
+ } else {
+ return myLong;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Throw an exception if the object is a NaN or infinite number.
+ *
+ * @param o
+ * The object to test.
+ * @throws JSONException
+ * If o is a non-finite number.
+ */
+ public static void testValidity(Object o) throws JSONException {
+ if (o != null) {
+ if (o instanceof Double) {
+ if (((Double) o).isInfinite() || ((Double) o).isNaN()) {
+ throw new JSONException(
+ "JSON does not allow non-finite numbers.");
+ }
+ } else if (o instanceof Float) {
+ if (((Float) o).isInfinite() || ((Float) o).isNaN()) {
+ throw new JSONException(
+ "JSON does not allow non-finite numbers.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Produce a JSONArray containing the values of the members of this
+ * JSONObject.
+ *
+ * @param names
+ * A JSONArray containing a list of key strings. This determines
+ * the sequence of the values in the result.
+ * @return A JSONArray of values.
+ * @throws JSONException
+ * If any of the values are non-finite numbers.
+ */
+ public JSONArray toJSONArray(JSONArray names) throws JSONException {
+ if (names == null || names.length() == 0) {
+ return null;
+ }
+ JSONArray ja = new JSONArray();
+ for (int i = 0; i < names.length(); i += 1) {
+ ja.put(opt(names.getString(i)));
+ }
+ return ja;
+ }
+
+ /**
+ * Make a JSON text of this JSONObject. For compactness, no whitespace is
+ * added. If this would not result in a syntactically correct JSON text,
+ * then null will be returned instead.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, portable, transmittable representation
+ * of the object, beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
+ * brace)</small>.
+ */
+ @Override
+ public String toString() {
+ try {
+ Iterator keys = keys();
+ StringBuffer sb = new StringBuffer("{");
+
+ while (keys.hasNext()) {
+ if (sb.length() > 1) {
+ sb.append(',');
+ }
+ Object o = keys.next();
+ sb.append(quote(o.toString()));
+ sb.append(':');
+ sb.append(valueToString(map.get(o)));
+ }
+ sb.append('}');
+ return sb.toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONObject.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param indentFactor
+ * The number of spaces to add to each level of indentation.
+ * @return a printable, displayable, portable, transmittable representation
+ * of the object, beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
+ * brace)</small>.
+ * @throws JSONException
+ * If the object contains an invalid number.
+ */
+ public String toString(int indentFactor) throws JSONException {
+ return toString(indentFactor, 0);
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONObject.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param indentFactor
+ * The number of spaces to add to each level of indentation.
+ * @param indent
+ * The indentation of the top level.
+ * @return a printable, displayable, transmittable representation of the
+ * object, beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
+ * brace)</small>.
+ * @throws JSONException
+ * If the object contains an invalid number.
+ */
+ String toString(int indentFactor, int indent) throws JSONException {
+ int i;
+ int length = length();
+ if (length == 0) {
+ return "{}";
+ }
+ Iterator keys = keys();
+ int newindent = indent + indentFactor;
+ Object object;
+ StringBuffer sb = new StringBuffer("{");
+ if (length == 1) {
+ object = keys.next();
+ sb.append(quote(object.toString()));
+ sb.append(": ");
+ sb.append(valueToString(map.get(object), indentFactor, indent));
+ } else {
+ while (keys.hasNext()) {
+ object = keys.next();
+ if (sb.length() > 1) {
+ sb.append(",\n");
+ } else {
+ sb.append('\n');
+ }
+ for (i = 0; i < newindent; i += 1) {
+ sb.append(' ');
+ }
+ sb.append(quote(object.toString()));
+ sb.append(": ");
+ sb.append(valueToString(map.get(object), indentFactor,
+ newindent));
+ }
+ if (sb.length() > 1) {
+ sb.append('\n');
+ for (i = 0; i < indent; i += 1) {
+ sb.append(' ');
+ }
+ }
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Make a JSON text of an Object value. If the object has an
+ * value.toJSONString() method, then that method will be used to produce the
+ * JSON text. The method is required to produce a strictly conforming text.
+ * If the object does not contain a toJSONString method (which is the most
+ * common case), then a text will be produced by other means. If the value
+ * is an array or Collection, then a JSONArray will be made from it and its
+ * toJSONString method will be called. If the value is a MAP, then a
+ * JSONObject will be made from it and its toJSONString method will be
+ * called. Otherwise, the value's toString method will be called, and the
+ * result will be quoted.
+ *
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param value
+ * The value to be serialized.
+ * @return a printable, displayable, transmittable representation of the
+ * object, beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
+ * brace)</small>.
+ * @throws JSONException
+ * If the value is or contains an invalid number.
+ */
+ public static String valueToString(Object value) throws JSONException {
+ if (value == null || value.equals(null)) {
+ return "null";
+ }
+ if (value instanceof JSONString) {
+ Object object;
+ try {
+ object = ((JSONString) value).toJSONString();
+ } catch (Exception e) {
+ throw new JSONException(e);
+ }
+ if (object instanceof String) {
+ return (String) object;
+ }
+ throw new JSONException("Bad value from toJSONString: " + object);
+ }
+ if (value instanceof Number) {
+ return numberToString((Number) value);
+ }
+ if (value instanceof Boolean || value instanceof JSONObject
+ || value instanceof JSONArray) {
+ return value.toString();
+ }
+ if (value instanceof Map) {
+ return new JSONObject((Map) value).toString();
+ }
+ if (value instanceof Collection) {
+ return new JSONArray((Collection) value).toString();
+ }
+ if (value.getClass().isArray()) {
+ return new JSONArray(value).toString();
+ }
+ return quote(value.toString());
+ }
+
+ /**
+ * Make a prettyprinted JSON text of an object value.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param value
+ * The value to be serialized.
+ * @param indentFactor
+ * The number of spaces to add to each level of indentation.
+ * @param indent
+ * The indentation of the top level.
+ * @return a printable, displayable, transmittable representation of the
+ * object, beginning with <code>{</code>&nbsp;<small>(left
+ * brace)</small> and ending with <code>}</code>&nbsp;<small>(right
+ * brace)</small>.
+ * @throws JSONException
+ * If the object contains an invalid number.
+ */
+ static String valueToString(Object value, int indentFactor, int indent)
+ throws JSONException {
+ if (value == null || value.equals(null)) {
+ return "null";
+ }
+ try {
+ if (value instanceof JSONString) {
+ Object o = ((JSONString) value).toJSONString();
+ if (o instanceof String) {
+ return (String) o;
+ }
+ }
+ } catch (Exception ignore) {
+ }
+ if (value instanceof Number) {
+ return numberToString((Number) value);
+ }
+ if (value instanceof Boolean) {
+ return value.toString();
+ }
+ if (value instanceof JSONObject) {
+ return ((JSONObject) value).toString(indentFactor, indent);
+ }
+ if (value instanceof JSONArray) {
+ return ((JSONArray) value).toString(indentFactor, indent);
+ }
+ if (value instanceof Map) {
+ return new JSONObject((Map) value).toString(indentFactor, indent);
+ }
+ if (value instanceof Collection) {
+ return new JSONArray((Collection) value).toString(indentFactor,
+ indent);
+ }
+ if (value.getClass().isArray()) {
+ return new JSONArray(value).toString(indentFactor, indent);
+ }
+ return quote(value.toString());
+ }
+
+ /**
+ * Wrap an object, if necessary. If the object is null, return the NULL
+ * object. If it is an array or collection, wrap it in a JSONArray. If it is
+ * a map, wrap it in a JSONObject. If it is a standard property (Double,
+ * String, et al) then it is already wrapped. Otherwise, if it comes from
+ * one of the java packages, turn it into a string. And if it doesn't, try
+ * to wrap it in a JSONObject. If the wrapping fails, then null is returned.
+ *
+ * @param object
+ * The object to wrap
+ * @return The wrapped value
+ */
+ public static Object wrap(Object object) {
+ try {
+ if (object == null) {
+ return NULL;
+ }
+ if (object instanceof JSONObject || object instanceof JSONArray
+ || NULL.equals(object) || object instanceof JSONString
+ || object instanceof Byte || object instanceof Character
+ || object instanceof Short || object instanceof Integer
+ || object instanceof Long || object instanceof Boolean
+ || object instanceof Float || object instanceof Double
+ || object instanceof String) {
+ return object;
+ }
+
+ if (object instanceof Collection) {
+ return new JSONArray((Collection) object);
+ }
+ if (object.getClass().isArray()) {
+ return new JSONArray(object);
+ }
+ if (object instanceof Map) {
+ return new JSONObject((Map) object);
+ }
+ Package objectPackage = object.getClass().getPackage();
+ String objectPackageName = objectPackage != null ? objectPackage
+ .getName() : "";
+ if (objectPackageName.startsWith("java.")
+ || objectPackageName.startsWith("javax.")
+ || object.getClass().getClassLoader() == null) {
+ return object.toString();
+ }
+ return new JSONObject(object);
+ } catch (Exception exception) {
+ return null;
+ }
+ }
+
+ /**
+ * Write the contents of the JSONObject as JSON text to a writer. For
+ * compactness, no whitespace is added.
+ * <p>
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ try {
+ boolean commanate = false;
+ Iterator keys = keys();
+ writer.write('{');
+
+ while (keys.hasNext()) {
+ if (commanate) {
+ writer.write(',');
+ }
+ Object key = keys.next();
+ writer.write(quote(key.toString()));
+ writer.write(':');
+ Object value = map.get(key);
+ if (value instanceof JSONObject) {
+ ((JSONObject) value).write(writer);
+ } else if (value instanceof JSONArray) {
+ ((JSONArray) value).write(writer);
+ } else {
+ writer.write(valueToString(value));
+ }
+ commanate = true;
+ }
+ writer.write('}');
+ return writer;
+ } catch (IOException exception) {
+ throw new JSONException(exception);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/external/json/JSONString.java b/server/src/com/vaadin/external/json/JSONString.java
new file mode 100644
index 0000000000..cc7e4d8c07
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONString.java
@@ -0,0 +1,21 @@
+package com.vaadin.external.json;
+
+import java.io.Serializable;
+
+/**
+ * The <code>JSONString</code> interface allows a <code>toJSONString()</code>
+ * method so that a class can change the behavior of
+ * <code>JSONObject.toString()</code>, <code>JSONArray.toString()</code>, and
+ * <code>JSONWriter.value(</code>Object<code>)</code>. The
+ * <code>toJSONString</code> method will be used instead of the default behavior
+ * of using the Object's <code>toString()</code> method and quoting the result.
+ */
+public interface JSONString extends Serializable {
+ /**
+ * The <code>toJSONString</code> method allows a class to produce its own
+ * JSON serialization.
+ *
+ * @return A strictly syntactically correct JSON text.
+ */
+ public String toJSONString();
+}
diff --git a/server/src/com/vaadin/external/json/JSONStringer.java b/server/src/com/vaadin/external/json/JSONStringer.java
new file mode 100644
index 0000000000..ae905cb15f
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONStringer.java
@@ -0,0 +1,84 @@
+package com.vaadin.external.json;
+
+/*
+ Copyright (c) 2006 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+import java.io.StringWriter;
+
+/**
+ * JSONStringer provides a quick and convenient way of producing JSON text. The
+ * texts produced strictly conform to JSON syntax rules. No whitespace is added,
+ * so the results are ready for transmission or storage. Each instance of
+ * JSONStringer can produce one JSON text.
+ * <p>
+ * A JSONStringer instance provides a <code>value</code> method for appending
+ * values to the text, and a <code>key</code> method for adding keys before
+ * values in objects. There are <code>array</code> and <code>endArray</code>
+ * methods that make and bound array values, and <code>object</code> and
+ * <code>endObject</code> methods which make and bound object values. All of
+ * these methods return the JSONWriter instance, permitting cascade style. For
+ * example,
+ *
+ * <pre>
+ * myString = new JSONStringer().object().key(&quot;JSON&quot;).value(&quot;Hello, World!&quot;)
+ * .endObject().toString();
+ * </pre>
+ *
+ * which produces the string
+ *
+ * <pre>
+ * {"JSON":"Hello, World!"}
+ * </pre>
+ * <p>
+ * The first method called must be <code>array</code> or <code>object</code>.
+ * There are no methods for adding commas or colons. JSONStringer adds them for
+ * you. Objects and arrays can be nested up to 20 levels deep.
+ * <p>
+ * This can sometimes be easier than using a JSONObject to build a string.
+ *
+ * @author JSON.org
+ * @version 2008-09-18
+ */
+public class JSONStringer extends JSONWriter {
+ /**
+ * Make a fresh JSONStringer. It can be used to build one JSON text.
+ */
+ public JSONStringer() {
+ super(new StringWriter());
+ }
+
+ /**
+ * Return the JSON text. This method is used to obtain the product of the
+ * JSONStringer instance. It will return <code>null</code> if there was a
+ * problem in the construction of the JSON text (such as the calls to
+ * <code>array</code> were not properly balanced with calls to
+ * <code>endArray</code>).
+ *
+ * @return The JSON text.
+ */
+ @Override
+ public String toString() {
+ return this.mode == 'd' ? this.writer.toString() : null;
+ }
+}
diff --git a/server/src/com/vaadin/external/json/JSONTokener.java b/server/src/com/vaadin/external/json/JSONTokener.java
new file mode 100644
index 0000000000..c3531cae1d
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONTokener.java
@@ -0,0 +1,451 @@
+package com.vaadin.external.json;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+
+/*
+ Copyright (c) 2002 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+/**
+ * A JSONTokener takes a source string and extracts characters and tokens from
+ * it. It is used by the JSONObject and JSONArray constructors to parse JSON
+ * source strings.
+ *
+ * @author JSON.org
+ * @version 2010-12-24
+ */
+public class JSONTokener implements Serializable {
+
+ private int character;
+ private boolean eof;
+ private int index;
+ private int line;
+ private char previous;
+ private Reader reader;
+ private boolean usePrevious;
+
+ /**
+ * Construct a JSONTokener from a Reader.
+ *
+ * @param reader
+ * A reader.
+ */
+ public JSONTokener(Reader reader) {
+ this.reader = reader.markSupported() ? reader : new BufferedReader(
+ reader);
+ eof = false;
+ usePrevious = false;
+ previous = 0;
+ index = 0;
+ character = 1;
+ line = 1;
+ }
+
+ /**
+ * Construct a JSONTokener from an InputStream.
+ */
+ public JSONTokener(InputStream inputStream) throws JSONException {
+ this(new InputStreamReader(inputStream));
+ }
+
+ /**
+ * Construct a JSONTokener from a string.
+ *
+ * @param s
+ * A source string.
+ */
+ public JSONTokener(String s) {
+ this(new StringReader(s));
+ }
+
+ /**
+ * Back up one character. This provides a sort of lookahead capability, so
+ * that you can test for a digit or letter before attempting to parse the
+ * next number or identifier.
+ */
+ public void back() throws JSONException {
+ if (usePrevious || index <= 0) {
+ throw new JSONException("Stepping back two steps is not supported");
+ }
+ index -= 1;
+ character -= 1;
+ usePrevious = true;
+ eof = false;
+ }
+
+ /**
+ * Get the hex value of a character (base16).
+ *
+ * @param c
+ * A character between '0' and '9' or between 'A' and 'F' or
+ * between 'a' and 'f'.
+ * @return An int between 0 and 15, or -1 if c was not a hex digit.
+ */
+ public static int dehexchar(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - ('A' - 10);
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - ('a' - 10);
+ }
+ return -1;
+ }
+
+ public boolean end() {
+ return eof && !usePrevious;
+ }
+
+ /**
+ * Determine if the source string still contains characters that next() can
+ * consume.
+ *
+ * @return true if not yet at the end of the source.
+ */
+ public boolean more() throws JSONException {
+ next();
+ if (end()) {
+ return false;
+ }
+ back();
+ return true;
+ }
+
+ /**
+ * Get the next character in the source string.
+ *
+ * @return The next character, or 0 if past the end of the source string.
+ */
+ public char next() throws JSONException {
+ int c;
+ if (usePrevious) {
+ usePrevious = false;
+ c = previous;
+ } else {
+ try {
+ c = reader.read();
+ } catch (IOException exception) {
+ throw new JSONException(exception);
+ }
+
+ if (c <= 0) { // End of stream
+ eof = true;
+ c = 0;
+ }
+ }
+ index += 1;
+ if (previous == '\r') {
+ line += 1;
+ character = c == '\n' ? 0 : 1;
+ } else if (c == '\n') {
+ line += 1;
+ character = 0;
+ } else {
+ character += 1;
+ }
+ previous = (char) c;
+ return previous;
+ }
+
+ /**
+ * Consume the next character, and check that it matches a specified
+ * character.
+ *
+ * @param c
+ * The character to match.
+ * @return The character.
+ * @throws JSONException
+ * if the character does not match.
+ */
+ public char next(char c) throws JSONException {
+ char n = next();
+ if (n != c) {
+ throw syntaxError("Expected '" + c + "' and instead saw '" + n
+ + "'");
+ }
+ return n;
+ }
+
+ /**
+ * Get the next n characters.
+ *
+ * @param n
+ * The number of characters to take.
+ * @return A string of n characters.
+ * @throws JSONException
+ * Substring bounds error if there are not n characters
+ * remaining in the source string.
+ */
+ public String next(int n) throws JSONException {
+ if (n == 0) {
+ return "";
+ }
+
+ char[] chars = new char[n];
+ int pos = 0;
+
+ while (pos < n) {
+ chars[pos] = next();
+ if (end()) {
+ throw syntaxError("Substring bounds error");
+ }
+ pos += 1;
+ }
+ return new String(chars);
+ }
+
+ /**
+ * Get the next char in the string, skipping whitespace.
+ *
+ * @throws JSONException
+ * @return A character, or 0 if there are no more characters.
+ */
+ public char nextClean() throws JSONException {
+ for (;;) {
+ char c = next();
+ if (c == 0 || c > ' ') {
+ return c;
+ }
+ }
+ }
+
+ /**
+ * Return the characters up to the next close quote character. Backslash
+ * processing is done. The formal JSON format does not allow strings in
+ * single quotes, but an implementation is allowed to accept them.
+ *
+ * @param quote
+ * The quoting character, either <code>"</code>
+ * &nbsp;<small>(double quote)</small> or <code>'</code>
+ * &nbsp;<small>(single quote)</small>.
+ * @return A String.
+ * @throws JSONException
+ * Unterminated string.
+ */
+ public String nextString(char quote) throws JSONException {
+ char c;
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ c = next();
+ switch (c) {
+ case 0:
+ case '\n':
+ case '\r':
+ throw syntaxError("Unterminated string");
+ case '\\':
+ c = next();
+ switch (c) {
+ case 'b':
+ sb.append('\b');
+ break;
+ case 't':
+ sb.append('\t');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 'u':
+ sb.append((char) Integer.parseInt(next(4), 16));
+ break;
+ case '"':
+ case '\'':
+ case '\\':
+ case '/':
+ sb.append(c);
+ break;
+ default:
+ throw syntaxError("Illegal escape.");
+ }
+ break;
+ default:
+ if (c == quote) {
+ return sb.toString();
+ }
+ sb.append(c);
+ }
+ }
+ }
+
+ /**
+ * Get the text up but not including the specified character or the end of
+ * line, whichever comes first.
+ *
+ * @param delimiter
+ * A delimiter character.
+ * @return A string.
+ */
+ public String nextTo(char delimiter) throws JSONException {
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ char c = next();
+ if (c == delimiter || c == 0 || c == '\n' || c == '\r') {
+ if (c != 0) {
+ back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+ /**
+ * Get the text up but not including one of the specified delimiter
+ * characters or the end of line, whichever comes first.
+ *
+ * @param delimiters
+ * A set of delimiter characters.
+ * @return A string, trimmed.
+ */
+ public String nextTo(String delimiters) throws JSONException {
+ char c;
+ StringBuffer sb = new StringBuffer();
+ for (;;) {
+ c = next();
+ if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') {
+ if (c != 0) {
+ back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+ /**
+ * Get the next value. The value can be a Boolean, Double, Integer,
+ * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
+ *
+ * @throws JSONException
+ * If syntax error.
+ *
+ * @return An object.
+ */
+ public Object nextValue() throws JSONException {
+ char c = nextClean();
+ String string;
+
+ switch (c) {
+ case '"':
+ case '\'':
+ return nextString(c);
+ case '{':
+ back();
+ return new JSONObject(this);
+ case '[':
+ back();
+ return new JSONArray(this);
+ }
+
+ /*
+ * Handle unquoted text. This could be the values true, false, or null,
+ * or it can be a number. An implementation (such as this one) is
+ * allowed to also accept non-standard forms.
+ *
+ * Accumulate characters until we reach the end of the text or a
+ * formatting character.
+ */
+
+ StringBuffer sb = new StringBuffer();
+ while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
+ sb.append(c);
+ c = next();
+ }
+ back();
+
+ string = sb.toString().trim();
+ if (string.equals("")) {
+ throw syntaxError("Missing value");
+ }
+ return JSONObject.stringToValue(string);
+ }
+
+ /**
+ * Skip characters until the next character is the requested character. If
+ * the requested character is not found, no characters are skipped.
+ *
+ * @param to
+ * A character to skip to.
+ * @return The requested character, or zero if the requested character is
+ * not found.
+ */
+ public char skipTo(char to) throws JSONException {
+ char c;
+ try {
+ int startIndex = index;
+ int startCharacter = character;
+ int startLine = line;
+ reader.mark(Integer.MAX_VALUE);
+ do {
+ c = next();
+ if (c == 0) {
+ reader.reset();
+ index = startIndex;
+ character = startCharacter;
+ line = startLine;
+ return c;
+ }
+ } while (c != to);
+ } catch (IOException exc) {
+ throw new JSONException(exc);
+ }
+
+ back();
+ return c;
+ }
+
+ /**
+ * Make a JSONException to signal a syntax error.
+ *
+ * @param message
+ * The error message.
+ * @return A JSONException object, suitable for throwing
+ */
+ public JSONException syntaxError(String message) {
+ return new JSONException(message + toString());
+ }
+
+ /**
+ * Make a printable string of this JSONTokener.
+ *
+ * @return " at {index} [character {character} line {line}]"
+ */
+ @Override
+ public String toString() {
+ return " at " + index + " [character " + character + " line " + line
+ + "]";
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/external/json/JSONWriter.java b/server/src/com/vaadin/external/json/JSONWriter.java
new file mode 100644
index 0000000000..5f9ddeeae2
--- /dev/null
+++ b/server/src/com/vaadin/external/json/JSONWriter.java
@@ -0,0 +1,355 @@
+package com.vaadin.external.json;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.Writer;
+
+/*
+ Copyright (c) 2006 JSON.org
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ The Software shall be used for Good, not Evil.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+/**
+ * JSONWriter provides a quick and convenient way of producing JSON text. The
+ * texts produced strictly conform to JSON syntax rules. No whitespace is added,
+ * so the results are ready for transmission or storage. Each instance of
+ * JSONWriter can produce one JSON text.
+ * <p>
+ * A JSONWriter instance provides a <code>value</code> method for appending
+ * values to the text, and a <code>key</code> method for adding keys before
+ * values in objects. There are <code>array</code> and <code>endArray</code>
+ * methods that make and bound array values, and <code>object</code> and
+ * <code>endObject</code> methods which make and bound object values. All of
+ * these methods return the JSONWriter instance, permitting a cascade style. For
+ * example,
+ *
+ * <pre>
+ * new JSONWriter(myWriter).object().key(&quot;JSON&quot;).value(&quot;Hello, World!&quot;)
+ * .endObject();
+ * </pre>
+ *
+ * which writes
+ *
+ * <pre>
+ * {"JSON":"Hello, World!"}
+ * </pre>
+ * <p>
+ * The first method called must be <code>array</code> or <code>object</code>.
+ * There are no methods for adding commas or colons. JSONWriter adds them for
+ * you. Objects and arrays can be nested up to 20 levels deep.
+ * <p>
+ * This can sometimes be easier than using a JSONObject to build a string.
+ *
+ * @author JSON.org
+ * @version 2011-11-14
+ */
+public class JSONWriter implements Serializable {
+ private static final int maxdepth = 200;
+
+ /**
+ * The comma flag determines if a comma should be output before the next
+ * value.
+ */
+ private boolean comma;
+
+ /**
+ * The current mode. Values: 'a' (array), 'd' (done), 'i' (initial), 'k'
+ * (key), 'o' (object).
+ */
+ protected char mode;
+
+ /**
+ * The object/array stack.
+ */
+ private final JSONObject stack[];
+
+ /**
+ * The stack top index. A value of 0 indicates that the stack is empty.
+ */
+ private int top;
+
+ /**
+ * The writer that will receive the output.
+ */
+ protected Writer writer;
+
+ /**
+ * Make a fresh JSONWriter. It can be used to build one JSON text.
+ */
+ public JSONWriter(Writer w) {
+ comma = false;
+ mode = 'i';
+ stack = new JSONObject[maxdepth];
+ top = 0;
+ writer = w;
+ }
+
+ /**
+ * Append a value.
+ *
+ * @param string
+ * A string value.
+ * @return this
+ * @throws JSONException
+ * If the value is out of sequence.
+ */
+ private JSONWriter append(String string) throws JSONException {
+ if (string == null) {
+ throw new JSONException("Null pointer");
+ }
+ if (mode == 'o' || mode == 'a') {
+ try {
+ if (comma && mode == 'a') {
+ writer.write(',');
+ }
+ writer.write(string);
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ if (mode == 'o') {
+ mode = 'k';
+ }
+ comma = true;
+ return this;
+ }
+ throw new JSONException("Value out of sequence.");
+ }
+
+ /**
+ * Begin appending a new array. All values until the balancing
+ * <code>endArray</code> will be appended to this array. The
+ * <code>endArray</code> method must be called to mark the array's end.
+ *
+ * @return this
+ * @throws JSONException
+ * If the nesting is too deep, or if the object is started in
+ * the wrong place (for example as a key or after the end of the
+ * outermost array or object).
+ */
+ public JSONWriter array() throws JSONException {
+ if (mode == 'i' || mode == 'o' || mode == 'a') {
+ push(null);
+ append("[");
+ comma = false;
+ return this;
+ }
+ throw new JSONException("Misplaced array.");
+ }
+
+ /**
+ * End something.
+ *
+ * @param mode
+ * Mode
+ * @param c
+ * Closing character
+ * @return this
+ * @throws JSONException
+ * If unbalanced.
+ */
+ private JSONWriter end(char mode, char c) throws JSONException {
+ if (this.mode != mode) {
+ throw new JSONException(mode == 'a' ? "Misplaced endArray."
+ : "Misplaced endObject.");
+ }
+ pop(mode);
+ try {
+ writer.write(c);
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ comma = true;
+ return this;
+ }
+
+ /**
+ * End an array. This method most be called to balance calls to
+ * <code>array</code>.
+ *
+ * @return this
+ * @throws JSONException
+ * If incorrectly nested.
+ */
+ public JSONWriter endArray() throws JSONException {
+ return end('a', ']');
+ }
+
+ /**
+ * End an object. This method most be called to balance calls to
+ * <code>object</code>.
+ *
+ * @return this
+ * @throws JSONException
+ * If incorrectly nested.
+ */
+ public JSONWriter endObject() throws JSONException {
+ return end('k', '}');
+ }
+
+ /**
+ * Append a key. The key will be associated with the next value. In an
+ * object, every value must be preceded by a key.
+ *
+ * @param string
+ * A key string.
+ * @return this
+ * @throws JSONException
+ * If the key is out of place. For example, keys do not belong
+ * in arrays or if the key is null.
+ */
+ public JSONWriter key(String string) throws JSONException {
+ if (string == null) {
+ throw new JSONException("Null key.");
+ }
+ if (mode == 'k') {
+ try {
+ stack[top - 1].putOnce(string, Boolean.TRUE);
+ if (comma) {
+ writer.write(',');
+ }
+ writer.write(JSONObject.quote(string));
+ writer.write(':');
+ comma = false;
+ mode = 'o';
+ return this;
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ }
+ throw new JSONException("Misplaced key.");
+ }
+
+ /**
+ * Begin appending a new object. All keys and values until the balancing
+ * <code>endObject</code> will be appended to this object. The
+ * <code>endObject</code> method must be called to mark the object's end.
+ *
+ * @return this
+ * @throws JSONException
+ * If the nesting is too deep, or if the object is started in
+ * the wrong place (for example as a key or after the end of the
+ * outermost array or object).
+ */
+ public JSONWriter object() throws JSONException {
+ if (mode == 'i') {
+ mode = 'o';
+ }
+ if (mode == 'o' || mode == 'a') {
+ append("{");
+ push(new JSONObject());
+ comma = false;
+ return this;
+ }
+ throw new JSONException("Misplaced object.");
+
+ }
+
+ /**
+ * Pop an array or object scope.
+ *
+ * @param c
+ * The scope to close.
+ * @throws JSONException
+ * If nesting is wrong.
+ */
+ private void pop(char c) throws JSONException {
+ if (top <= 0) {
+ throw new JSONException("Nesting error.");
+ }
+ char m = stack[top - 1] == null ? 'a' : 'k';
+ if (m != c) {
+ throw new JSONException("Nesting error.");
+ }
+ top -= 1;
+ mode = top == 0 ? 'd' : stack[top - 1] == null ? 'a' : 'k';
+ }
+
+ /**
+ * Push an array or object scope.
+ *
+ * @param c
+ * The scope to open.
+ * @throws JSONException
+ * If nesting is too deep.
+ */
+ private void push(JSONObject jo) throws JSONException {
+ if (top >= maxdepth) {
+ throw new JSONException("Nesting too deep.");
+ }
+ stack[top] = jo;
+ mode = jo == null ? 'a' : 'k';
+ top += 1;
+ }
+
+ /**
+ * Append either the value <code>true</code> or the value <code>false</code>
+ * .
+ *
+ * @param b
+ * A boolean.
+ * @return this
+ * @throws JSONException
+ */
+ public JSONWriter value(boolean b) throws JSONException {
+ return append(b ? "true" : "false");
+ }
+
+ /**
+ * Append a double value.
+ *
+ * @param d
+ * A double.
+ * @return this
+ * @throws JSONException
+ * If the number is not finite.
+ */
+ public JSONWriter value(double d) throws JSONException {
+ return this.value(new Double(d));
+ }
+
+ /**
+ * Append a long value.
+ *
+ * @param l
+ * A long.
+ * @return this
+ * @throws JSONException
+ */
+ public JSONWriter value(long l) throws JSONException {
+ return append(Long.toString(l));
+ }
+
+ /**
+ * Append an object value.
+ *
+ * @param object
+ * The object to append. It can be null, or a Boolean, Number,
+ * String, JSONObject, or JSONArray, or an object that implements
+ * JSONString.
+ * @return this
+ * @throws JSONException
+ * If the value is out of sequence.
+ */
+ public JSONWriter value(Object object) throws JSONException {
+ return append(JSONObject.valueToString(object));
+ }
+}
diff --git a/server/src/com/vaadin/external/json/README b/server/src/com/vaadin/external/json/README
new file mode 100644
index 0000000000..ca6dc11764
--- /dev/null
+++ b/server/src/com/vaadin/external/json/README
@@ -0,0 +1,68 @@
+JSON in Java [package org.json]
+
+Douglas Crockford
+douglas@crockford.com
+
+2011-02-02
+
+
+JSON is a light-weight, language independent, data interchange format.
+See http://www.JSON.org/
+
+The files in this package implement JSON encoders/decoders in Java.
+It also includes the capability to convert between JSON and XML, HTTP
+headers, Cookies, and CDL.
+
+This is a reference implementation. There is a large number of JSON packages
+in Java. Perhaps someday the Java community will standardize on one. Until
+then, choose carefully.
+
+The license includes this restriction: "The software shall be used for good,
+not evil." If your conscience cannot live with that, then choose a different
+package.
+
+The package compiles on Java 1.2 thru Java 1.4.
+
+
+JSONObject.java: The JSONObject can parse text from a String or a JSONTokener
+to produce a map-like object. The object provides methods for manipulating its
+contents, and for producing a JSON compliant object serialization.
+
+JSONArray.java: The JSONObject can parse text from a String or a JSONTokener
+to produce a vector-like object. The object provides methods for manipulating
+its contents, and for producing a JSON compliant array serialization.
+
+JSONTokener.java: The JSONTokener breaks a text into a sequence of individual
+tokens. It can be constructed from a String, Reader, or InputStream.
+
+JSONException.java: The JSONException is the standard exception type thrown
+by this package.
+
+
+JSONString.java: The JSONString interface requires a toJSONString method,
+allowing an object to provide its own serialization.
+
+JSONStringer.java: The JSONStringer provides a convenient facility for
+building JSON strings.
+
+JSONWriter.java: The JSONWriter provides a convenient facility for building
+JSON text through a writer.
+
+
+CDL.java: CDL provides support for converting between JSON and comma
+delimited lists.
+
+Cookie.java: Cookie provides support for converting between JSON and cookies.
+
+CookieList.java: CookieList provides support for converting between JSON and
+cookie lists.
+
+HTTP.java: HTTP provides support for converting between JSON and HTTP headers.
+
+HTTPTokener.java: HTTPTokener extends JSONTokener for parsing HTTP headers.
+
+XML.java: XML provides support for converting between JSON and XML.
+
+JSONML.java: JSONML provides support for converting between JSONML and XML.
+
+XMLTokener.java: XMLTokener extends JSONTokener for parsing XML text. \ No newline at end of file
diff --git a/server/src/com/vaadin/navigator/FragmentManager.java b/server/src/com/vaadin/navigator/FragmentManager.java
new file mode 100644
index 0000000000..f1fd90e569
--- /dev/null
+++ b/server/src/com/vaadin/navigator/FragmentManager.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * Fragment manager that handles interaction between Navigator and URI fragments
+ * or other similar view identification and bookmarking system.
+ *
+ * Alternative implementations can be created for HTML5 pushState, for portlet
+ * URL navigation and other similar systems.
+ *
+ * This interface is mostly for internal use by {@link Navigator}.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface FragmentManager extends Serializable {
+ /**
+ * Return the current fragment (location string) including view name and any
+ * optional parameters.
+ *
+ * @return current view and parameter string, not null
+ */
+ public String getFragment();
+
+ /**
+ * Set the current fragment (location string) in the application URL or
+ * similar location, including view name and any optional parameters.
+ *
+ * @param fragment
+ * new view and parameter string, not null
+ */
+ public void setFragment(String fragment);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/navigator/Navigator.java b/server/src/com/vaadin/navigator/Navigator.java
new file mode 100644
index 0000000000..1813301fe6
--- /dev/null
+++ b/server/src/com/vaadin/navigator/Navigator.java
@@ -0,0 +1,656 @@
+package com.vaadin.navigator;
+
+/*
+ @VaadinApache2LicenseForJavaFiles@
+ */
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
+import com.vaadin.terminal.Page;
+import com.vaadin.terminal.Page.FragmentChangedEvent;
+import com.vaadin.terminal.Page.FragmentChangedListener;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.CustomComponent;
+
+/**
+ * Navigator utility that allows switching of views in a part of an application.
+ *
+ * The view switching can be based e.g. on URI fragments containing the view
+ * name and parameters to the view. There are two types of parameters for views:
+ * an optional parameter string that is included in the fragment (may be
+ * bookmarkable).
+ *
+ * Views can be explicitly registered or dynamically generated and listening to
+ * view changes is possible.
+ *
+ * Note that {@link Navigator} is not a component itself but comes with
+ * {@link SimpleViewDisplay} which is a component that displays the selected
+ * view as its contents.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public class Navigator implements Serializable {
+
+ // TODO divert navigation e.g. if no permissions? Or just show another view
+ // but keep URL? how best to intercept
+ // TODO investigate relationship with TouchKit navigation support
+
+ /**
+ * Empty view component.
+ */
+ public static class EmptyView extends CssLayout implements View {
+ /**
+ * Create minimally sized empty view.
+ */
+ public EmptyView() {
+ setWidth("0px");
+ setHeight("0px");
+ }
+
+ @Override
+ public void navigateTo(String fragmentParameters) {
+ // nothing to do
+ }
+ }
+
+ /**
+ * Fragment manager using URI fragments of a Page to track views and enable
+ * listening to view changes.
+ *
+ * This class is mostly for internal use by Navigator, and is only public
+ * and static to enable testing.
+ */
+ public static class UriFragmentManager implements FragmentManager,
+ FragmentChangedListener {
+ private final Page page;
+ private final Navigator navigator;
+
+ /**
+ * Create a new URIFragmentManager and attach it to listen to URI
+ * fragment changes of a {@link Page}.
+ *
+ * @param page
+ * page whose URI fragment to get and modify
+ * @param navigator
+ * {@link Navigator} to notify of fragment changes (using
+ * {@link Navigator#navigateTo(String)}
+ */
+ public UriFragmentManager(Page page, Navigator navigator) {
+ this.page = page;
+ this.navigator = navigator;
+
+ page.addListener(this);
+ }
+
+ @Override
+ public String getFragment() {
+ return page.getFragment();
+ }
+
+ @Override
+ public void setFragment(String fragment) {
+ page.setFragment(fragment, false);
+ }
+
+ @Override
+ public void fragmentChanged(FragmentChangedEvent event) {
+ UriFragmentManager.this.navigator.navigateTo(getFragment());
+ }
+ }
+
+ /**
+ * View display that is a component itself and replaces its contents with
+ * the view.
+ *
+ * This display only supports views that are {@link Component}s themselves.
+ * Attempting to display a view that is not a component causes an exception
+ * to be thrown.
+ *
+ * By default, the view display has full size.
+ */
+ public static class SimpleViewDisplay extends CustomComponent implements
+ ViewDisplay {
+
+ /**
+ * Create new {@link ViewDisplay} that is itself a component displaying
+ * the view.
+ */
+ public SimpleViewDisplay() {
+ setSizeFull();
+ }
+
+ @Override
+ public void showView(View view) {
+ if (view instanceof Component) {
+ setCompositionRoot((Component) view);
+ } else {
+ throw new IllegalArgumentException("View is not a component: "
+ + view);
+ }
+ }
+ }
+
+ /**
+ * View display that replaces the contents of a {@link ComponentContainer}
+ * with the active {@link View}.
+ *
+ * All components of the container are removed before adding the new view to
+ * it.
+ *
+ * This display only supports views that are {@link Component}s themselves.
+ * Attempting to display a view that is not a component causes an exception
+ * to be thrown.
+ */
+ public static class ComponentContainerViewDisplay implements ViewDisplay {
+
+ private final ComponentContainer container;
+
+ /**
+ * Create new {@link ViewDisplay} that updates a
+ * {@link ComponentContainer} to show the view.
+ */
+ public ComponentContainerViewDisplay(ComponentContainer container) {
+ this.container = container;
+ }
+
+ @Override
+ public void showView(View view) {
+ if (view instanceof Component) {
+ container.removeAllComponents();
+ container.addComponent((Component) view);
+ } else {
+ throw new IllegalArgumentException("View is not a component: "
+ + view);
+ }
+ }
+ }
+
+ /**
+ * View provider which supports mapping a single view name to a single
+ * pre-initialized view instance.
+ *
+ * For most cases, ClassBasedViewProvider should be used instead of this.
+ */
+ public static class StaticViewProvider implements ViewProvider {
+ private final String viewName;
+ private final View view;
+
+ /**
+ * Create a new view provider which returns a pre-created view instance.
+ *
+ * @param viewName
+ * name of the view (not null)
+ * @param view
+ * view instance to return (not null), reused on every
+ * request
+ */
+ public StaticViewProvider(String viewName, View view) {
+ this.viewName = viewName;
+ this.view = view;
+ }
+
+ @Override
+ public String getViewName(String viewAndParameters) {
+ if (null == viewAndParameters) {
+ return null;
+ }
+ if (viewAndParameters.startsWith(viewName)) {
+ return viewName;
+ }
+ return null;
+ }
+
+ @Override
+ public View getView(String viewName) {
+ if (this.viewName.equals(viewName)) {
+ return view;
+ }
+ return null;
+ }
+
+ /**
+ * Get the view name for this provider.
+ *
+ * @return view name for this provider
+ */
+ public String getViewName() {
+ return viewName;
+ }
+ }
+
+ /**
+ * View provider which maps a single view name to a class to instantiate for
+ * the view.
+ *
+ * Note that the view class must be accessible by the class loader used by
+ * the provider. This may require its visibility to be public.
+ *
+ * This class is primarily for internal use by {@link Navigator}.
+ */
+ public static class ClassBasedViewProvider implements ViewProvider {
+
+ private final String viewName;
+ private final Class<? extends View> viewClass;
+
+ /**
+ * Create a new view provider which creates new view instances based on
+ * a view class.
+ *
+ * @param viewName
+ * name of the views to create (not null)
+ * @param viewClass
+ * class to instantiate when a view is requested (not null)
+ */
+ public ClassBasedViewProvider(String viewName,
+ Class<? extends View> viewClass) {
+ if (null == viewName || null == viewClass) {
+ throw new IllegalArgumentException(
+ "View name and class should not be null");
+ }
+ this.viewName = viewName;
+ this.viewClass = viewClass;
+ }
+
+ @Override
+ public String getViewName(String viewAndParameters) {
+ if (null == viewAndParameters) {
+ return null;
+ }
+ if (viewAndParameters.equals(viewName)
+ || viewAndParameters.startsWith(viewName + "/")) {
+ return viewName;
+ }
+ return null;
+ }
+
+ @Override
+ public View getView(String viewName) {
+ if (this.viewName.equals(viewName)) {
+ try {
+ View view = viewClass.newInstance();
+ return view;
+ } catch (InstantiationException e) {
+ // TODO error handling
+ throw new RuntimeException(e);
+ } catch (IllegalAccessException e) {
+ // TODO error handling
+ throw new RuntimeException(e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the view name for this provider.
+ *
+ * @return view name for this provider
+ */
+ public String getViewName() {
+ return viewName;
+ }
+
+ /**
+ * Get the view class for this provider.
+ *
+ * @return {@link View} class
+ */
+ public Class<? extends View> getViewClass() {
+ return viewClass;
+ }
+ }
+
+ private final FragmentManager fragmentManager;
+ private final ViewDisplay display;
+ private View currentView = null;
+ private List<ViewChangeListener> listeners = new LinkedList<ViewChangeListener>();
+ private List<ViewProvider> providers = new LinkedList<ViewProvider>();
+
+ /**
+ * Create a navigator that is tracking the active view using URI fragments
+ * of the current {@link Page} and replacing the contents of a
+ * {@link ComponentContainer} with the active view.
+ *
+ * In case the container is not on the current page, use another
+ * {@link Navigator#Navigator(Page, ViewDisplay)} with an explicitly created
+ * {@link ComponentContainerViewDisplay}.
+ *
+ * All components of the container are removed each time before adding the
+ * active {@link View}. Views must implement {@link Component} when using
+ * this constructor.
+ *
+ * <p>
+ * After all {@link View}s and {@link ViewProvider}s have been registered,
+ * the application should trigger navigation to the current fragment using
+ * e.g.
+ *
+ * <pre>
+ * navigator.navigateTo(Page.getCurrent().getFragment());
+ * </pre>
+ *
+ * @param container
+ * ComponentContainer whose contents should be replaced with the
+ * active view on view change
+ */
+ public Navigator(ComponentContainer container) {
+ display = new ComponentContainerViewDisplay(container);
+ fragmentManager = new UriFragmentManager(Page.getCurrent(), this);
+ }
+
+ /**
+ * Create a navigator that is tracking the active view using URI fragments.
+ *
+ * <p>
+ * After all {@link View}s and {@link ViewProvider}s have been registered,
+ * the application should trigger navigation to the current fragment using
+ * e.g.
+ *
+ * <pre>
+ * navigator.navigateTo(Page.getCurrent().getFragment());
+ * </pre>
+ *
+ * @param page
+ * whose URI fragments are used
+ * @param display
+ * where to display the views
+ */
+ public Navigator(Page page, ViewDisplay display) {
+ this.display = display;
+ fragmentManager = new UriFragmentManager(page, this);
+ }
+
+ /**
+ * Create a navigator.
+ *
+ * When a custom fragment manager is not needed, use the constructor
+ * {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based
+ * fragment manager.
+ *
+ * Note that navigation to the initial view must be performed explicitly by
+ * the application after creating a Navigator using this constructor.
+ *
+ * @param fragmentManager
+ * fragment manager keeping track of the active view and enabling
+ * bookmarking and direct navigation
+ * @param display
+ * where to display the views
+ */
+ public Navigator(FragmentManager fragmentManager, ViewDisplay display) {
+ this.display = display;
+ this.fragmentManager = fragmentManager;
+ }
+
+ /**
+ * Navigate to a view and initialize the view with given parameters.
+ *
+ * The view string consists of a view name optionally followed by a slash
+ * and (fragment) parameters. ViewProviders are used to find and create the
+ * correct type of view.
+ *
+ * If multiple providers return a matching view, the view with the longest
+ * name is selected. This way, e.g. hierarchies of subviews can be
+ * registered like "admin/", "admin/users", "admin/settings" and the longest
+ * match is used.
+ *
+ * If the view being deactivated indicates it wants a confirmation for the
+ * navigation operation, the user is asked for the confirmation.
+ *
+ * Registered {@link ViewChangeListener}s are called upon successful view
+ * change.
+ *
+ * @param viewAndParameters
+ * view name and parameters
+ */
+ public void navigateTo(String viewAndParameters) {
+ String longestViewName = null;
+ View viewWithLongestName = null;
+ for (ViewProvider provider : providers) {
+ String viewName = provider.getViewName(viewAndParameters);
+ if (null != viewName
+ && (longestViewName == null || viewName.length() > longestViewName
+ .length())) {
+ View view = provider.getView(viewName);
+ if (null != view) {
+ longestViewName = viewName;
+ viewWithLongestName = view;
+ }
+ }
+ }
+ if (viewWithLongestName != null) {
+ String parameters = null;
+ if (viewAndParameters.length() > longestViewName.length() + 1) {
+ parameters = viewAndParameters.substring(longestViewName
+ .length() + 1);
+ }
+ navigateTo(viewWithLongestName, longestViewName, parameters);
+ }
+ // TODO if no view is found, what to do?
+ }
+
+ /**
+ * Internal method activating a view, setting its parameters and calling
+ * listeners.
+ *
+ * This method also verifies that the user is allowed to perform the
+ * navigation operation.
+ *
+ * @param view
+ * view to activate
+ * @param viewName
+ * (optional) name of the view or null not to set the fragment
+ * @param fragmentParameters
+ * parameters passed in the fragment for the view
+ */
+ protected void navigateTo(View view, String viewName,
+ String fragmentParameters) {
+ ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
+ viewName, fragmentParameters);
+ if (!isViewChangeAllowed(event)) {
+ return;
+ }
+
+ if (null != viewName && getFragmentManager() != null) {
+ String currentFragment = viewName;
+ if (fragmentParameters != null) {
+ currentFragment += "/" + fragmentParameters;
+ }
+ if (!currentFragment.equals(getFragmentManager().getFragment())) {
+ getFragmentManager().setFragment(currentFragment);
+ }
+ }
+
+ view.navigateTo(fragmentParameters);
+ currentView = view;
+
+ if (display != null) {
+ display.showView(view);
+ }
+
+ fireViewChange(event);
+ }
+
+ /**
+ * Check whether view change is allowed.
+ *
+ * All related listeners are called. The view change is blocked if any of
+ * them wants to block the navigation operation.
+ *
+ * The view change listeners may also e.g. open a warning or question dialog
+ * and save the parameters to re-initiate the navigation operation upon user
+ * action.
+ *
+ * @param event
+ * view change event (not null, view change not yet performed)
+ * @return true if the view change should be allowed, false to silently
+ * block the navigation operation
+ */
+ protected boolean isViewChangeAllowed(ViewChangeEvent event) {
+ for (ViewChangeListener l : listeners) {
+ if (!l.isViewChangeAllowed(event)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return the fragment manager that is used to get, listen to and manipulate
+ * the URI fragment or other source of navigation information.
+ *
+ * @return fragment manager in use
+ */
+ protected FragmentManager getFragmentManager() {
+ return fragmentManager;
+ }
+
+ /**
+ * Returns the ViewDisplay used by the navigator. Unless another display is
+ * specified, a {@link SimpleViewDisplay} (which is a {@link Component}) is
+ * used by default.
+ *
+ * @return current ViewDisplay
+ */
+ public ViewDisplay getDisplay() {
+ return display;
+ }
+
+ /**
+ * Fire an event when the current view has changed.
+ *
+ * @param event
+ * view change event (not null)
+ */
+ protected void fireViewChange(ViewChangeEvent event) {
+ for (ViewChangeListener l : listeners) {
+ l.navigatorViewChanged(event);
+ }
+ }
+
+ /**
+ * Register a static, pre-initialized view instance for a view name.
+ *
+ * Registering another view with a name that is already registered
+ * overwrites the old registration of the same type.
+ *
+ * @param viewName
+ * String that identifies a view (not null nor empty string)
+ * @param view
+ * {@link View} instance (not null)
+ */
+ public void addView(String viewName, View view) {
+
+ // Check parameters
+ if (viewName == null || view == null) {
+ throw new IllegalArgumentException(
+ "view and viewName must be non-null");
+ }
+
+ removeView(viewName);
+ registerProvider(new StaticViewProvider(viewName, view));
+ }
+
+ /**
+ * Register for a view name a view class.
+ *
+ * Registering another view with a name that is already registered
+ * overwrites the old registration of the same type.
+ *
+ * A new view instance is created every time a view is requested.
+ *
+ * @param viewName
+ * String that identifies a view (not null nor empty string)
+ * @param viewClass
+ * {@link View} class to instantiate when a view is requested
+ * (not null)
+ */
+ public void addView(String viewName, Class<? extends View> viewClass) {
+
+ // Check parameters
+ if (viewName == null || viewClass == null) {
+ throw new IllegalArgumentException(
+ "view and viewClass must be non-null");
+ }
+
+ removeView(viewName);
+ registerProvider(new ClassBasedViewProvider(viewName, viewClass));
+ }
+
+ /**
+ * Remove view from navigator.
+ *
+ * This method only applies to views registered using
+ * {@link #addView(String, View)} or {@link #addView(String, Class)}.
+ *
+ * @param viewName
+ * name of the view to remove
+ */
+ public void removeView(String viewName) {
+ Iterator<ViewProvider> it = providers.iterator();
+ while (it.hasNext()) {
+ ViewProvider provider = it.next();
+ if (provider instanceof StaticViewProvider) {
+ StaticViewProvider staticProvider = (StaticViewProvider) provider;
+ if (staticProvider.getViewName().equals(viewName)) {
+ it.remove();
+ }
+ } else if (provider instanceof ClassBasedViewProvider) {
+ ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider;
+ if (classBasedProvider.getViewName().equals(viewName)) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a view provider (factory).
+ *
+ * Providers are called in order of registration until one that can handle
+ * the requested view name is found.
+ *
+ * @param provider
+ * provider to register
+ */
+ public void registerProvider(ViewProvider provider) {
+ providers.add(provider);
+ }
+
+ /**
+ * Unregister a view provider (factory).
+ *
+ * @param provider
+ * provider to unregister
+ */
+ public void unregisterProvider(ViewProvider provider) {
+ providers.remove(provider);
+ }
+
+ /**
+ * Listen to changes of the active view.
+ *
+ * The listener will get notified after the view has changed.
+ *
+ * @param listener
+ * Listener to invoke after view changes.
+ */
+ public void addListener(ViewChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ /**
+ * Remove a view change listener.
+ *
+ * @param listener
+ * Listener to remove.
+ */
+ public void removeListener(ViewChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/navigator/View.java b/server/src/com/vaadin/navigator/View.java
new file mode 100644
index 0000000000..4d135b4c0b
--- /dev/null
+++ b/server/src/com/vaadin/navigator/View.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Component;
+
+/**
+ * Interface for all views controlled by the navigator.
+ *
+ * Each view added to the navigator must implement this interface. Typically, a
+ * view is a {@link Component}.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface View extends Serializable {
+
+ /**
+ * This view is navigated to.
+ *
+ * This method is always called before the view is shown on screen. If there
+ * is any additional id to data what should be shown in the view, it is also
+ * optionally passed as parameter.
+ *
+ * TODO fragmentParameters null if no parameters or empty string?
+ *
+ * @param fragmentParameters
+ * parameters to the view or null if none given. This is the
+ * string that appears e.g. in URI after "viewname/"
+ */
+ public void navigateTo(String fragmentParameters);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/navigator/ViewChangeListener.java b/server/src/com/vaadin/navigator/ViewChangeListener.java
new file mode 100644
index 0000000000..2eb34e6fcf
--- /dev/null
+++ b/server/src/com/vaadin/navigator/ViewChangeListener.java
@@ -0,0 +1,118 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+import java.util.EventObject;
+
+/**
+ * Interface for listening to View changes before and after they occur.
+ *
+ * Implementations of this interface can also block navigation between views
+ * before it is performed.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewChangeListener extends Serializable {
+
+ /**
+ * Event received by the listener for attempted and executed view changes.
+ */
+ public static class ViewChangeEvent extends EventObject {
+ private final View oldView;
+ private final View newView;
+ private final String viewName;
+ private final String fragmentParameters;
+
+ /**
+ * Create a new view change event.
+ *
+ * @param navigator
+ * Navigator that triggered the event, not null
+ */
+ public ViewChangeEvent(Navigator navigator, View oldView, View newView,
+ String viewName, String fragmentParameters) {
+ super(navigator);
+ this.oldView = oldView;
+ this.newView = newView;
+ this.viewName = viewName;
+ this.fragmentParameters = fragmentParameters;
+ }
+
+ /**
+ * Returns the navigator that triggered this event.
+ *
+ * @return Navigator (not null)
+ */
+ public Navigator getNavigator() {
+ return (Navigator) getSource();
+ }
+
+ /**
+ * Returns the view being deactivated.
+ *
+ * @return old View
+ */
+ public View getOldView() {
+ return oldView;
+ }
+
+ /**
+ * Returns the view being activated.
+ *
+ * @return new View
+ */
+ public View getNewView() {
+ return newView;
+ }
+
+ /**
+ * Returns the view name of the view being activated.
+ *
+ * @return view name of the new View
+ */
+ public String getViewName() {
+ return viewName;
+ }
+
+ /**
+ * Returns the parameters for the view being activated.
+ *
+ * @return fragment parameters (potentially bookmarkable) for the new
+ * view
+ */
+ public String getFragmentParameters() {
+ return fragmentParameters;
+ }
+ }
+
+ /**
+ * Check whether changing the view is permissible.
+ *
+ * This method may also e.g. open a "save" dialog or question about the
+ * change, which may re-initiate the navigation operation after user action.
+ *
+ * If this listener does not want to block the view change (e.g. does not
+ * know the view in question), it should return true. If any listener
+ * returns false, the view change is not allowed.
+ *
+ * @param event
+ * view change event
+ * @return true if the view change should be allowed or this listener does
+ * not care about the view change, false to block the change
+ */
+ public boolean isViewChangeAllowed(ViewChangeEvent event);
+
+ /**
+ * Invoked after the view has changed. Be careful for deadlocks if you
+ * decide to change the view again in the listener.
+ *
+ * @param event
+ * view change event
+ */
+ public void navigatorViewChanged(ViewChangeEvent event);
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/navigator/ViewDisplay.java b/server/src/com/vaadin/navigator/ViewDisplay.java
new file mode 100644
index 0000000000..6016951394
--- /dev/null
+++ b/server/src/com/vaadin/navigator/ViewDisplay.java
@@ -0,0 +1,29 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * Interface for displaying a view in an appropriate location.
+ *
+ * The view display can be a component/layout itself or can modify a separate
+ * layout.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewDisplay extends Serializable {
+ /**
+ * Remove previously shown view and show the newly selected view in its
+ * place.
+ *
+ * The parameters for the view have been set before this method is called.
+ *
+ * @param view
+ * new view to show
+ */
+ public void showView(View view);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/navigator/ViewProvider.java b/server/src/com/vaadin/navigator/ViewProvider.java
new file mode 100644
index 0000000000..4d9d22acab
--- /dev/null
+++ b/server/src/com/vaadin/navigator/ViewProvider.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.navigator;
+
+import java.io.Serializable;
+
+/**
+ * A provider for view instances that can return pre-registered views or
+ * dynamically create new views.
+ *
+ * If multiple providers are used, {@link #getViewName(String)} of each is
+ * called (in registration order) until one of them returns a non-null value.
+ * The {@link #getView(String)} method of that provider is then used.
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public interface ViewProvider extends Serializable {
+ /**
+ * Extract the view name from a combined view name and parameter string.
+ * This method should return a view name if and only if this provider
+ * handles creation of such views.
+ *
+ * @param viewAndParameters
+ * string with view name and its fragment parameters (if given),
+ * not null
+ * @return view name if the view is handled by this provider, null otherwise
+ */
+ public String getViewName(String viewAndParameters);
+
+ /**
+ * Create or return a pre-created instance of a view.
+ *
+ * The parameters for the view are set separately by the navigator when the
+ * view is activated.
+ *
+ * @param viewName
+ * name of the view, not null
+ * @return newly created view (null if none available for the view name)
+ */
+ public View getView(String viewName);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/package.html b/server/src/com/vaadin/package.html
new file mode 100644
index 0000000000..f771019709
--- /dev/null
+++ b/server/src/com/vaadin/package.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<p>The Vaadin base package. Contains the Application class, the
+starting point of any application that uses Vaadin.</p>
+
+<p>Contains all Vaadin core classes. A Vaadin application is based
+on the {@link com.vaadin.Application} class and deployed as a servlet
+using {@link com.vaadin.terminal.gwt.server.ApplicationServlet} or
+{@link com.vaadin.terminal.gwt.server.GAEApplicationServlet} (for Google
+App Engine).</p>
+
+<p>Vaadin applications can also be deployed as portlets using {@link
+com.vaadin.terminal.gwt.server.ApplicationPortlet} (JSR-168) or {@link
+com.vaadin.terminal.gwt.server.ApplicationPortlet2} (JSR-286).</p>
+
+<p>All classes in Vaadin are serializable unless otherwise noted.
+This allows Vaadin applications to run in cluster and cloud
+environments.</p>
+
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml b/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml
new file mode 100644
index 0000000000..bd91d05b02
--- /dev/null
+++ b/server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml
@@ -0,0 +1,6 @@
+<module>
+ <!-- WS Compiler: manually edited -->
+
+ <!-- Inherit the DefaultWidgetSet -->
+ <inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet" />
+</module>
diff --git a/server/src/com/vaadin/service/ApplicationContext.java b/server/src/com/vaadin/service/ApplicationContext.java
new file mode 100644
index 0000000000..71bff7b865
--- /dev/null
+++ b/server/src/com/vaadin/service/ApplicationContext.java
@@ -0,0 +1,165 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.service;
+
+import java.io.File;
+import java.io.Serializable;
+import java.net.URL;
+import java.util.Collection;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.gwt.server.AbstractCommunicationManager;
+
+/**
+ * <code>ApplicationContext</code> provides information about the running
+ * context of the application. Each context is shared by all applications that
+ * are open for one user. In a web-environment this corresponds to a
+ * HttpSession.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.1
+ */
+public interface ApplicationContext extends Serializable {
+
+ /**
+ * Returns application context base directory.
+ *
+ * Typically an application is deployed in a such way that is has an
+ * application directory. For web applications this directory is the root
+ * directory of the web applications. In some cases applications might not
+ * have an application directory (for example web applications running
+ * inside a war).
+ *
+ * @return The application base directory or null if the application has no
+ * base directory.
+ */
+ public File getBaseDirectory();
+
+ /**
+ * Returns a collection of all the applications in this context.
+ *
+ * Each application context contains all active applications for one user.
+ *
+ * @return A collection containing all the applications in this context.
+ */
+ public Collection<Application> getApplications();
+
+ /**
+ * Adds a transaction listener to this context. The transaction listener is
+ * called before and after each each request related to this session except
+ * when serving static resources.
+ *
+ * The transaction listener must not be null.
+ *
+ * @see com.vaadin.service.ApplicationContext#addTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener)
+ */
+ public void addTransactionListener(TransactionListener listener);
+
+ /**
+ * Removes a transaction listener from this context.
+ *
+ * @param listener
+ * the listener to be removed.
+ * @see TransactionListener
+ */
+ public void removeTransactionListener(TransactionListener listener);
+
+ /**
+ * Generate a URL that can be used as the relative location of e.g. an
+ * {@link ApplicationResource}.
+ *
+ * This method should only be called from the processing of a UIDL request,
+ * not from a background thread. The return value is null if used outside a
+ * suitable request.
+ *
+ * @deprecated this method is intended for terminal implementation only and
+ * is subject to change/removal from the interface (to
+ * {@link AbstractCommunicationManager})
+ *
+ * @param resource
+ * @param urlKey
+ * a key for the resource that can later be extracted from a URL
+ * with {@link #getURLKey(URL, String)}
+ */
+ @Deprecated
+ public String generateApplicationResourceURL(ApplicationResource resource,
+ String urlKey);
+
+ /**
+ * Tests if a URL is for an application resource (APP/...).
+ *
+ * @deprecated this method is intended for terminal implementation only and
+ * is subject to change/removal from the interface (to
+ * {@link AbstractCommunicationManager})
+ *
+ * @param context
+ * @param relativeUri
+ * @return
+ */
+ @Deprecated
+ public boolean isApplicationResourceURL(URL context, String relativeUri);
+
+ /**
+ * Gets the identifier (key) from an application resource URL. This key is
+ * the one that was given to
+ * {@link #generateApplicationResourceURL(ApplicationResource, String)} when
+ * creating the URL.
+ *
+ * @deprecated this method is intended for terminal implementation only and
+ * is subject to change/removal from the interface (to
+ * {@link AbstractCommunicationManager})
+ *
+ *
+ * @param context
+ * @param relativeUri
+ * @return
+ */
+ @Deprecated
+ public String getURLKey(URL context, String relativeUri);
+
+ /**
+ * Interface for listening to transaction events. Implement this interface
+ * to listen to all transactions between the client and the application.
+ *
+ */
+ public interface TransactionListener extends Serializable {
+
+ /**
+ * Invoked at the beginning of every transaction.
+ *
+ * The transaction is linked to the context, not the application so if
+ * you have multiple applications running in the same context you need
+ * to check that the request is associated with the application you are
+ * interested in. This can be done looking at the application parameter.
+ *
+ * @param application
+ * the Application object.
+ * @param transactionData
+ * the Data identifying the transaction.
+ */
+ public void transactionStart(Application application,
+ Object transactionData);
+
+ /**
+ * Invoked at the end of every transaction.
+ *
+ * The transaction is linked to the context, not the application so if
+ * you have multiple applications running in the same context you need
+ * to check that the request is associated with the application you are
+ * interested in. This can be done looking at the application parameter.
+ *
+ * @param applcation
+ * the Application object.
+ * @param transactionData
+ * the Data identifying the transaction.
+ */
+ public void transactionEnd(Application application,
+ Object transactionData);
+
+ }
+}
diff --git a/server/src/com/vaadin/service/FileTypeResolver.java b/server/src/com/vaadin/service/FileTypeResolver.java
new file mode 100644
index 0000000000..c457c16eb4
--- /dev/null
+++ b/server/src/com/vaadin/service/FileTypeResolver.java
@@ -0,0 +1,385 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.service;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.ThemeResource;
+
+/**
+ * Utility class that can figure out mime-types and icons related to files.
+ * <p>
+ * Note : The icons are associated purely to mime-types, so a file may not have
+ * a custom icon accessible with this class.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class FileTypeResolver implements Serializable {
+
+ /**
+ * Default icon given if no icon is specified for a mime-type.
+ */
+ static public Resource DEFAULT_ICON = new ThemeResource(
+ "../runo/icons/16/document.png");
+
+ /**
+ * Default mime-type.
+ */
+ static public String DEFAULT_MIME_TYPE = "application/octet-stream";
+
+ /**
+ * Initial file extension to mime-type mapping.
+ */
+ static private String initialExtToMIMEMap = "application/cu-seeme csm cu,"
+ + "application/dsptype tsp,"
+ + "application/futuresplash spl,"
+ + "application/mac-binhex40 hqx,"
+ + "application/msaccess mdb,"
+ + "application/msword doc dot,"
+ + "application/octet-stream bin,"
+ + "application/oda oda,"
+ + "application/pdf pdf,"
+ + "application/pgp-signature pgp,"
+ + "application/postscript ps ai eps,"
+ + "application/rtf rtf,"
+ + "application/vnd.ms-excel xls xlb,"
+ + "application/vnd.ms-powerpoint ppt pps pot,"
+ + "application/vnd.wap.wmlc wmlc,"
+ + "application/vnd.wap.wmlscriptc wmlsc,"
+ + "application/wordperfect5.1 wp5,"
+ + "application/zip zip,"
+ + "application/x-123 wk,"
+ + "application/x-bcpio bcpio,"
+ + "application/x-chess-pgn pgn,"
+ + "application/x-cpio cpio,"
+ + "application/x-debian-package deb,"
+ + "application/x-director dcr dir dxr,"
+ + "application/x-dms dms,"
+ + "application/x-dvi dvi,"
+ + "application/x-xfig fig,"
+ + "application/x-font pfa pfb gsf pcf pcf.Z,"
+ + "application/x-gnumeric gnumeric,"
+ + "application/x-gtar gtar tgz taz,"
+ + "application/x-hdf hdf,"
+ + "application/x-httpd-php phtml pht php,"
+ + "application/x-httpd-php3 php3,"
+ + "application/x-httpd-php3-source phps,"
+ + "application/x-httpd-php3-preprocessed php3p,"
+ + "application/x-httpd-php4 php4,"
+ + "application/x-ica ica,"
+ + "application/x-java-archive jar,"
+ + "application/x-java-serialized-object ser,"
+ + "application/x-java-vm class,"
+ + "application/x-javascript js,"
+ + "application/x-kchart chrt,"
+ + "application/x-killustrator kil,"
+ + "application/x-kpresenter kpr kpt,"
+ + "application/x-kspread ksp,"
+ + "application/x-kword kwd kwt,"
+ + "application/x-latex latex,"
+ + "application/x-lha lha,"
+ + "application/x-lzh lzh,"
+ + "application/x-lzx lzx,"
+ + "application/x-maker frm maker frame fm fb book fbdoc,"
+ + "application/x-mif mif,"
+ + "application/x-msdos-program com exe bat dll,"
+ + "application/x-msi msi,"
+ + "application/x-netcdf nc cdf,"
+ + "application/x-ns-proxy-autoconfig pac,"
+ + "application/x-object o,"
+ + "application/x-ogg ogg,"
+ + "application/x-oz-application oza,"
+ + "application/x-perl pl pm,"
+ + "application/x-pkcs7-crl crl,"
+ + "application/x-redhat-package-manager rpm,"
+ + "application/x-shar shar,"
+ + "application/x-shockwave-flash swf swfl,"
+ + "application/x-star-office sdd sda,"
+ + "application/x-stuffit sit,"
+ + "application/x-sv4cpio sv4cpio,"
+ + "application/x-sv4crc sv4crc,"
+ + "application/x-tar tar,"
+ + "application/x-tex-gf gf,"
+ + "application/x-tex-pk pk PK,"
+ + "application/x-texinfo texinfo texi,"
+ + "application/x-trash ~ % bak old sik,"
+ + "application/x-troff t tr roff,"
+ + "application/x-troff-man man,"
+ + "application/x-troff-me me,"
+ + "application/x-troff-ms ms,"
+ + "application/x-ustar ustar,"
+ + "application/x-wais-source src,"
+ + "application/x-wingz wz,"
+ + "application/x-x509-ca-cert crt,"
+ + "audio/basic au snd,"
+ + "audio/midi mid midi,"
+ + "audio/mpeg mpga mpega mp2 mp3,"
+ + "audio/mpegurl m3u,"
+ + "audio/prs.sid sid,"
+ + "audio/x-aiff aif aiff aifc,"
+ + "audio/x-gsm gsm,"
+ + "audio/x-pn-realaudio ra rm ram,"
+ + "audio/x-scpls pls,"
+ + "audio/x-wav wav,"
+ + "audio/ogg ogg,"
+ + "audio/mp4 m4a,"
+ + "audio/x-aac aac,"
+ + "image/bitmap bmp,"
+ + "image/gif gif,"
+ + "image/ief ief,"
+ + "image/jpeg jpeg jpg jpe,"
+ + "image/pcx pcx,"
+ + "image/png png,"
+ + "image/svg+xml svg svgz,"
+ + "image/tiff tiff tif,"
+ + "image/vnd.wap.wbmp wbmp,"
+ + "image/x-cmu-raster ras,"
+ + "image/x-coreldraw cdr,"
+ + "image/x-coreldrawpattern pat,"
+ + "image/x-coreldrawtemplate cdt,"
+ + "image/x-corelphotopaint cpt,"
+ + "image/x-jng jng,"
+ + "image/x-portable-anymap pnm,"
+ + "image/x-portable-bitmap pbm,"
+ + "image/x-portable-graymap pgm,"
+ + "image/x-portable-pixmap ppm,"
+ + "image/x-rgb rgb,"
+ + "image/x-xbitmap xbm,"
+ + "image/x-xpixmap xpm,"
+ + "image/x-xwindowdump xwd,"
+ + "text/comma-separated-values csv,"
+ + "text/css css,"
+ + "text/html htm html xhtml,"
+ + "text/mathml mml,"
+ + "text/plain txt text diff,"
+ + "text/richtext rtx,"
+ + "text/tab-separated-values tsv,"
+ + "text/vnd.wap.wml wml,"
+ + "text/vnd.wap.wmlscript wmls,"
+ + "text/xml xml,"
+ + "text/x-c++hdr h++ hpp hxx hh,"
+ + "text/x-c++src c++ cpp cxx cc,"
+ + "text/x-chdr h,"
+ + "text/x-csh csh,"
+ + "text/x-csrc c,"
+ + "text/x-java java,"
+ + "text/x-moc moc,"
+ + "text/x-pascal p pas,"
+ + "text/x-setext etx,"
+ + "text/x-sh sh,"
+ + "text/x-tcl tcl tk,"
+ + "text/x-tex tex ltx sty cls,"
+ + "text/x-vcalendar vcs,"
+ + "text/x-vcard vcf,"
+ + "video/dl dl,"
+ + "video/fli fli,"
+ + "video/gl gl,"
+ + "video/mpeg mpeg mpg mpe,"
+ + "video/quicktime qt mov,"
+ + "video/x-mng mng,"
+ + "video/x-ms-asf asf asx,"
+ + "video/x-msvideo avi,"
+ + "video/x-sgi-movie movie,"
+ + "video/ogg ogv,"
+ + "video/mp4 mp4,"
+ + "x-world/x-vrml vrm vrml wrl";
+
+ /**
+ * File extension to MIME type mapping. All extensions are in lower case.
+ */
+ static private Hashtable<String, String> extToMIMEMap = new Hashtable<String, String>();
+
+ /**
+ * MIME type to Icon mapping.
+ */
+ static private Hashtable<String, Resource> MIMEToIconMap = new Hashtable<String, Resource>();
+
+ static {
+
+ // Initialize extension to MIME map
+ final StringTokenizer lines = new StringTokenizer(initialExtToMIMEMap,
+ ",");
+ while (lines.hasMoreTokens()) {
+ final String line = lines.nextToken();
+ final StringTokenizer exts = new StringTokenizer(line);
+ final String type = exts.nextToken();
+ while (exts.hasMoreTokens()) {
+ final String ext = exts.nextToken();
+ addExtension(ext, type);
+ }
+ }
+
+ // Initialize Icons
+ ThemeResource folder = new ThemeResource("../runo/icons/16/folder.png");
+ addIcon("inode/drive", folder);
+ addIcon("inode/directory", folder);
+ }
+
+ /**
+ * Gets the mime-type of a file. Currently the mime-type is resolved based
+ * only on the file name extension.
+ *
+ * @param fileName
+ * the name of the file whose mime-type is requested.
+ * @return mime-type <code>String</code> for the given filename
+ */
+ public static String getMIMEType(String fileName) {
+
+ // Checks for nulls
+ if (fileName == null) {
+ throw new NullPointerException("Filename can not be null");
+ }
+
+ // Calculates the extension of the file
+ int dotIndex = fileName.indexOf(".");
+ while (dotIndex >= 0 && fileName.indexOf(".", dotIndex + 1) >= 0) {
+ dotIndex = fileName.indexOf(".", dotIndex + 1);
+ }
+ dotIndex++;
+
+ if (fileName.length() > dotIndex) {
+ String ext = fileName.substring(dotIndex);
+
+ // Ignore any query parameters
+ int queryStringStart = ext.indexOf('?');
+ if (queryStringStart > 0) {
+ ext = ext.substring(0, queryStringStart);
+ }
+
+ // Return type from extension map, if found
+ final String type = extToMIMEMap.get(ext.toLowerCase());
+ if (type != null) {
+ return type;
+ }
+ }
+
+ return DEFAULT_MIME_TYPE;
+ }
+
+ /**
+ * Gets the descriptive icon representing file, based on the filename. First
+ * the mime-type for the given filename is resolved, and then the
+ * corresponding icon is fetched from the internal icon storage. If it is
+ * not found the default icon is returned.
+ *
+ * @param fileName
+ * the name of the file whose icon is requested.
+ * @return the icon corresponding to the given file
+ */
+ public static Resource getIcon(String fileName) {
+ return getIconByMimeType(getMIMEType(fileName));
+ }
+
+ private static Resource getIconByMimeType(String mimeType) {
+ final Resource icon = MIMEToIconMap.get(mimeType);
+ if (icon != null) {
+ return icon;
+ }
+
+ // If nothing is known about the file-type, general file
+ // icon is used
+ return DEFAULT_ICON;
+ }
+
+ /**
+ * Gets the descriptive icon representing a file. First the mime-type for
+ * the given file name is resolved, and then the corresponding icon is
+ * fetched from the internal icon storage. If it is not found the default
+ * icon is returned.
+ *
+ * @param file
+ * the file whose icon is requested.
+ * @return the icon corresponding to the given file
+ */
+ public static Resource getIcon(File file) {
+ return getIconByMimeType(getMIMEType(file));
+ }
+
+ /**
+ * Gets the mime-type for a file. Currently the returned file type is
+ * resolved by the filename extension only.
+ *
+ * @param file
+ * the file whose mime-type is requested.
+ * @return the files mime-type <code>String</code>
+ */
+ public static String getMIMEType(File file) {
+
+ // Checks for nulls
+ if (file == null) {
+ throw new NullPointerException("File can not be null");
+ }
+
+ // Directories
+ if (file.isDirectory()) {
+ // Drives
+ if (file.getParentFile() == null) {
+ return "inode/drive";
+ } else {
+ return "inode/directory";
+ }
+ }
+
+ // Return type from extension
+ return getMIMEType(file.getName());
+ }
+
+ /**
+ * Adds a mime-type mapping for the given filename extension. If the
+ * extension is already in the internal mapping it is overwritten.
+ *
+ * @param extension
+ * the filename extension to be associated with
+ * <code>MIMEType</code>.
+ * @param MIMEType
+ * the new mime-type for <code>extension</code>.
+ */
+ public static void addExtension(String extension, String MIMEType) {
+ extToMIMEMap.put(extension.toLowerCase(), MIMEType);
+ }
+
+ /**
+ * Adds a icon for the given mime-type. If the mime-type also has a
+ * corresponding icon, it is replaced with the new icon.
+ *
+ * @param MIMEType
+ * the mime-type whose icon is to be changed.
+ * @param icon
+ * the new icon to be associated with <code>MIMEType</code>.
+ */
+ public static void addIcon(String MIMEType, Resource icon) {
+ MIMEToIconMap.put(MIMEType, icon);
+ }
+
+ /**
+ * Gets the internal file extension to mime-type mapping.
+ *
+ * @return unmodifiable map containing the current file extension to
+ * mime-type mapping
+ */
+ public static Map<String, String> getExtensionToMIMETypeMapping() {
+ return Collections.unmodifiableMap(extToMIMEMap);
+ }
+
+ /**
+ * Gets the internal mime-type to icon mapping.
+ *
+ * @return unmodifiable map containing the current mime-type to icon mapping
+ */
+ public static Map<String, Resource> getMIMETypeToIconMapping() {
+ return Collections.unmodifiableMap(MIMEToIconMap);
+ }
+}
diff --git a/server/src/com/vaadin/service/package.html b/server/src/com/vaadin/service/package.html
new file mode 100644
index 0000000000..ea21139b91
--- /dev/null
+++ b/server/src/com/vaadin/service/package.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides some general service classes used throughout Vaadin
+based applications.</p>
+
+<!-- <h2>Package Specification</h2> -->
+
+<!-- Package spec here -->
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/terminal/AbstractClientConnector.java b/server/src/com/vaadin/terminal/AbstractClientConnector.java
new file mode 100644
index 0000000000..9c68361382
--- /dev/null
+++ b/server/src/com/vaadin/terminal/AbstractClientConnector.java
@@ -0,0 +1,510 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.logging.Logger;
+
+import com.vaadin.Application;
+import com.vaadin.shared.communication.ClientRpc;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+import com.vaadin.terminal.gwt.server.ClientMethodInvocation;
+import com.vaadin.terminal.gwt.server.RpcManager;
+import com.vaadin.terminal.gwt.server.RpcTarget;
+import com.vaadin.terminal.gwt.server.ServerRpcManager;
+import com.vaadin.ui.HasComponents;
+import com.vaadin.ui.Root;
+
+/**
+ * An abstract base class for ClientConnector implementations. This class
+ * provides all the basic functionality required for connectors.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public abstract class AbstractClientConnector implements ClientConnector {
+ /**
+ * A map from client to server RPC interface class to the RPC call manager
+ * that handles incoming RPC calls for that interface.
+ */
+ private Map<Class<?>, RpcManager> rpcManagerMap = new HashMap<Class<?>, RpcManager>();
+
+ /**
+ * A map from server to client RPC interface class to the RPC proxy that
+ * sends ourgoing RPC calls for that interface.
+ */
+ private Map<Class<?>, ClientRpc> rpcProxyMap = new HashMap<Class<?>, ClientRpc>();
+
+ /**
+ * Shared state object to be communicated from the server to the client when
+ * modified.
+ */
+ private SharedState sharedState;
+
+ /**
+ * Pending RPC method invocations to be sent.
+ */
+ private ArrayList<ClientMethodInvocation> pendingInvocations = new ArrayList<ClientMethodInvocation>();
+
+ private String connectorId;
+
+ private ArrayList<Extension> extensions = new ArrayList<Extension>();
+
+ private ClientConnector parent;
+
+ /* Documentation copied from interface */
+ @Override
+ public void requestRepaint() {
+ Root root = getRoot();
+ if (root != null) {
+ root.getConnectorTracker().markDirty(this);
+ }
+ }
+
+ /**
+ * Registers an RPC interface implementation for this component.
+ *
+ * A component can listen to multiple RPC interfaces, and subclasses can
+ * register additional implementations.
+ *
+ * @since 7.0
+ *
+ * @param implementation
+ * RPC interface implementation
+ * @param rpcInterfaceType
+ * RPC interface class for which the implementation should be
+ * registered
+ */
+ protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) {
+ rpcManagerMap.put(rpcInterfaceType, new ServerRpcManager<T>(
+ implementation, rpcInterfaceType));
+ }
+
+ /**
+ * Registers an RPC interface implementation for this component.
+ *
+ * A component can listen to multiple RPC interfaces, and subclasses can
+ * register additional implementations.
+ *
+ * @since 7.0
+ *
+ * @param implementation
+ * RPC interface implementation. Also used to deduce the type.
+ */
+ protected <T extends ServerRpc> void registerRpc(T implementation) {
+ Class<?> cls = implementation.getClass();
+ Class<?>[] interfaces = cls.getInterfaces();
+ while (interfaces.length == 0) {
+ // Search upwards until an interface is found. It must be found as T
+ // extends ServerRpc
+ cls = cls.getSuperclass();
+ interfaces = cls.getInterfaces();
+ }
+ if (interfaces.length != 1
+ || !(ServerRpc.class.isAssignableFrom(interfaces[0]))) {
+ throw new RuntimeException(
+ "Use registerRpc(T implementation, Class<T> rpcInterfaceType) if the Rpc implementation implements more than one interface");
+ }
+ @SuppressWarnings("unchecked")
+ Class<T> type = (Class<T>) interfaces[0];
+ registerRpc(implementation, type);
+ }
+
+ @Override
+ public SharedState getState() {
+ if (null == sharedState) {
+ sharedState = createState();
+ }
+ return sharedState;
+ }
+
+ /**
+ * Creates the shared state bean to be used in server to client
+ * communication.
+ * <p>
+ * By default a state object of the defined return type of
+ * {@link #getState()} is created. Subclasses can override this method and
+ * return a new instance of the correct state class but this should rarely
+ * be necessary.
+ * </p>
+ * <p>
+ * No configuration of the values of the state should be performed in
+ * {@link #createState()}.
+ *
+ * @since 7.0
+ *
+ * @return new shared state object
+ */
+ protected SharedState createState() {
+ try {
+ return getStateType().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Error creating state of type " + getStateType().getName()
+ + " for " + getClass().getName(), e);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.server.ClientConnector#getStateType()
+ */
+ @Override
+ public Class<? extends SharedState> getStateType() {
+ try {
+ Method m = getClass().getMethod("getState", (Class[]) null);
+ Class<?> type = m.getReturnType();
+ return type.asSubclass(SharedState.class);
+ } catch (Exception e) {
+ throw new RuntimeException("Error finding state type for "
+ + getClass().getName(), e);
+ }
+ }
+
+ /**
+ * Returns an RPC proxy for a given server to client RPC interface for this
+ * component.
+ *
+ * TODO more javadoc, subclasses, ...
+ *
+ * @param rpcInterface
+ * RPC interface type
+ *
+ * @since 7.0
+ */
+ public <T extends ClientRpc> T getRpcProxy(final Class<T> rpcInterface) {
+ // create, initialize and return a dynamic proxy for RPC
+ try {
+ if (!rpcProxyMap.containsKey(rpcInterface)) {
+ Class<?> proxyClass = Proxy.getProxyClass(
+ rpcInterface.getClassLoader(), rpcInterface);
+ Constructor<?> constructor = proxyClass
+ .getConstructor(InvocationHandler.class);
+ T rpcProxy = rpcInterface.cast(constructor
+ .newInstance(new RpcInvoicationHandler(rpcInterface)));
+ // cache the proxy
+ rpcProxyMap.put(rpcInterface, rpcProxy);
+ }
+ return (T) rpcProxyMap.get(rpcInterface);
+ } catch (Exception e) {
+ // TODO exception handling?
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static final class AllChildrenIterable implements
+ Iterable<ClientConnector>, Serializable {
+ private final ClientConnector connector;
+
+ private AllChildrenIterable(ClientConnector connector) {
+ this.connector = connector;
+ }
+
+ @Override
+ public Iterator<ClientConnector> iterator() {
+ CombinedIterator<ClientConnector> iterator = new CombinedIterator<ClientConnector>();
+ iterator.addIterator(connector.getExtensions().iterator());
+
+ if (connector instanceof HasComponents) {
+ HasComponents hasComponents = (HasComponents) connector;
+ iterator.addIterator(hasComponents.iterator());
+ }
+
+ return iterator;
+ }
+ }
+
+ private class RpcInvoicationHandler implements InvocationHandler,
+ Serializable {
+
+ private String rpcInterfaceName;
+
+ public RpcInvoicationHandler(Class<?> rpcInterface) {
+ rpcInterfaceName = rpcInterface.getName().replaceAll("\\$", ".");
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ addMethodInvocationToQueue(rpcInterfaceName, method, args);
+ // TODO no need to do full repaint if only RPC calls
+ requestRepaint();
+ return null;
+ }
+
+ }
+
+ /**
+ * For internal use: adds a method invocation to the pending RPC call queue.
+ *
+ * @param interfaceName
+ * RPC interface name
+ * @param method
+ * RPC method
+ * @param parameters
+ * RPC all parameters
+ *
+ * @since 7.0
+ */
+ protected void addMethodInvocationToQueue(String interfaceName,
+ Method method, Object[] parameters) {
+ // add to queue
+ pendingInvocations.add(new ClientMethodInvocation(this, interfaceName,
+ method, parameters));
+ }
+
+ /**
+ * @see RpcTarget#getRpcManager(Class)
+ *
+ * @param rpcInterface
+ * RPC interface for which a call was made
+ * @return RPC Manager handling calls for the interface
+ *
+ * @since 7.0
+ */
+ @Override
+ public RpcManager getRpcManager(Class<?> rpcInterface) {
+ return rpcManagerMap.get(rpcInterface);
+ }
+
+ @Override
+ public List<ClientMethodInvocation> retrievePendingRpcCalls() {
+ if (pendingInvocations.isEmpty()) {
+ return Collections.emptyList();
+ } else {
+ List<ClientMethodInvocation> result = pendingInvocations;
+ pendingInvocations = new ArrayList<ClientMethodInvocation>();
+ return Collections.unmodifiableList(result);
+ }
+ }
+
+ @Override
+ public String getConnectorId() {
+ if (connectorId == null) {
+ if (getApplication() == null) {
+ throw new RuntimeException(
+ "Component must be attached to an application when getConnectorId() is called for the first time");
+ }
+ connectorId = getApplication().createConnectorId(this);
+ }
+ return connectorId;
+ }
+
+ /**
+ * Finds the Application to which this connector belongs. If the connector
+ * has not been attached, <code>null</code> is returned.
+ *
+ * @return The connector's application, or <code>null</code> if not attached
+ */
+ protected Application getApplication() {
+ Root root = getRoot();
+ if (root == null) {
+ return null;
+ } else {
+ return root.getApplication();
+ }
+ }
+
+ /**
+ * Finds a Root ancestor of this connector. <code>null</code> is returned if
+ * no Root ancestor is found (typically because the connector is not
+ * attached to a proper hierarchy).
+ *
+ * @return the Root ancestor of this connector, or <code>null</code> if none
+ * is found.
+ */
+ @Override
+ public Root getRoot() {
+ ClientConnector connector = this;
+ while (connector != null) {
+ if (connector instanceof Root) {
+ return (Root) connector;
+ }
+ connector = connector.getParent();
+ }
+ return null;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractClientConnector.class.getName());
+ }
+
+ @Override
+ public void requestRepaintAll() {
+ requestRepaint();
+
+ for (ClientConnector connector : getAllChildrenIterable(this)) {
+ connector.requestRepaintAll();
+ }
+ }
+
+ private static final class CombinedIterator<T> implements Iterator<T>,
+ Serializable {
+
+ private final Collection<Iterator<? extends T>> iterators = new ArrayList<Iterator<? extends T>>();
+
+ public void addIterator(Iterator<? extends T> iterator) {
+ iterators.add(iterator);
+ }
+
+ @Override
+ public boolean hasNext() {
+ for (Iterator<? extends T> i : iterators) {
+ if (i.hasNext()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public T next() {
+ for (Iterator<? extends T> i : iterators) {
+ if (i.hasNext()) {
+ return i.next();
+ }
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Get an Iterable for iterating over all child connectors, including both
+ * extensions and child components.
+ *
+ * @param connector
+ * the connector to get children for
+ * @return an Iterable giving all child connectors.
+ */
+ public static Iterable<ClientConnector> getAllChildrenIterable(
+ final ClientConnector connector) {
+ return new AllChildrenIterable(connector);
+ }
+
+ @Override
+ public Collection<Extension> getExtensions() {
+ return Collections.unmodifiableCollection(extensions);
+ }
+
+ /**
+ * Add an extension to this connector. This method is protected to allow
+ * extensions to select which targets they can extend.
+ *
+ * @param extension
+ * the extension to add
+ */
+ protected void addExtension(Extension extension) {
+ ClientConnector previousParent = extension.getParent();
+ if (previousParent == this) {
+ // Nothing to do, already attached
+ return;
+ } else if (previousParent != null) {
+ throw new IllegalStateException(
+ "Moving an extension from one parent to another is not supported");
+ }
+
+ extensions.add(extension);
+ extension.setParent(this);
+ requestRepaint();
+ }
+
+ @Override
+ public void removeExtension(Extension extension) {
+ extension.setParent(null);
+ extensions.remove(extension);
+ requestRepaint();
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+
+ // If the parent is not changed, don't do anything
+ if (parent == this.parent) {
+ return;
+ }
+
+ if (parent != null && this.parent != null) {
+ throw new IllegalStateException(getClass().getName()
+ + " already has a parent.");
+ }
+
+ // Send detach event if the component have been connected to a window
+ if (getApplication() != null) {
+ detach();
+ }
+
+ // Connect to new parent
+ this.parent = parent;
+
+ // Send attach event if connected to an application
+ if (getApplication() != null) {
+ attach();
+ }
+ }
+
+ @Override
+ public ClientConnector getParent() {
+ return parent;
+ }
+
+ @Override
+ public void attach() {
+ requestRepaint();
+
+ getRoot().getConnectorTracker().registerConnector(this);
+
+ for (ClientConnector connector : getAllChildrenIterable(this)) {
+ connector.attach();
+ }
+
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>
+ * The {@link #getApplication()} and {@link #getRoot()} methods might return
+ * <code>null</code> after this method is called.
+ * </p>
+ */
+ @Override
+ public void detach() {
+ for (ClientConnector connector : getAllChildrenIterable(this)) {
+ connector.detach();
+ }
+
+ getRoot().getConnectorTracker().unregisterConnector(this);
+ }
+
+ @Override
+ public boolean isConnectorEnabled() {
+ if (getParent() == null) {
+ // No parent -> the component cannot receive updates from the client
+ return false;
+ } else {
+ return getParent().isConnectorEnabled();
+ }
+ }
+}
diff --git a/server/src/com/vaadin/terminal/AbstractErrorMessage.java b/server/src/com/vaadin/terminal/AbstractErrorMessage.java
new file mode 100644
index 0000000000..f7cd0e6aad
--- /dev/null
+++ b/server/src/com/vaadin/terminal/AbstractErrorMessage.java
@@ -0,0 +1,176 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.data.Buffered;
+import com.vaadin.data.Validator;
+import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
+
+/**
+ * Base class for component error messages.
+ *
+ * This class is used on the server side to construct the error messages to send
+ * to the client.
+ *
+ * @since 7.0
+ */
+public abstract class AbstractErrorMessage implements ErrorMessage {
+
+ public enum ContentMode {
+ /**
+ * Content mode, where the error contains only plain text.
+ */
+ TEXT,
+ /**
+ * Content mode, where the error contains preformatted text.
+ */
+ PREFORMATTED,
+ /**
+ * Content mode, where the error contains XHTML.
+ */
+ XHTML;
+ }
+
+ /**
+ * Content mode.
+ */
+ private ContentMode mode = ContentMode.TEXT;
+
+ /**
+ * Message in content mode.
+ */
+ private String message;
+
+ /**
+ * Error level.
+ */
+ private ErrorLevel level = ErrorLevel.ERROR;
+
+ private List<ErrorMessage> causes = new ArrayList<ErrorMessage>();
+
+ protected AbstractErrorMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ protected void setMessage(String message) {
+ this.message = message;
+ }
+
+ /* Documented in interface */
+ @Override
+ public ErrorLevel getErrorLevel() {
+ return level;
+ }
+
+ protected void setErrorLevel(ErrorLevel level) {
+ this.level = level;
+ }
+
+ protected ContentMode getMode() {
+ return mode;
+ }
+
+ protected void setMode(ContentMode mode) {
+ this.mode = mode;
+ }
+
+ protected List<ErrorMessage> getCauses() {
+ return causes;
+ }
+
+ protected void addCause(ErrorMessage cause) {
+ causes.add(cause);
+ }
+
+ @Override
+ public String getFormattedHtmlMessage() {
+ String result = null;
+ switch (getMode()) {
+ case TEXT:
+ result = AbstractApplicationServlet.safeEscapeForHtml(getMessage());
+ break;
+ case PREFORMATTED:
+ result = "<pre>"
+ + AbstractApplicationServlet
+ .safeEscapeForHtml(getMessage()) + "</pre>";
+ break;
+ case XHTML:
+ result = getMessage();
+ break;
+ }
+ // if no message, combine the messages of all children
+ if (null == result && null != getCauses() && getCauses().size() > 0) {
+ StringBuilder sb = new StringBuilder();
+ for (ErrorMessage cause : getCauses()) {
+ String childMessage = cause.getFormattedHtmlMessage();
+ if (null != childMessage) {
+ sb.append("<div>");
+ sb.append(childMessage);
+ sb.append("</div>\n");
+ }
+ }
+ if (sb.length() > 0) {
+ result = sb.toString();
+ }
+ }
+ // still no message? use an empty string for backwards compatibility
+ if (null == result) {
+ result = "";
+ }
+ return result;
+ }
+
+ // TODO replace this with a helper method elsewhere?
+ public static ErrorMessage getErrorMessageForException(Throwable t) {
+ if (null == t) {
+ return null;
+ } else if (t instanceof ErrorMessage) {
+ // legacy case for custom error messages
+ return (ErrorMessage) t;
+ } else if (t instanceof Validator.InvalidValueException) {
+ UserError error = new UserError(
+ ((Validator.InvalidValueException) t).getHtmlMessage(),
+ ContentMode.XHTML, ErrorLevel.ERROR);
+ for (Validator.InvalidValueException nestedException : ((Validator.InvalidValueException) t)
+ .getCauses()) {
+ error.addCause(getErrorMessageForException(nestedException));
+ }
+ return error;
+ } else if (t instanceof Buffered.SourceException) {
+ // no message, only the causes to be painted
+ UserError error = new UserError(null);
+ // in practice, this was always ERROR in Vaadin 6 unless tweaked in
+ // custom exceptions implementing ErrorMessage
+ error.setErrorLevel(ErrorLevel.ERROR);
+ // causes
+ for (Throwable nestedException : ((Buffered.SourceException) t)
+ .getCauses()) {
+ error.addCause(getErrorMessageForException(nestedException));
+ }
+ return error;
+ } else {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ t.printStackTrace(pw);
+ return new SystemError(sw.toString());
+ }
+ }
+
+ /* Documented in superclass */
+ @Override
+ public String toString() {
+ return getMessage();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/AbstractExtension.java b/server/src/com/vaadin/terminal/AbstractExtension.java
new file mode 100644
index 0000000000..33a60e39ef
--- /dev/null
+++ b/server/src/com/vaadin/terminal/AbstractExtension.java
@@ -0,0 +1,76 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import com.vaadin.terminal.gwt.server.ClientConnector;
+
+/**
+ * An extension is an entity that is attached to a Component or another
+ * Extension and independently communicates between client and server.
+ * <p>
+ * Extensions can use shared state and RPC in the same way as components.
+ * <p>
+ * AbstractExtension adds a mechanism for adding the extension to any Connector
+ * (extend). To let the Extension determine what kind target it can be added to,
+ * the extend method is declared as protected.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public abstract class AbstractExtension extends AbstractClientConnector
+ implements Extension {
+ private boolean previouslyAttached = false;
+
+ /**
+ * Gets a type that the parent must be an instance of. Override this if the
+ * extension only support certain targets, e.g. if only TextFields can be
+ * extended.
+ *
+ * @return a type that the parent must be an instance of
+ */
+ protected Class<? extends ClientConnector> getSupportedParentType() {
+ return ClientConnector.class;
+ }
+
+ /**
+ * Add this extension to the target connector. This method is protected to
+ * allow subclasses to require a more specific type of target.
+ *
+ * @param target
+ * the connector to attach this extension to
+ */
+ protected void extend(AbstractClientConnector target) {
+ target.addExtension(this);
+ }
+
+ /**
+ * Remove this extension from its target. After an extension has been
+ * removed, it can not be attached again.
+ */
+ public void removeFromTarget() {
+ getParent().removeExtension(this);
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ if (previouslyAttached && parent != null) {
+ throw new IllegalStateException(
+ "An extension can not be set to extend a new target after getting detached from the previous.");
+ }
+
+ Class<? extends ClientConnector> supportedParentType = getSupportedParentType();
+ if (parent == null || supportedParentType.isInstance(parent)) {
+ super.setParent(parent);
+ previouslyAttached = true;
+ } else {
+ throw new IllegalArgumentException(getClass().getName()
+ + " can only be attached to targets of type "
+ + supportedParentType.getName() + " but attach to "
+ + parent.getClass().getName() + " was attempted.");
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java
new file mode 100644
index 0000000000..7bafb6d2b3
--- /dev/null
+++ b/server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java
@@ -0,0 +1,162 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.ui.JavaScriptFunction;
+
+/**
+ * Base class for Extensions with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript extension is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * extension. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyExtension</code> for the
+ * server-side
+ * <code>com.example.MyExtension extends AbstractJavaScriptExtension</code>
+ * class. If MyExtension instead extends <code>com.example.SuperExtension</code>
+ * , then <code>com_example_SuperExtension</code> will also be attempted if
+ * <code>com_example_MyExtension</code> has not been defined.
+ * <p>
+ *
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin with the following
+ * functions:
+ * <ul>
+ * <li><code>getConnectorId()</code> - returns a string with the id of the
+ * connector.</li>
+ * <li><code>getParentId([connectorId])</code> - returns a string with the id of
+ * the connector's parent. If <code>connectorId</code> is provided, the id of
+ * the parent of the corresponding connector with the passed id is returned
+ * instead.</li>
+ * <li><code>getElement([connectorId])</code> - returns the DOM Element that is
+ * the root of a connector's widget. <code>null</code> is returned if the
+ * connector can not be found or if the connector doesn't have a widget. If
+ * <code>connectorId</code> is not provided, the connector id of the current
+ * connector will be used.</li>
+ * <li><code>getState()</code> - returns an object corresponding to the shared
+ * state defined on the server. The scheme for conversion between Java and
+ * JavaScript types is described bellow.</li>
+ * <li><code>registerRpc([name, ] rpcObject)</code> - registers the
+ * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an
+ * object with field containing functions for all eligible RPC functions. If
+ * <code>name</code> is provided, the RPC handler will only used for RPC calls
+ * for the RPC interface with the same fully qualified Java name. If no
+ * <code>name</code> is provided, the RPC handler will be used for all incoming
+ * RPC invocations where the RPC method name is defined as a function field in
+ * the handler. The scheme for conversion between Java types in the RPC
+ * interface definition and the JavaScript values passed as arguments to the
+ * handler functions is described bellow.</li>
+ * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If
+ * <code>name</code> is provided, the proxy object will contain functions for
+ * all methods in the RPC interface with the same fully qualified name, provided
+ * a RPC handler has been registered by the server-side code. If no
+ * <code>name</code> is provided, the returned RPC proxy object will contain
+ * functions for all methods in all RPC interfaces registered for the connector
+ * on the server. If the same method name is present in multiple registered RPC
+ * interfaces, the corresponding function in the RPC proxy object will throw an
+ * exception when called. The scheme for conversion between Java types in the
+ * RPC interface and the JavaScript values that should be passed to the
+ * functions is described bellow.</li>
+ * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL
+ * that can be used in the browser. This is just way of accessing
+ * {@link ApplicationConnection#translateVaadinUri(String)}</li>
+ * </ul>
+ * The connector wrapper also supports these special functions:
+ * <ul>
+ * <li><code>onStateChange</code> - If the JavaScript code assigns a function to
+ * the field, that function is called whenever the contents of the shared state
+ * is changed.</li>
+ * <li>Any field name corresponding to a call to
+ * {@link #addFunction(String, JavaScriptFunction)} on the server will
+ * automatically be present as a function that triggers the registered function
+ * on the server.</li>
+ * <li>Any field name referred to using
+ * {@link #callFunction(String, Object...)} on the server will be called if a
+ * function has been assigned to the field.</li>
+ * </ul>
+ * <p>
+ *
+ * Values in the Shared State and in RPC calls are converted between Java and
+ * JavaScript using the following conventions:
+ * <ul>
+ * <li>Primitive Java numbers (byte, char, int, long, float, double) and their
+ * boxed types (Byte, Character, Integer, Long, Float, Double) are represented
+ * by JavaScript numbers.</li>
+ * <li>The primitive Java boolean and the boxed Boolean are represented by
+ * JavaScript booleans.</li>
+ * <li>Java Strings are represented by JavaScript strings.</li>
+ * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
+ * <li>Map<String, ?> in Java is represented by JavaScript object with fields
+ * corresponding to the map keys.</li>
+ * <li>Any other Java Map is represented by a JavaScript array containing two
+ * arrays, the first contains the keys and the second contains the values in the
+ * same order.</li>
+ * <li>A Java Bean is represented by a JavaScript object with fields
+ * corresponding to the bean's properties.</li>
+ * <li>A Java Connector is represented by a JavaScript string containing the
+ * connector's id.</li>
+ * <li>A pluggable serialization mechanism is provided for types not described
+ * here. Please refer to the documentation for specific types for serialization
+ * information.</li>
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public abstract class AbstractJavaScriptExtension extends AbstractExtension {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ @Override
+ protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side callback
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName, JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments should only contain
+ * data types that can be represented in JavaScript including primitives,
+ * their boxed types, arrays, String, List, Set, Map, Connector and
+ * JavaBeans.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ public JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
diff --git a/server/src/com/vaadin/terminal/ApplicationResource.java b/server/src/com/vaadin/terminal/ApplicationResource.java
new file mode 100644
index 0000000000..da92642d02
--- /dev/null
+++ b/server/src/com/vaadin/terminal/ApplicationResource.java
@@ -0,0 +1,75 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+import com.vaadin.Application;
+
+/**
+ * This interface must be implemented by classes wishing to provide Application
+ * resources.
+ * <p>
+ * <code>ApplicationResource</code> are a set of named resources (pictures,
+ * sounds, etc) associated with some specific application. Having named
+ * application resources provides a convenient method for having inter-theme
+ * common resources for an application.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ApplicationResource extends Resource, Serializable {
+
+ /**
+ * Default cache time.
+ */
+ public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24;
+
+ /**
+ * Gets resource as stream.
+ */
+ public DownloadStream getStream();
+
+ /**
+ * Gets the application of the resource.
+ */
+ public Application getApplication();
+
+ /**
+ * Gets the virtual filename for this resource.
+ *
+ * @return the file name associated to this resource.
+ */
+ public String getFilename();
+
+ /**
+ * Gets the length of cache expiration time.
+ *
+ * <p>
+ * This gives the adapter the possibility cache streams sent to the client.
+ * The caching may be made in adapter or at the client if the client
+ * supports caching. Default is <code>DEFAULT_CACHETIME</code>.
+ * </p>
+ *
+ * @return Cache time in milliseconds
+ */
+ public long getCacheTime();
+
+ /**
+ * Gets the size of the download buffer used for this resource.
+ *
+ * <p>
+ * If the buffer size is 0, the buffer size is decided by the terminal
+ * adapter. The default value is 0.
+ * </p>
+ *
+ * @return int the size of the buffer in bytes.
+ */
+ public int getBufferSize();
+
+}
diff --git a/server/src/com/vaadin/terminal/ClassResource.java b/server/src/com/vaadin/terminal/ClassResource.java
new file mode 100644
index 0000000000..b74c8e7bb7
--- /dev/null
+++ b/server/src/com/vaadin/terminal/ClassResource.java
@@ -0,0 +1,178 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+import com.vaadin.Application;
+import com.vaadin.service.FileTypeResolver;
+
+/**
+ * <code>ClassResource</code> is a named resource accessed with the class
+ * loader.
+ *
+ * This can be used to access resources such as icons, files, etc.
+ *
+ * @see java.lang.Class#getResource(java.lang.String)
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ClassResource implements ApplicationResource, Serializable {
+
+ /**
+ * Default buffer size for this stream resource.
+ */
+ private int bufferSize = 0;
+
+ /**
+ * Default cache time for this stream resource.
+ */
+ private long cacheTime = DEFAULT_CACHETIME;
+
+ /**
+ * Associated class used for indetifying the source of the resource.
+ */
+ private final Class<?> associatedClass;
+
+ /**
+ * Name of the resource is relative to the associated class.
+ */
+ private final String resourceName;
+
+ /**
+ * Application used for serving the class.
+ */
+ private final Application application;
+
+ /**
+ * Creates a new application resource instance. The resource id is relative
+ * to the location of the application class.
+ *
+ * @param resourceName
+ * the Unique identifier of the resource within the application.
+ * @param application
+ * the application this resource will be added to.
+ */
+ public ClassResource(String resourceName, Application application) {
+ this(application.getClass(), resourceName, application);
+ }
+
+ /**
+ * Creates a new application resource instance.
+ *
+ * @param associatedClass
+ * the class of the which the resource is associated.
+ * @param resourceName
+ * the Unique identifier of the resource within the application.
+ * @param application
+ * the application this resource will be added to.
+ */
+ public ClassResource(Class<?> associatedClass, String resourceName,
+ Application application) {
+ this.associatedClass = associatedClass;
+ this.resourceName = resourceName;
+ this.application = application;
+ if (resourceName == null || associatedClass == null) {
+ throw new NullPointerException();
+ }
+ application.addResource(this);
+ }
+
+ /**
+ * Gets the MIME type of this resource.
+ *
+ * @see com.vaadin.terminal.Resource#getMIMEType()
+ */
+ @Override
+ public String getMIMEType() {
+ return FileTypeResolver.getMIMEType(resourceName);
+ }
+
+ /**
+ * Gets the application of this resource.
+ *
+ * @see com.vaadin.terminal.ApplicationResource#getApplication()
+ */
+ @Override
+ public Application getApplication() {
+ return application;
+ }
+
+ /**
+ * Gets the virtual filename for this resource.
+ *
+ * @return the file name associated to this resource.
+ * @see com.vaadin.terminal.ApplicationResource#getFilename()
+ */
+ @Override
+ public String getFilename() {
+ int index = 0;
+ int next = 0;
+ while ((next = resourceName.indexOf('/', index)) > 0
+ && next + 1 < resourceName.length()) {
+ index = next + 1;
+ }
+ return resourceName.substring(index);
+ }
+
+ /**
+ * Gets resource as stream.
+ *
+ * @see com.vaadin.terminal.ApplicationResource#getStream()
+ */
+ @Override
+ public DownloadStream getStream() {
+ final DownloadStream ds = new DownloadStream(
+ associatedClass.getResourceAsStream(resourceName),
+ getMIMEType(), getFilename());
+ ds.setBufferSize(getBufferSize());
+ ds.setCacheTime(cacheTime);
+ return ds;
+ }
+
+ /* documented in superclass */
+ @Override
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ /**
+ * Sets the size of the download buffer used for this resource.
+ *
+ * @param bufferSize
+ * the size of the buffer in bytes.
+ */
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ /* documented in superclass */
+ @Override
+ public long getCacheTime() {
+ return cacheTime;
+ }
+
+ /**
+ * Sets the length of cache expiration time.
+ *
+ * <p>
+ * This gives the adapter the possibility cache streams sent to the client.
+ * The caching may be made in adapter or at the client if the client
+ * supports caching. Zero or negavive value disbales the caching of this
+ * stream.
+ * </p>
+ *
+ * @param cacheTime
+ * the cache time in milliseconds.
+ *
+ */
+ public void setCacheTime(long cacheTime) {
+ this.cacheTime = cacheTime;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/CombinedRequest.java b/server/src/com/vaadin/terminal/CombinedRequest.java
new file mode 100644
index 0000000000..5b92feb39a
--- /dev/null
+++ b/server/src/com/vaadin/terminal/CombinedRequest.java
@@ -0,0 +1,187 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+import com.vaadin.Application;
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.external.json.JSONObject;
+import com.vaadin.terminal.gwt.server.WebApplicationContext;
+import com.vaadin.terminal.gwt.server.WebBrowser;
+
+/**
+ * A {@link WrappedRequest} with path and parameters from one request and
+ * {@link WrappedRequest.BrowserDetails} extracted from another request.
+ *
+ * This class is intended to be used for a two request initialization where the
+ * first request fetches the actual application page and the second request
+ * contains information extracted from the browser using javascript.
+ *
+ */
+public class CombinedRequest implements WrappedRequest {
+
+ private final WrappedRequest secondRequest;
+ private Map<String, String[]> parameterMap;
+
+ /**
+ * Creates a new combined request based on the second request and some
+ * details from the first request.
+ *
+ * @param secondRequest
+ * the second request which will be used as the foundation of the
+ * combined request
+ * @throws JSONException
+ * if the initialParams parameter can not be decoded
+ */
+ public CombinedRequest(WrappedRequest secondRequest) throws JSONException {
+ this.secondRequest = secondRequest;
+
+ HashMap<String, String[]> map = new HashMap<String, String[]>();
+ JSONObject initialParams = new JSONObject(
+ secondRequest.getParameter("initialParams"));
+ for (Iterator<?> keys = initialParams.keys(); keys.hasNext();) {
+ String name = (String) keys.next();
+ JSONArray jsonValues = initialParams.getJSONArray(name);
+ String[] values = new String[jsonValues.length()];
+ for (int i = 0; i < values.length; i++) {
+ values[i] = jsonValues.getString(i);
+ }
+ map.put(name, values);
+ }
+
+ parameterMap = Collections.unmodifiableMap(map);
+
+ }
+
+ @Override
+ public String getParameter(String parameter) {
+ String[] strings = getParameterMap().get(parameter);
+ if (strings == null || strings.length == 0) {
+ return null;
+ } else {
+ return strings[0];
+ }
+ }
+
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ return parameterMap;
+ }
+
+ @Override
+ public int getContentLength() {
+ return secondRequest.getContentLength();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return secondRequest.getInputStream();
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return secondRequest.getAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ secondRequest.setAttribute(name, value);
+ }
+
+ @Override
+ public String getRequestPathInfo() {
+ return secondRequest.getParameter("initialPath");
+ }
+
+ @Override
+ public int getSessionMaxInactiveInterval() {
+ return secondRequest.getSessionMaxInactiveInterval();
+ }
+
+ @Override
+ public Object getSessionAttribute(String name) {
+ return secondRequest.getSessionAttribute(name);
+ }
+
+ @Override
+ public void setSessionAttribute(String name, Object attribute) {
+ secondRequest.setSessionAttribute(name, attribute);
+ }
+
+ @Override
+ public String getContentType() {
+ return secondRequest.getContentType();
+ }
+
+ @Override
+ public BrowserDetails getBrowserDetails() {
+ return new BrowserDetails() {
+ @Override
+ public String getUriFragment() {
+ String fragment = secondRequest.getParameter("fr");
+ if (fragment == null) {
+ return "";
+ } else {
+ return fragment;
+ }
+ }
+
+ @Override
+ public String getWindowName() {
+ return secondRequest.getParameter("wn");
+ }
+
+ @Override
+ public WebBrowser getWebBrowser() {
+ WebApplicationContext context = (WebApplicationContext) Application
+ .getCurrent().getContext();
+ return context.getBrowser();
+ }
+ };
+ }
+
+ /**
+ * Gets the original second request. This can be used e.g. if a request
+ * parameter from the second request is required.
+ *
+ * @return the original second wrapped request
+ */
+ public WrappedRequest getSecondRequest() {
+ return secondRequest;
+ }
+
+ @Override
+ public Locale getLocale() {
+ return secondRequest.getLocale();
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return secondRequest.getRemoteAddr();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return secondRequest.isSecure();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return secondRequest.getHeader(name);
+ }
+
+ @Override
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return secondRequest.getDeploymentConfiguration();
+ }
+}
diff --git a/server/src/com/vaadin/terminal/CompositeErrorMessage.java b/server/src/com/vaadin/terminal/CompositeErrorMessage.java
new file mode 100644
index 0000000000..b82b622f54
--- /dev/null
+++ b/server/src/com/vaadin/terminal/CompositeErrorMessage.java
@@ -0,0 +1,112 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * Class for combining multiple error messages together.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class CompositeErrorMessage extends AbstractErrorMessage {
+
+ /**
+ * Constructor for CompositeErrorMessage.
+ *
+ * @param errorMessages
+ * the Array of error messages that are listed togeter. Nulls are
+ * ignored, but at least one message is required.
+ */
+ public CompositeErrorMessage(ErrorMessage[] errorMessages) {
+ super(null);
+ setErrorLevel(ErrorLevel.INFORMATION);
+
+ for (int i = 0; i < errorMessages.length; i++) {
+ addErrorMessage(errorMessages[i]);
+ }
+
+ if (getCauses().size() == 0) {
+ throw new IllegalArgumentException(
+ "Composite error message must have at least one error");
+ }
+
+ }
+
+ /**
+ * Constructor for CompositeErrorMessage.
+ *
+ * @param errorMessages
+ * the Collection of error messages that are listed together. At
+ * least one message is required.
+ */
+ public CompositeErrorMessage(
+ Collection<? extends ErrorMessage> errorMessages) {
+ super(null);
+ setErrorLevel(ErrorLevel.INFORMATION);
+
+ for (final Iterator<? extends ErrorMessage> i = errorMessages
+ .iterator(); i.hasNext();) {
+ addErrorMessage(i.next());
+ }
+
+ if (getCauses().size() == 0) {
+ throw new IllegalArgumentException(
+ "Composite error message must have at least one error");
+ }
+ }
+
+ /**
+ * Adds a error message into this composite message. Updates the level
+ * field.
+ *
+ * @param error
+ * the error message to be added. Duplicate errors are ignored.
+ */
+ private void addErrorMessage(ErrorMessage error) {
+ if (error != null && !getCauses().contains(error)) {
+ addCause(error);
+ if (error.getErrorLevel().intValue() > getErrorLevel().intValue()) {
+ setErrorLevel(error.getErrorLevel());
+ }
+ }
+ }
+
+ /**
+ * Gets Error Iterator.
+ *
+ * @return the error iterator.
+ */
+ public Iterator<ErrorMessage> iterator() {
+ return getCauses().iterator();
+ }
+
+ /**
+ * Returns a comma separated list of the error messages.
+ *
+ * @return String, comma separated list of error messages.
+ */
+ @Override
+ public String toString() {
+ String retval = "[";
+ int pos = 0;
+ for (final Iterator<ErrorMessage> i = getCauses().iterator(); i
+ .hasNext();) {
+ if (pos > 0) {
+ retval += ",";
+ }
+ pos++;
+ retval += i.next().toString();
+ }
+ retval += "]";
+
+ return retval;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/DeploymentConfiguration.java b/server/src/com/vaadin/terminal/DeploymentConfiguration.java
new file mode 100644
index 0000000000..ae96dcaec5
--- /dev/null
+++ b/server/src/com/vaadin/terminal/DeploymentConfiguration.java
@@ -0,0 +1,123 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.util.Iterator;
+import java.util.Properties;
+
+import javax.portlet.PortletContext;
+import javax.servlet.ServletContext;
+
+import com.vaadin.terminal.gwt.server.AddonContext;
+import com.vaadin.terminal.gwt.server.AddonContextListener;
+
+/**
+ * Provide deployment specific settings that are required outside terminal
+ * specific code.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @since 7.0
+ */
+public interface DeploymentConfiguration extends Serializable {
+
+ /**
+ * Gets the base URL of the location of Vaadin's static files.
+ *
+ * @param request
+ * the request for which the location should be determined
+ *
+ * @return a string with the base URL for static files
+ */
+ public String getStaticFileLocation(WrappedRequest request);
+
+ /**
+ * Gets the widgetset that is configured for this deployment, e.g. from a
+ * parameter in web.xml.
+ *
+ * @param request
+ * the request for which a widgetset is required
+ * @return the name of the widgetset
+ */
+ public String getConfiguredWidgetset(WrappedRequest request);
+
+ /**
+ * Gets the theme that is configured for this deployment, e.g. from a portal
+ * parameter or just some sensible default value.
+ *
+ * @param request
+ * the request for which a theme is required
+ * @return the name of the theme
+ */
+ public String getConfiguredTheme(WrappedRequest request);
+
+ /**
+ * Checks whether the Vaadin application will be rendered on its own in the
+ * browser or whether it will be included into some other context. A
+ * standalone application may do things that might interfere with other
+ * parts of a page, e.g. changing the page title and requesting focus upon
+ * loading.
+ *
+ * @param request
+ * the request for which the application is loaded
+ * @return a boolean indicating whether the application should be standalone
+ */
+ public boolean isStandalone(WrappedRequest request);
+
+ /**
+ * Gets a configured property. The properties are typically read from e.g.
+ * web.xml or from system properties of the JVM.
+ *
+ * @param propertyName
+ * The simple of the property, in some contexts, lookup might be
+ * performed using variations of the provided name.
+ * @param defaultValue
+ * the default value that should be used if no value has been
+ * defined
+ * @return the property value, or the passed default value if no property
+ * value is found
+ */
+ public String getApplicationOrSystemProperty(String propertyName,
+ String defaultValue);
+
+ /**
+ * Get the class loader to use for loading classes loaded by name, e.g.
+ * custom Root classes. <code>null</code> indicates that the default class
+ * loader should be used.
+ *
+ * @return the class loader to use, or <code>null</code>
+ */
+ public ClassLoader getClassLoader();
+
+ /**
+ * Returns the MIME type of the specified file, or null if the MIME type is
+ * not known. The MIME type is determined by the configuration of the
+ * container, and may be specified in a deployment descriptor. Common MIME
+ * types are "text/html" and "image/gif".
+ *
+ * @param resourceName
+ * a String specifying the name of a file
+ * @return a String specifying the file's MIME type
+ *
+ * @see ServletContext#getMimeType(String)
+ * @see PortletContext#getMimeType(String)
+ */
+ public String getMimeType(String resourceName);
+
+ /**
+ * Gets the properties configured for the deployment, e.g. as init
+ * parameters to the servlet or portlet.
+ *
+ * @return properties for the application.
+ */
+ public Properties getInitParameters();
+
+ public Iterator<AddonContextListener> getAddonContextListeners();
+
+ public AddonContext getAddonContext();
+
+ public void setAddonContext(AddonContext vaadinContext);
+}
diff --git a/server/src/com/vaadin/terminal/DownloadStream.java b/server/src/com/vaadin/terminal/DownloadStream.java
new file mode 100644
index 0000000000..9853b0eee2
--- /dev/null
+++ b/server/src/com/vaadin/terminal/DownloadStream.java
@@ -0,0 +1,335 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.vaadin.terminal.gwt.server.Constants;
+
+/**
+ * Downloadable stream.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class DownloadStream implements Serializable {
+
+ /**
+ * Maximum cache time.
+ */
+ public static final long MAX_CACHETIME = Long.MAX_VALUE;
+
+ /**
+ * Default cache time.
+ */
+ public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24;
+
+ private InputStream stream;
+
+ private String contentType;
+
+ private String fileName;
+
+ private Map<String, String> params;
+
+ private long cacheTime = DEFAULT_CACHETIME;
+
+ private int bufferSize = 0;
+
+ /**
+ * Creates a new instance of DownloadStream.
+ */
+ public DownloadStream(InputStream stream, String contentType,
+ String fileName) {
+ setStream(stream);
+ setContentType(contentType);
+ setFileName(fileName);
+ }
+
+ /**
+ * Gets downloadable stream.
+ *
+ * @return output stream.
+ */
+ public InputStream getStream() {
+ return stream;
+ }
+
+ /**
+ * Sets the stream.
+ *
+ * @param stream
+ * The stream to set
+ */
+ public void setStream(InputStream stream) {
+ this.stream = stream;
+ }
+
+ /**
+ * Gets stream content type.
+ *
+ * @return type of the stream content.
+ */
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Sets stream content type.
+ *
+ * @param contentType
+ * the contentType to set
+ */
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * Returns the file name.
+ *
+ * @return the name of the file.
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ /**
+ * Sets the file name.
+ *
+ * @param fileName
+ * the file name to set.
+ */
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ /**
+ * Sets a paramater for download stream. Parameters are optional information
+ * about the downloadable stream and their meaning depends on the used
+ * adapter. For example in WebAdapter they are interpreted as HTTP response
+ * headers.
+ *
+ * If the parameters by this name exists, the old value is replaced.
+ *
+ * @param name
+ * the Name of the parameter to set.
+ * @param value
+ * the Value of the parameter to set.
+ */
+ public void setParameter(String name, String value) {
+ if (params == null) {
+ params = new HashMap<String, String>();
+ }
+ params.put(name, value);
+ }
+
+ /**
+ * Gets a paramater for download stream. Parameters are optional information
+ * about the downloadable stream and their meaning depends on the used
+ * adapter. For example in WebAdapter they are interpreted as HTTP response
+ * headers.
+ *
+ * @param name
+ * the Name of the parameter to set.
+ * @return Value of the parameter or null if the parameter does not exist.
+ */
+ public String getParameter(String name) {
+ if (params != null) {
+ return params.get(name);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the names of the parameters.
+ *
+ * @return Iterator of names or null if no parameters are set.
+ */
+ public Iterator<String> getParameterNames() {
+ if (params != null) {
+ return params.keySet().iterator();
+ }
+ return null;
+ }
+
+ /**
+ * Gets length of cache expiration time. This gives the adapter the
+ * possibility cache streams sent to the client. The caching may be made in
+ * adapter or at the client if the client supports caching. Default is
+ * <code>DEFAULT_CACHETIME</code>.
+ *
+ * @return Cache time in milliseconds
+ */
+ public long getCacheTime() {
+ return cacheTime;
+ }
+
+ /**
+ * Sets length of cache expiration time. This gives the adapter the
+ * possibility cache streams sent to the client. The caching may be made in
+ * adapter or at the client if the client supports caching. Zero or negavive
+ * value disbales the caching of this stream.
+ *
+ * @param cacheTime
+ * the cache time in milliseconds.
+ */
+ public void setCacheTime(long cacheTime) {
+ this.cacheTime = cacheTime;
+ }
+
+ /**
+ * Gets the size of the download buffer.
+ *
+ * @return int The size of the buffer in bytes.
+ */
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ /**
+ * Sets the size of the download buffer.
+ *
+ * @param bufferSize
+ * the size of the buffer in bytes.
+ *
+ * @since 7.0
+ */
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ /**
+ * Writes this download stream to a wrapped response. This takes care of
+ * setting response headers according to what is defined in this download
+ * stream ({@link #getContentType()}, {@link #getCacheTime()},
+ * {@link #getFileName()}) and transferring the data from the stream (
+ * {@link #getStream()}) to the response. Defined parameters (
+ * {@link #getParameterNames()}) are also included as headers in the
+ * response. If there's is a parameter named <code>Location</code>, a
+ * redirect (302 Moved temporarily) is sent instead of the contents of this
+ * stream.
+ *
+ * @param response
+ * the wrapped response to write this download stream to
+ * @throws IOException
+ * passed through from the wrapped response
+ *
+ * @since 7.0
+ */
+ public void writeTo(WrappedResponse response) throws IOException {
+ if (getParameter("Location") != null) {
+ response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+ response.setHeader("Location", getParameter("Location"));
+ return;
+ }
+
+ // Download from given stream
+ final InputStream data = getStream();
+ if (data != null) {
+
+ OutputStream out = null;
+ try {
+ // Sets content type
+ response.setContentType(getContentType());
+
+ // Sets cache headers
+ response.setCacheTime(getCacheTime());
+
+ // Copy download stream parameters directly
+ // to HTTP headers.
+ final Iterator<String> i = getParameterNames();
+ if (i != null) {
+ while (i.hasNext()) {
+ final String param = i.next();
+ response.setHeader(param, getParameter(param));
+ }
+ }
+
+ // suggest local filename from DownloadStream if
+ // Content-Disposition
+ // not explicitly set
+ String contentDispositionValue = getParameter("Content-Disposition");
+ if (contentDispositionValue == null) {
+ contentDispositionValue = "filename=\"" + getFileName()
+ + "\"";
+ response.setHeader("Content-Disposition",
+ contentDispositionValue);
+ }
+
+ int bufferSize = getBufferSize();
+ if (bufferSize <= 0 || bufferSize > Constants.MAX_BUFFER_SIZE) {
+ bufferSize = Constants.DEFAULT_BUFFER_SIZE;
+ }
+ final byte[] buffer = new byte[bufferSize];
+ int bytesRead = 0;
+
+ out = response.getOutputStream();
+
+ long totalWritten = 0;
+ while ((bytesRead = data.read(buffer)) > 0) {
+ out.write(buffer, 0, bytesRead);
+
+ totalWritten += bytesRead;
+ if (totalWritten >= buffer.length) {
+ // Avoid chunked encoding for small resources
+ out.flush();
+ }
+ }
+ } finally {
+ tryToCloseStream(out);
+ tryToCloseStream(data);
+ }
+ }
+ }
+
+ /**
+ * Helper method that tries to close an output stream and ignores any
+ * exceptions.
+ *
+ * @param out
+ * the output stream to close, <code>null</code> is also
+ * supported
+ */
+ static void tryToCloseStream(OutputStream out) {
+ try {
+ // try to close output stream (e.g. file handle)
+ if (out != null) {
+ out.close();
+ }
+ } catch (IOException e1) {
+ // NOP
+ }
+ }
+
+ /**
+ * Helper method that tries to close an input stream and ignores any
+ * exceptions.
+ *
+ * @param in
+ * the input stream to close, <code>null</code> is also supported
+ */
+ static void tryToCloseStream(InputStream in) {
+ try {
+ // try to close output stream (e.g. file handle)
+ if (in != null) {
+ in.close();
+ }
+ } catch (IOException e1) {
+ // NOP
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/ErrorMessage.java b/server/src/com/vaadin/terminal/ErrorMessage.java
new file mode 100644
index 0000000000..60a0780a72
--- /dev/null
+++ b/server/src/com/vaadin/terminal/ErrorMessage.java
@@ -0,0 +1,126 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+/**
+ * Interface for rendering error messages to terminal. All the visible errors
+ * shown to user must implement this interface.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ErrorMessage extends Serializable {
+
+ public enum ErrorLevel {
+ /**
+ * Error code for informational messages.
+ */
+ INFORMATION("info", 0),
+ /**
+ * Error code for warning messages.
+ */
+ WARNING("warning", 1),
+ /**
+ * Error code for regular error messages.
+ */
+ ERROR("error", 2),
+ /**
+ * Error code for critical error messages.
+ */
+ CRITICAL("critical", 3),
+ /**
+ * Error code for system errors and bugs.
+ */
+ SYSTEMERROR("system", 4);
+
+ String text;
+ int errorLevel;
+
+ private ErrorLevel(String text, int errorLevel) {
+ this.text = text;
+ this.errorLevel = errorLevel;
+ }
+
+ /**
+ * Textual representation for server-client communication of level
+ *
+ * @return String for error severity
+ */
+ public String getText() {
+ return text;
+ }
+
+ /**
+ * Integer representation of error severity for comparison
+ *
+ * @return integer for error severity
+ */
+ public int intValue() {
+ return errorLevel;
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link ErrorLevel#SYSTEMERROR} instead    
+ */
+ @Deprecated
+ public static final ErrorLevel SYSTEMERROR = ErrorLevel.SYSTEMERROR;
+
+ /**
+ * @deprecated from 7.0, use {@link ErrorLevel#CRITICAL} instead    
+ */
+ @Deprecated
+ public static final ErrorLevel CRITICAL = ErrorLevel.CRITICAL;
+
+ /**
+ * @deprecated from 7.0, use {@link ErrorLevel#ERROR} instead    
+ */
+
+ @Deprecated
+ public static final ErrorLevel ERROR = ErrorLevel.ERROR;
+
+ /**
+ * @deprecated from 7.0, use {@link ErrorLevel#WARNING} instead    
+ */
+ @Deprecated
+ public static final ErrorLevel WARNING = ErrorLevel.WARNING;
+
+ /**
+ * @deprecated from 7.0, use {@link ErrorLevel#INFORMATION} instead    
+ */
+ @Deprecated
+ public static final ErrorLevel INFORMATION = ErrorLevel.INFORMATION;
+
+ /**
+ * Gets the errors level.
+ *
+ * @return the level of error as an integer.
+ */
+ public ErrorLevel getErrorLevel();
+
+ /**
+ * Returns the HTML formatted message to show in as the error message on the
+ * client.
+ *
+ * This method should perform any necessary escaping to avoid XSS attacks.
+ *
+ * TODO this API may still change to use a separate data transfer object
+ *
+ * @return HTML formatted string for the error message
+ * @since 7.0
+ */
+ public String getFormattedHtmlMessage();
+
+}
diff --git a/server/src/com/vaadin/terminal/Extension.java b/server/src/com/vaadin/terminal/Extension.java
new file mode 100644
index 0000000000..ef5bb4cf8d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Extension.java
@@ -0,0 +1,27 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import com.vaadin.terminal.gwt.server.ClientConnector;
+
+/**
+ * An extension is an entity that is attached to a Component or another
+ * Extension and independently communicates between client and server.
+ * <p>
+ * An extension can only be attached once. It is not supported to move an
+ * extension from one target to another.
+ * <p>
+ * Extensions can use shared state and RPC in the same way as components.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public interface Extension extends ClientConnector {
+ /*
+ * Currently just an empty marker interface to distinguish between
+ * extensions and other connectors, e.g. components
+ */
+}
diff --git a/server/src/com/vaadin/terminal/ExternalResource.java b/server/src/com/vaadin/terminal/ExternalResource.java
new file mode 100644
index 0000000000..84fcc65a44
--- /dev/null
+++ b/server/src/com/vaadin/terminal/ExternalResource.java
@@ -0,0 +1,118 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.net.URL;
+
+import com.vaadin.service.FileTypeResolver;
+
+/**
+ * <code>ExternalResource</code> implements source for resources fetched from
+ * location specified by URL:s. The resources are fetched directly by the client
+ * terminal and are not fetched trough the terminal adapter.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ExternalResource implements Resource, Serializable {
+
+ /**
+ * Url of the download.
+ */
+ private String sourceURL = null;
+
+ /**
+ * MIME Type for the resource
+ */
+ private String mimeType = null;
+
+ /**
+ * Creates a new download component for downloading directly from given URL.
+ *
+ * @param sourceURL
+ * the source URL.
+ */
+ public ExternalResource(URL sourceURL) {
+ if (sourceURL == null) {
+ throw new RuntimeException("Source must be non-null");
+ }
+
+ this.sourceURL = sourceURL.toString();
+ }
+
+ /**
+ * Creates a new download component for downloading directly from given URL.
+ *
+ * @param sourceURL
+ * the source URL.
+ * @param mimeType
+ * the MIME Type
+ */
+ public ExternalResource(URL sourceURL, String mimeType) {
+ this(sourceURL);
+ this.mimeType = mimeType;
+ }
+
+ /**
+ * Creates a new download component for downloading directly from given URL.
+ *
+ * @param sourceURL
+ * the source URL.
+ */
+ public ExternalResource(String sourceURL) {
+ if (sourceURL == null) {
+ throw new RuntimeException("Source must be non-null");
+ }
+
+ this.sourceURL = sourceURL.toString();
+ }
+
+ /**
+ * Creates a new download component for downloading directly from given URL.
+ *
+ * @param sourceURL
+ * the source URL.
+ * @param mimeType
+ * the MIME Type
+ */
+ public ExternalResource(String sourceURL, String mimeType) {
+ this(sourceURL);
+ this.mimeType = mimeType;
+ }
+
+ /**
+ * Gets the URL of the external resource.
+ *
+ * @return the URL of the external resource.
+ */
+ public String getURL() {
+ return sourceURL;
+ }
+
+ /**
+ * Gets the MIME type of the resource.
+ *
+ * @see com.vaadin.terminal.Resource#getMIMEType()
+ */
+ @Override
+ public String getMIMEType() {
+ if (mimeType == null) {
+ mimeType = FileTypeResolver.getMIMEType(getURL().toString());
+ }
+ return mimeType;
+ }
+
+ /**
+ * Sets the MIME type of the resource.
+ */
+ public void setMIMEType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/FileResource.java b/server/src/com/vaadin/terminal/FileResource.java
new file mode 100644
index 0000000000..e3c9f0172a
--- /dev/null
+++ b/server/src/com/vaadin/terminal/FileResource.java
@@ -0,0 +1,174 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import com.vaadin.Application;
+import com.vaadin.service.FileTypeResolver;
+import com.vaadin.terminal.Terminal.ErrorEvent;
+
+/**
+ * <code>FileResources</code> are files or directories on local filesystem. The
+ * files and directories are served through URI:s to the client terminal and
+ * thus must be registered to an URI context before they can be used. The
+ * resource is automatically registered to the application when it is created.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class FileResource implements ApplicationResource {
+
+ /**
+ * Default buffer size for this stream resource.
+ */
+ private int bufferSize = 0;
+
+ /**
+ * File where the downloaded content is fetched from.
+ */
+ private File sourceFile;
+
+ /**
+ * Application.
+ */
+ private final Application application;
+
+ /**
+ * Default cache time for this stream resource.
+ */
+ private long cacheTime = DownloadStream.DEFAULT_CACHETIME;
+
+ /**
+ * Creates a new file resource for providing given file for client
+ * terminals.
+ */
+ public FileResource(File sourceFile, Application application) {
+ this.application = application;
+ setSourceFile(sourceFile);
+ application.addResource(this);
+ }
+
+ /**
+ * Gets the resource as stream.
+ *
+ * @see com.vaadin.terminal.ApplicationResource#getStream()
+ */
+ @Override
+ public DownloadStream getStream() {
+ try {
+ final DownloadStream ds = new DownloadStream(new FileInputStream(
+ sourceFile), getMIMEType(), getFilename());
+ ds.setParameter("Content-Length",
+ String.valueOf(sourceFile.length()));
+
+ ds.setCacheTime(cacheTime);
+ return ds;
+ } catch (final FileNotFoundException e) {
+ // Log the exception using the application error handler
+ getApplication().getErrorHandler().terminalError(new ErrorEvent() {
+
+ @Override
+ public Throwable getThrowable() {
+ return e;
+ }
+
+ });
+
+ return null;
+ }
+ }
+
+ /**
+ * Gets the source file.
+ *
+ * @return the source File.
+ */
+ public File getSourceFile() {
+ return sourceFile;
+ }
+
+ /**
+ * Sets the source file.
+ *
+ * @param sourceFile
+ * the source file to set.
+ */
+ public void setSourceFile(File sourceFile) {
+ this.sourceFile = sourceFile;
+ }
+
+ /**
+ * @see com.vaadin.terminal.ApplicationResource#getApplication()
+ */
+ @Override
+ public Application getApplication() {
+ return application;
+ }
+
+ /**
+ * @see com.vaadin.terminal.ApplicationResource#getFilename()
+ */
+ @Override
+ public String getFilename() {
+ return sourceFile.getName();
+ }
+
+ /**
+ * @see com.vaadin.terminal.Resource#getMIMEType()
+ */
+ @Override
+ public String getMIMEType() {
+ return FileTypeResolver.getMIMEType(sourceFile);
+ }
+
+ /**
+ * Gets the length of cache expiration time. This gives the adapter the
+ * possibility cache streams sent to the client. The caching may be made in
+ * adapter or at the client if the client supports caching. Default is
+ * <code>DownloadStream.DEFAULT_CACHETIME</code>.
+ *
+ * @return Cache time in milliseconds.
+ */
+ @Override
+ public long getCacheTime() {
+ return cacheTime;
+ }
+
+ /**
+ * Sets the length of cache expiration time. This gives the adapter the
+ * possibility cache streams sent to the client. The caching may be made in
+ * adapter or at the client if the client supports caching. Zero or negavive
+ * value disbales the caching of this stream.
+ *
+ * @param cacheTime
+ * the cache time in milliseconds.
+ */
+ public void setCacheTime(long cacheTime) {
+ this.cacheTime = cacheTime;
+ }
+
+ /* documented in superclass */
+ @Override
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ /**
+ * Sets the size of the download buffer used for this resource.
+ *
+ * @param bufferSize
+ * the size of the buffer in bytes.
+ */
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java
new file mode 100644
index 0000000000..265e578c6d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java
@@ -0,0 +1,116 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.shared.JavaScriptConnectorState;
+import com.vaadin.terminal.gwt.client.JavaScriptConnectorHelper;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.AbstractJavaScriptComponent;
+import com.vaadin.ui.JavaScript.JavaScriptCallbackRpc;
+import com.vaadin.ui.JavaScriptFunction;
+
+/**
+ * Internal helper class used to implement functionality common to
+ * {@link AbstractJavaScriptComponent} and {@link AbstractJavaScriptExtension}.
+ * Corresponding support in client-side code is in
+ * {@link JavaScriptConnectorHelper}.
+ * <p>
+ * You should most likely no use this class directly.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public class JavaScriptCallbackHelper implements Serializable {
+
+ private static final Method CALL_METHOD = ReflectTools.findMethod(
+ JavaScriptCallbackRpc.class, "call", String.class, JSONArray.class);
+ private AbstractClientConnector connector;
+
+ private Map<String, JavaScriptFunction> callbacks = new HashMap<String, JavaScriptFunction>();
+ private JavaScriptCallbackRpc javascriptCallbackRpc;
+
+ public JavaScriptCallbackHelper(AbstractClientConnector connector) {
+ this.connector = connector;
+ }
+
+ public void registerCallback(String functionName,
+ JavaScriptFunction javaScriptCallback) {
+ callbacks.put(functionName, javaScriptCallback);
+ JavaScriptConnectorState state = getConnectorState();
+ if (state.getCallbackNames().add(functionName)) {
+ connector.requestRepaint();
+ }
+ ensureRpc();
+ }
+
+ private JavaScriptConnectorState getConnectorState() {
+ JavaScriptConnectorState state = (JavaScriptConnectorState) connector
+ .getState();
+ return state;
+ }
+
+ private void ensureRpc() {
+ if (javascriptCallbackRpc == null) {
+ javascriptCallbackRpc = new JavaScriptCallbackRpc() {
+ @Override
+ public void call(String name, JSONArray arguments) {
+ JavaScriptFunction callback = callbacks.get(name);
+ try {
+ callback.call(arguments);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ };
+ connector.registerRpc(javascriptCallbackRpc);
+ }
+ }
+
+ public void invokeCallback(String name, Object... arguments) {
+ if (callbacks.containsKey(name)) {
+ throw new IllegalStateException(
+ "Can't call callback "
+ + name
+ + " on the client because a callback with the same name is registered on the server.");
+ }
+ JSONArray args = new JSONArray(Arrays.asList(arguments));
+ connector.addMethodInvocationToQueue(
+ JavaScriptCallbackRpc.class.getName(), CALL_METHOD,
+ new Object[] { name, args });
+ connector.requestRepaint();
+ }
+
+ public void registerRpc(Class<?> rpcInterfaceType) {
+ if (rpcInterfaceType == JavaScriptCallbackRpc.class) {
+ // Ignore
+ return;
+ }
+ Map<String, Set<String>> rpcInterfaces = getConnectorState()
+ .getRpcInterfaces();
+ String interfaceName = rpcInterfaceType.getName();
+ if (!rpcInterfaces.containsKey(interfaceName)) {
+ Set<String> methodNames = new HashSet<String>();
+
+ for (Method method : rpcInterfaceType.getMethods()) {
+ methodNames.add(method.getName());
+ }
+
+ rpcInterfaces.put(interfaceName, methodNames);
+ connector.requestRepaint();
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/KeyMapper.java b/server/src/com/vaadin/terminal/KeyMapper.java
new file mode 100644
index 0000000000..3f19692ef1
--- /dev/null
+++ b/server/src/com/vaadin/terminal/KeyMapper.java
@@ -0,0 +1,86 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+/**
+ * <code>KeyMapper</code> is the simple two-way map for generating textual keys
+ * for objects and retrieving the objects later with the key.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class KeyMapper<V> implements Serializable {
+
+ private int lastKey = 0;
+
+ private final HashMap<V, String> objectKeyMap = new HashMap<V, String>();
+
+ private final HashMap<String, V> keyObjectMap = new HashMap<String, V>();
+
+ /**
+ * Gets key for an object.
+ *
+ * @param o
+ * the object.
+ */
+ public String key(V o) {
+
+ if (o == null) {
+ return "null";
+ }
+
+ // If the object is already mapped, use existing key
+ String key = objectKeyMap.get(o);
+ if (key != null) {
+ return key;
+ }
+
+ // If the object is not yet mapped, map it
+ key = String.valueOf(++lastKey);
+ objectKeyMap.put(o, key);
+ keyObjectMap.put(key, o);
+
+ return key;
+ }
+
+ /**
+ * Retrieves object with the key.
+ *
+ * @param key
+ * the name with the desired value.
+ * @return the object with the key.
+ */
+ public V get(String key) {
+ return keyObjectMap.get(key);
+ }
+
+ /**
+ * Removes object from the mapper.
+ *
+ * @param removeobj
+ * the object to be removed.
+ */
+ public void remove(V removeobj) {
+ final String key = objectKeyMap.get(removeobj);
+
+ if (key != null) {
+ objectKeyMap.remove(removeobj);
+ keyObjectMap.remove(key);
+ }
+ }
+
+ /**
+ * Removes all objects from the mapper.
+ */
+ public void removeAll() {
+ objectKeyMap.clear();
+ keyObjectMap.clear();
+ }
+}
diff --git a/server/src/com/vaadin/terminal/LegacyPaint.java b/server/src/com/vaadin/terminal/LegacyPaint.java
new file mode 100644
index 0000000000..ea93e3db7f
--- /dev/null
+++ b/server/src/com/vaadin/terminal/LegacyPaint.java
@@ -0,0 +1,85 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.PaintTarget.PaintStatus;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HasComponents;
+
+public class LegacyPaint implements Serializable {
+ /**
+ *
+ * <p>
+ * Paints the Paintable into a UIDL stream. This method creates the UIDL
+ * sequence describing it and outputs it to the given UIDL stream.
+ * </p>
+ *
+ * <p>
+ * It is called when the contents of the component should be painted in
+ * response to the component first being shown or having been altered so
+ * that its visual representation is changed.
+ * </p>
+ *
+ * <p>
+ * <b>Do not override this to paint your component.</b> Override
+ * {@link #paintContent(PaintTarget)} instead.
+ * </p>
+ *
+ *
+ * @param target
+ * the target UIDL stream where the component should paint itself
+ * to.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public static void paint(Component component, PaintTarget target)
+ throws PaintException {
+ // Only paint content of visible components.
+ if (!isVisibleInContext(component)) {
+ return;
+ }
+
+ final String tag = target.getTag(component);
+ final PaintStatus status = target.startPaintable(component, tag);
+ if (PaintStatus.CACHED == status) {
+ // nothing to do but flag as cached and close the paintable tag
+ target.addAttribute("cached", true);
+ } else {
+ // Paint the contents of the component
+ if (component instanceof Vaadin6Component) {
+ ((Vaadin6Component) component).paintContent(target);
+ }
+
+ }
+ target.endPaintable(component);
+
+ }
+
+ /**
+ * Checks if the component is visible and its parent is visible,
+ * recursively.
+ * <p>
+ * This is only a helper until paint is moved away from this class.
+ *
+ * @return
+ */
+ protected static boolean isVisibleInContext(Component c) {
+ HasComponents p = c.getParent();
+ while (p != null) {
+ if (!p.isVisible()) {
+ return false;
+ }
+ p = p.getParent();
+ }
+ if (c.getParent() != null && !c.getParent().isComponentVisible(c)) {
+ return false;
+ }
+
+ // All parents visible, return this state
+ return c.isVisible();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/Page.java b/server/src/com/vaadin/terminal/Page.java
new file mode 100644
index 0000000000..a068e7573e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Page.java
@@ -0,0 +1,646 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.EventObject;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.event.EventRouter;
+import com.vaadin.shared.ui.root.PageClientRpc;
+import com.vaadin.terminal.WrappedRequest.BrowserDetails;
+import com.vaadin.terminal.gwt.client.ui.notification.VNotification;
+import com.vaadin.terminal.gwt.client.ui.root.VRoot;
+import com.vaadin.terminal.gwt.server.WebApplicationContext;
+import com.vaadin.terminal.gwt.server.WebBrowser;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.JavaScript;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.Root;
+
+public class Page implements Serializable {
+
+ /**
+ * Listener that gets notified when the size of the browser window
+ * containing the root has changed.
+ *
+ * @see Root#addListener(BrowserWindowResizeListener)
+ */
+ public interface BrowserWindowResizeListener extends Serializable {
+ /**
+ * Invoked when the browser window containing a Root has been resized.
+ *
+ * @param event
+ * a browser window resize event
+ */
+ public void browserWindowResized(BrowserWindowResizeEvent event);
+ }
+
+ /**
+ * Event that is fired when a browser window containing a root is resized.
+ */
+ public class BrowserWindowResizeEvent extends EventObject {
+
+ private final int width;
+ private final int height;
+
+ /**
+ * Creates a new event
+ *
+ * @param source
+ * the root for which the browser window has been resized
+ * @param width
+ * the new width of the browser window
+ * @param height
+ * the new height of the browser window
+ */
+ public BrowserWindowResizeEvent(Page source, int width, int height) {
+ super(source);
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public Page getSource() {
+ return (Page) super.getSource();
+ }
+
+ /**
+ * Gets the new browser window height
+ *
+ * @return an integer with the new pixel height of the browser window
+ */
+ public int getHeight() {
+ return height;
+ }
+
+ /**
+ * Gets the new browser window width
+ *
+ * @return an integer with the new pixel width of the browser window
+ */
+ public int getWidth() {
+ return width;
+ }
+ }
+
+ /**
+ * Private class for storing properties related to opening resources.
+ */
+ private class OpenResource implements Serializable {
+
+ /**
+ * The resource to open
+ */
+ private final Resource resource;
+
+ /**
+ * The name of the target window
+ */
+ private final String name;
+
+ /**
+ * The width of the target window
+ */
+ private final int width;
+
+ /**
+ * The height of the target window
+ */
+ private final int height;
+
+ /**
+ * The border style of the target window
+ */
+ private final int border;
+
+ /**
+ * Creates a new open resource.
+ *
+ * @param resource
+ * The resource to open
+ * @param name
+ * The name of the target window
+ * @param width
+ * The width of the target window
+ * @param height
+ * The height of the target window
+ * @param border
+ * The border style of the target window
+ */
+ private OpenResource(Resource resource, String name, int width,
+ int height, int border) {
+ this.resource = resource;
+ this.name = name;
+ this.width = width;
+ this.height = height;
+ this.border = border;
+ }
+
+ /**
+ * Paints the open request. Should be painted inside the window.
+ *
+ * @param target
+ * the paint target
+ * @throws PaintException
+ * if the paint operation fails
+ */
+ private void paintContent(PaintTarget target) throws PaintException {
+ target.startTag("open");
+ target.addAttribute("src", resource);
+ if (name != null && name.length() > 0) {
+ target.addAttribute("name", name);
+ }
+ if (width >= 0) {
+ target.addAttribute("width", width);
+ }
+ if (height >= 0) {
+ target.addAttribute("height", height);
+ }
+ switch (border) {
+ case BORDER_MINIMAL:
+ target.addAttribute("border", "minimal");
+ break;
+ case BORDER_NONE:
+ target.addAttribute("border", "none");
+ break;
+ }
+
+ target.endTag("open");
+ }
+ }
+
+ private static final Method BROWSWER_RESIZE_METHOD = ReflectTools
+ .findMethod(BrowserWindowResizeListener.class,
+ "browserWindowResized", BrowserWindowResizeEvent.class);
+
+ /**
+ * A border style used for opening resources in a window without a border.
+ */
+ public static final int BORDER_NONE = 0;
+
+ /**
+ * A border style used for opening resources in a window with a minimal
+ * border.
+ */
+ public static final int BORDER_MINIMAL = 1;
+
+ /**
+ * A border style that indicates that the default border style should be
+ * used when opening resources.
+ */
+ public static final int BORDER_DEFAULT = 2;
+
+ /**
+ * Listener that listens changes in URI fragment.
+ */
+ public interface FragmentChangedListener extends Serializable {
+ public void fragmentChanged(FragmentChangedEvent event);
+ }
+
+ private static final Method FRAGMENT_CHANGED_METHOD = ReflectTools
+ .findMethod(Page.FragmentChangedListener.class, "fragmentChanged",
+ FragmentChangedEvent.class);
+
+ /**
+ * Resources to be opened automatically on next repaint. The list is
+ * automatically cleared when it has been sent to the client.
+ */
+ private final LinkedList<OpenResource> openList = new LinkedList<OpenResource>();
+
+ /**
+ * A list of notifications that are waiting to be sent to the client.
+ * Cleared (set to null) when the notifications have been sent.
+ */
+ private List<Notification> notifications;
+
+ /**
+ * Event fired when uri fragment changes.
+ */
+ public class FragmentChangedEvent extends EventObject {
+
+ /**
+ * The new uri fragment
+ */
+ private final String fragment;
+
+ /**
+ * Creates a new instance of UriFragmentReader change event.
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public FragmentChangedEvent(Page source, String fragment) {
+ super(source);
+ this.fragment = fragment;
+ }
+
+ /**
+ * Gets the root in which the fragment has changed.
+ *
+ * @return the root in which the fragment has changed
+ */
+ public Page getPage() {
+ return (Page) getSource();
+ }
+
+ /**
+ * Get the new fragment
+ *
+ * @return the new fragment
+ */
+ public String getFragment() {
+ return fragment;
+ }
+ }
+
+ private EventRouter eventRouter;
+
+ /**
+ * The current URI fragment.
+ */
+ private String fragment;
+
+ private final Root root;
+
+ private int browserWindowWidth = -1;
+ private int browserWindowHeight = -1;
+
+ private JavaScript javaScript;
+
+ public Page(Root root) {
+ this.root = root;
+ }
+
+ private void addListener(Class<?> eventType, Object target, Method method) {
+ if (eventRouter == null) {
+ eventRouter = new EventRouter();
+ }
+ eventRouter.addListener(eventType, target, method);
+ }
+
+ private void removeListener(Class<?> eventType, Object target, Method method) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, target, method);
+ }
+ }
+
+ public void addListener(Page.FragmentChangedListener listener) {
+ addListener(FragmentChangedEvent.class, listener,
+ FRAGMENT_CHANGED_METHOD);
+ }
+
+ public void removeListener(Page.FragmentChangedListener listener) {
+ removeListener(FragmentChangedEvent.class, listener,
+ FRAGMENT_CHANGED_METHOD);
+ }
+
+ /**
+ * Sets URI fragment. Optionally fires a {@link FragmentChangedEvent}
+ *
+ * @param newFragment
+ * id of the new fragment
+ * @param fireEvent
+ * true to fire event
+ * @see FragmentChangedEvent
+ * @see Page.FragmentChangedListener
+ */
+ public void setFragment(String newFragment, boolean fireEvents) {
+ if (newFragment == null) {
+ throw new NullPointerException("The fragment may not be null");
+ }
+ if (!newFragment.equals(fragment)) {
+ fragment = newFragment;
+ if (fireEvents) {
+ fireEvent(new FragmentChangedEvent(this, newFragment));
+ }
+ root.requestRepaint();
+ }
+ }
+
+ private void fireEvent(EventObject event) {
+ if (eventRouter != null) {
+ eventRouter.fireEvent(event);
+ }
+ }
+
+ /**
+ * Sets URI fragment. This method fires a {@link FragmentChangedEvent}
+ *
+ * @param newFragment
+ * id of the new fragment
+ * @see FragmentChangedEvent
+ * @see Page.FragmentChangedListener
+ */
+ public void setFragment(String newFragment) {
+ setFragment(newFragment, true);
+ }
+
+ /**
+ * Gets currently set URI fragment.
+ * <p>
+ * To listen changes in fragment, hook a
+ * {@link Page.FragmentChangedListener}.
+ *
+ * @return the current fragment in browser uri or null if not known
+ */
+ public String getFragment() {
+ return fragment;
+ }
+
+ public void init(WrappedRequest request) {
+ BrowserDetails browserDetails = request.getBrowserDetails();
+ if (browserDetails != null) {
+ fragment = browserDetails.getUriFragment();
+ }
+ }
+
+ public WebBrowser getWebBrowser() {
+ return ((WebApplicationContext) root.getApplication().getContext())
+ .getBrowser();
+ }
+
+ public void setBrowserWindowSize(Integer width, Integer height) {
+ boolean fireEvent = false;
+
+ if (width != null) {
+ int newWidth = width.intValue();
+ if (newWidth != browserWindowWidth) {
+ browserWindowWidth = newWidth;
+ fireEvent = true;
+ }
+ }
+
+ if (height != null) {
+ int newHeight = height.intValue();
+ if (newHeight != browserWindowHeight) {
+ browserWindowHeight = newHeight;
+ fireEvent = true;
+ }
+ }
+
+ if (fireEvent) {
+ fireEvent(new BrowserWindowResizeEvent(this, browserWindowWidth,
+ browserWindowHeight));
+ }
+
+ }
+
+ /**
+ * Adds a new {@link BrowserWindowResizeListener} to this root. The listener
+ * will be notified whenever the browser window within which this root
+ * resides is resized.
+ *
+ * @param resizeListener
+ * the listener to add
+ *
+ * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent)
+ * @see #setResizeLazy(boolean)
+ */
+ public void addListener(BrowserWindowResizeListener resizeListener) {
+ addListener(BrowserWindowResizeEvent.class, resizeListener,
+ BROWSWER_RESIZE_METHOD);
+ }
+
+ /**
+ * Removes a {@link BrowserWindowResizeListener} from this root. The
+ * listener will no longer be notified when the browser window is resized.
+ *
+ * @param resizeListener
+ * the listener to remove
+ */
+ public void removeListener(BrowserWindowResizeListener resizeListener) {
+ removeListener(BrowserWindowResizeEvent.class, resizeListener,
+ BROWSWER_RESIZE_METHOD);
+ }
+
+ /**
+ * Gets the last known height of the browser window in which this root
+ * resides.
+ *
+ * @return the browser window height in pixels
+ */
+ public int getBrowserWindowHeight() {
+ return browserWindowHeight;
+ }
+
+ /**
+ * Gets the last known width of the browser window in which this root
+ * resides.
+ *
+ * @return the browser window width in pixels
+ */
+ public int getBrowserWindowWidth() {
+ return browserWindowWidth;
+ }
+
+ public JavaScript getJavaScript() {
+ if (javaScript == null) {
+ // Create and attach on first use
+ javaScript = new JavaScript();
+ javaScript.extend(root);
+ }
+
+ return javaScript;
+ }
+
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (!openList.isEmpty()) {
+ for (final Iterator<OpenResource> i = openList.iterator(); i
+ .hasNext();) {
+ (i.next()).paintContent(target);
+ }
+ openList.clear();
+ }
+
+ // Paint notifications
+ if (notifications != null) {
+ target.startTag("notifications");
+ for (final Iterator<Notification> it = notifications.iterator(); it
+ .hasNext();) {
+ final Notification n = it.next();
+ target.startTag("notification");
+ if (n.getCaption() != null) {
+ target.addAttribute(
+ VNotification.ATTRIBUTE_NOTIFICATION_CAPTION,
+ n.getCaption());
+ }
+ if (n.getDescription() != null) {
+ target.addAttribute(
+ VNotification.ATTRIBUTE_NOTIFICATION_MESSAGE,
+ n.getDescription());
+ }
+ if (n.getIcon() != null) {
+ target.addAttribute(
+ VNotification.ATTRIBUTE_NOTIFICATION_ICON,
+ n.getIcon());
+ }
+ if (!n.isHtmlContentAllowed()) {
+ target.addAttribute(
+ VRoot.NOTIFICATION_HTML_CONTENT_NOT_ALLOWED, true);
+ }
+ target.addAttribute(
+ VNotification.ATTRIBUTE_NOTIFICATION_POSITION,
+ n.getPosition());
+ target.addAttribute(VNotification.ATTRIBUTE_NOTIFICATION_DELAY,
+ n.getDelayMsec());
+ if (n.getStyleName() != null) {
+ target.addAttribute(
+ VNotification.ATTRIBUTE_NOTIFICATION_STYLE,
+ n.getStyleName());
+ }
+ target.endTag("notification");
+ }
+ target.endTag("notifications");
+ notifications = null;
+ }
+
+ if (fragment != null) {
+ target.addAttribute(VRoot.FRAGMENT_VARIABLE, fragment);
+ }
+
+ }
+
+ /**
+ * Opens the given resource in this root. The contents of this Root is
+ * replaced by the {@code Resource}.
+ *
+ * @param resource
+ * the resource to show in this root
+ */
+ public void open(Resource resource) {
+ openList.add(new OpenResource(resource, null, -1, -1, BORDER_DEFAULT));
+ root.requestRepaint();
+ }
+
+ /**
+ * Opens the given resource in a window with the given name.
+ * <p>
+ * The supplied {@code windowName} is used as the target name in a
+ * window.open call in the client. This means that special values such as
+ * "_blank", "_self", "_top", "_parent" have special meaning. An empty or
+ * <code>null</code> window name is also a special case.
+ * </p>
+ * <p>
+ * "", null and "_self" as {@code windowName} all causes the resource to be
+ * opened in the current window, replacing any old contents. For
+ * downloadable content you should avoid "_self" as "_self" causes the
+ * client to skip rendering of any other changes as it considers them
+ * irrelevant (the page will be replaced by the resource). This can speed up
+ * the opening of a resource, but it might also put the client side into an
+ * inconsistent state if the window content is not completely replaced e.g.,
+ * if the resource is downloaded instead of displayed in the browser.
+ * </p>
+ * <p>
+ * "_blank" as {@code windowName} causes the resource to always be opened in
+ * a new window or tab (depends on the browser and browser settings).
+ * </p>
+ * <p>
+ * "_top" and "_parent" as {@code windowName} works as specified by the HTML
+ * standard.
+ * </p>
+ * <p>
+ * Any other {@code windowName} will open the resource in a window with that
+ * name, either by opening a new window/tab in the browser or by replacing
+ * the contents of an existing window with that name.
+ * </p>
+ *
+ * @param resource
+ * the resource.
+ * @param windowName
+ * the name of the window.
+ */
+ public void open(Resource resource, String windowName) {
+ openList.add(new OpenResource(resource, windowName, -1, -1,
+ BORDER_DEFAULT));
+ root.requestRepaint();
+ }
+
+ /**
+ * Opens the given resource in a window with the given size, border and
+ * name. For more information on the meaning of {@code windowName}, see
+ * {@link #open(Resource, String)}.
+ *
+ * @param resource
+ * the resource.
+ * @param windowName
+ * the name of the window.
+ * @param width
+ * the width of the window in pixels
+ * @param height
+ * the height of the window in pixels
+ * @param border
+ * the border style of the window. See {@link #BORDER_NONE
+ * Window.BORDER_* constants}
+ */
+ public void open(Resource resource, String windowName, int width,
+ int height, int border) {
+ openList.add(new OpenResource(resource, windowName, width, height,
+ border));
+ root.requestRepaint();
+ }
+
+ /**
+ * Internal helper method to actually add a notification.
+ *
+ * @param notification
+ * the notification to add
+ */
+ private void addNotification(Notification notification) {
+ if (notifications == null) {
+ notifications = new LinkedList<Notification>();
+ }
+ notifications.add(notification);
+ root.requestRepaint();
+ }
+
+ /**
+ * Shows a notification message.
+ *
+ * @see Notification
+ *
+ * @param notification
+ * The notification message to show
+ *
+ * @deprecated Use Notification.show(Page) instead.
+ */
+ @Deprecated
+ public void showNotification(Notification notification) {
+ addNotification(notification);
+ }
+
+ /**
+ * Gets the Page to which the current root belongs. This is automatically
+ * defined when processing requests to the server. In other cases, (e.g.
+ * from background threads), the current root is not automatically defined.
+ *
+ * @see Root#getCurrent()
+ *
+ * @return the current page instance if available, otherwise
+ * <code>null</code>
+ */
+ public static Page getCurrent() {
+ Root currentRoot = Root.getCurrent();
+ if (currentRoot == null) {
+ return null;
+ }
+ return currentRoot.getPage();
+ }
+
+ /**
+ * Sets the page title. The page title is displayed by the browser e.g. as
+ * the title of the browser window or as the title of the tab.
+ *
+ * @param title
+ * the new page title to set
+ */
+ public void setTitle(String title) {
+ root.getRpcProxy(PageClientRpc.class).setTitle(title);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/PaintException.java b/server/src/com/vaadin/terminal/PaintException.java
new file mode 100644
index 0000000000..68f689b7f1
--- /dev/null
+++ b/server/src/com/vaadin/terminal/PaintException.java
@@ -0,0 +1,54 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * <code>PaintExcepection</code> is thrown if painting of a component fails.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class PaintException extends IOException implements Serializable {
+
+ /**
+ * Constructs an instance of <code>PaintExeception</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message.
+ */
+ public PaintException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs an instance of <code>PaintExeception</code> with the specified
+ * detail message and cause.
+ *
+ * @param msg
+ * the detail message.
+ * @param cause
+ * the cause
+ */
+ public PaintException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Constructs an instance of <code>PaintExeception</code> from IOException.
+ *
+ * @param exception
+ * the original exception.
+ */
+ public PaintException(IOException exception) {
+ super(exception.getMessage());
+ }
+}
diff --git a/server/src/com/vaadin/terminal/PaintTarget.java b/server/src/com/vaadin/terminal/PaintTarget.java
new file mode 100644
index 0000000000..b658c9f4a3
--- /dev/null
+++ b/server/src/com/vaadin/terminal/PaintTarget.java
@@ -0,0 +1,509 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import com.vaadin.terminal.StreamVariable.StreamingStartEvent;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+import com.vaadin.ui.Component;
+
+/**
+ * This interface defines the methods for painting XML to the UIDL stream.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface PaintTarget extends Serializable {
+
+ /**
+ * Prints single XMLsection.
+ *
+ * Prints full XML section. The section data is escaped from XML tags and
+ * surrounded by XML start and end-tags.
+ *
+ * @param sectionTagName
+ * the name of the tag.
+ * @param sectionData
+ * the scetion data.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addSection(String sectionTagName, String sectionData)
+ throws PaintException;
+
+ /**
+ * Result of starting to paint a Paintable (
+ * {@link PaintTarget#startPaintable(Component, String)}).
+ *
+ * @since 7.0
+ */
+ public enum PaintStatus {
+ /**
+ * Painting started, addVariable() and addAttribute() etc. methods may
+ * be called.
+ */
+ PAINTING,
+ /**
+ * A previously unpainted or painted {@link Paintable} has been queued
+ * be created/update later in a separate change in the same set of
+ * changes.
+ */
+ CACHED
+ }
+
+ /**
+ * Prints element start tag of a paintable section. Starts a paintable
+ * section using the given tag. The PaintTarget may implement a caching
+ * scheme, that checks the paintable has actually changed or can a cached
+ * version be used instead. This method should call the startTag method.
+ * <p>
+ * If the Paintable is found in cache and this function returns true it may
+ * omit the content and close the tag, in which case cached content should
+ * be used.
+ * </p>
+ * <p>
+ * This method may also add only a reference to the paintable and queue the
+ * paintable to be painted separately.
+ * </p>
+ * <p>
+ * Each paintable being painted should be closed by a matching
+ * {@link #endPaintable(Component)} regardless of the {@link PaintStatus}
+ * returned.
+ * </p>
+ *
+ * @param paintable
+ * the paintable to start.
+ * @param tag
+ * the name of the start tag.
+ * @return {@link PaintStatus} - ready to paint or already cached on the
+ * client (also used for sub paintables that are painted later
+ * separately)
+ * @throws PaintException
+ * if the paint operation failed.
+ * @see #startTag(String)
+ * @since 7.0 (previously using startTag(Paintable, String))
+ */
+ public PaintStatus startPaintable(Component paintable, String tag)
+ throws PaintException;
+
+ /**
+ * Prints paintable element end tag.
+ *
+ * Calls to {@link #startPaintable(Component, String)}should be matched by
+ * {@link #endPaintable(Component)}. If the parent tag is closed before
+ * every child tag is closed a PaintException is raised.
+ *
+ * @param paintable
+ * the paintable to close.
+ * @throws PaintException
+ * if the paint operation failed.
+ * @since 7.0 (previously using engTag(String))
+ */
+ public void endPaintable(Component paintable) throws PaintException;
+
+ /**
+ * Prints element start tag.
+ *
+ * <pre>
+ * Todo:
+ * Checking of input values
+ * </pre>
+ *
+ * @param tagName
+ * the name of the start tag.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void startTag(String tagName) throws PaintException;
+
+ /**
+ * Prints element end tag.
+ *
+ * If the parent tag is closed before every child tag is closed an
+ * PaintException is raised.
+ *
+ * @param tagName
+ * the name of the end tag.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void endTag(String tagName) throws PaintException;
+
+ /**
+ * Adds a boolean attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, boolean value) throws PaintException;
+
+ /**
+ * Adds a integer attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, int value) throws PaintException;
+
+ /**
+ * Adds a resource attribute to component. Atributes must be added before
+ * any content is written.
+ *
+ * @param name
+ * the Attribute name
+ * @param value
+ * the Attribute value
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, Resource value) throws PaintException;
+
+ /**
+ * Adds details about {@link StreamVariable} to the UIDL stream. Eg. in web
+ * terminals Receivers are typically rendered for the client side as URLs,
+ * where the client side implementation can do an http post request.
+ * <p>
+ * The urls in UIDL message may use Vaadin specific protocol. Before
+ * actually using the urls on the client side, they should be passed via
+ * {@link ApplicationConnection#translateVaadinUri(String)}.
+ * <p>
+ * Note that in current terminal implementation StreamVariables are cleaned
+ * from the terminal only when:
+ * <ul>
+ * <li>a StreamVariable with same name replaces an old one
+ * <li>the variable owner is no more attached
+ * <li>the developer signals this by calling
+ * {@link StreamingStartEvent#disposeStreamVariable()}
+ * </ul>
+ * Most commonly a component developer can just ignore this issue, but with
+ * strict memory requirements and lots of StreamVariables implementations
+ * that reserve a lot of memory this may be a critical issue.
+ *
+ * @param owner
+ * the ReceiverOwner that can track the progress of streaming to
+ * the given StreamVariable
+ * @param name
+ * an identifying name for the StreamVariable
+ * @param value
+ * the StreamVariable to paint
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name,
+ StreamVariable value) throws PaintException;
+
+ /**
+ * Adds a long attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, long value) throws PaintException;
+
+ /**
+ * Adds a float attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, float value) throws PaintException;
+
+ /**
+ * Adds a double attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, double value) throws PaintException;
+
+ /**
+ * Adds a string attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Boolean attribute name.
+ * @param value
+ * the Boolean attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, String value) throws PaintException;
+
+ /**
+ * TODO
+ *
+ * @param name
+ * @param value
+ * @throws PaintException
+ */
+ public void addAttribute(String name, Map<?, ?> value)
+ throws PaintException;
+
+ /**
+ * Adds a Paintable type attribute. On client side the value will be a
+ * terminal specific reference to corresponding component on client side
+ * implementation.
+ *
+ * @param name
+ * the name of the attribute
+ * @param value
+ * the Paintable to be referenced on client side
+ * @throws PaintException
+ */
+ public void addAttribute(String name, Component value)
+ throws PaintException;
+
+ /**
+ * Adds a string type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, String value)
+ throws PaintException;
+
+ /**
+ * Adds a int type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, int value)
+ throws PaintException;
+
+ /**
+ * Adds a long type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, long value)
+ throws PaintException;
+
+ /**
+ * Adds a float type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, float value)
+ throws PaintException;
+
+ /**
+ * Adds a double type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, double value)
+ throws PaintException;
+
+ /**
+ * Adds a boolean type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, boolean value)
+ throws PaintException;
+
+ /**
+ * Adds a string array type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, String[] value)
+ throws PaintException;
+
+ /**
+ * Adds a Paintable type variable. On client side the variable value will be
+ * a terminal specific reference to corresponding component on client side
+ * implementation. When updated from client side, terminal will map the
+ * client side component reference back to a corresponding server side
+ * reference.
+ *
+ * @param owner
+ * the Listener for variable changes
+ * @param name
+ * the name of the variable
+ * @param value
+ * the initial value of the variable
+ *
+ * @throws PaintException
+ * if the paint oparation fails
+ */
+ public void addVariable(VariableOwner owner, String name, Component value)
+ throws PaintException;
+
+ /**
+ * Adds a upload stream type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addUploadStreamVariable(VariableOwner owner, String name)
+ throws PaintException;
+
+ /**
+ * Prints single XML section.
+ * <p>
+ * Prints full XML section. The section data must be XML and it is
+ * surrounded by XML start and end-tags.
+ * </p>
+ *
+ * @param sectionTagName
+ * the tag name.
+ * @param sectionData
+ * the section data to be printed.
+ * @param namespace
+ * the namespace.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addXMLSection(String sectionTagName, String sectionData,
+ String namespace) throws PaintException;
+
+ /**
+ * Adds UIDL directly. The UIDL must be valid in accordance with the
+ * UIDL.dtd
+ *
+ * @param uidl
+ * the UIDL to be added.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addUIDL(java.lang.String uidl) throws PaintException;
+
+ /**
+ * Adds text node. All the contents of the text are XML-escaped.
+ *
+ * @param text
+ * the Text to add
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ void addText(String text) throws PaintException;
+
+ /**
+ * Adds CDATA node to target UIDL-tree.
+ *
+ * @param text
+ * the Character data to add
+ * @throws PaintException
+ * if the paint operation failed.
+ * @since 3.1
+ */
+ void addCharacterData(String text) throws PaintException;
+
+ public void addAttribute(String string, Object[] keys);
+
+ /**
+ * @return the "tag" string used in communication to present given
+ * {@link ClientConnector} type. Terminal may define how to present
+ * the connector.
+ */
+ public String getTag(ClientConnector paintable);
+
+ /**
+ * @return true if a full repaint has been requested. E.g. refresh in a
+ * browser window or such.
+ */
+ public boolean isFullRepaint();
+
+}
diff --git a/server/src/com/vaadin/terminal/RequestHandler.java b/server/src/com/vaadin/terminal/RequestHandler.java
new file mode 100644
index 0000000000..f37201715d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/RequestHandler.java
@@ -0,0 +1,36 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import com.vaadin.Application;
+
+/**
+ * Handler for producing a response to non-UIDL requests. Handlers can be added
+ * to applications using {@link Application#addRequestHandler(RequestHandler)}
+ */
+public interface RequestHandler extends Serializable {
+
+ /**
+ * Handles a non-UIDL request. If a response is written, this method should
+ * return <code>false</code> to indicate that no more request handlers
+ * should be invoked for the request.
+ *
+ * @param application
+ * The application to which the request belongs
+ * @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
+ */
+ boolean handleRequest(Application application, WrappedRequest request,
+ WrappedResponse response) throws IOException;
+
+}
diff --git a/server/src/com/vaadin/terminal/Resource.java b/server/src/com/vaadin/terminal/Resource.java
new file mode 100644
index 0000000000..58dc4fea9d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Resource.java
@@ -0,0 +1,26 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+/**
+ * <code>Resource</code> provided to the client terminal. Support for actually
+ * displaying the resource type is left to the terminal.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Resource extends Serializable {
+
+ /**
+ * Gets the MIME type of the resource.
+ *
+ * @return the MIME type of the resource.
+ */
+ public String getMIMEType();
+}
diff --git a/server/src/com/vaadin/terminal/Scrollable.java b/server/src/com/vaadin/terminal/Scrollable.java
new file mode 100644
index 0000000000..472954c556
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Scrollable.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * This interface is implemented by all visual objects that can be scrolled
+ * programmatically from the server-side. The unit of scrolling is pixel.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Scrollable extends Serializable {
+
+ /**
+ * Gets scroll left offset.
+ *
+ * <p>
+ * Scrolling offset is the number of pixels this scrollable has been
+ * scrolled right.
+ * </p>
+ *
+ * @return Horizontal scrolling position in pixels.
+ */
+ public int getScrollLeft();
+
+ /**
+ * Sets scroll left offset.
+ *
+ * <p>
+ * Scrolling offset is the number of pixels this scrollable has been
+ * scrolled right.
+ * </p>
+ *
+ * @param scrollLeft
+ * the xOffset.
+ */
+ public void setScrollLeft(int scrollLeft);
+
+ /**
+ * Gets scroll top offset.
+ *
+ * <p>
+ * Scrolling offset is the number of pixels this scrollable has been
+ * scrolled down.
+ * </p>
+ *
+ * @return Vertical scrolling position in pixels.
+ */
+ public int getScrollTop();
+
+ /**
+ * Sets scroll top offset.
+ *
+ * <p>
+ * Scrolling offset is the number of pixels this scrollable has been
+ * scrolled down.
+ * </p>
+ *
+ * <p>
+ * The scrolling position is limited by the current height of the content
+ * area. If the position is below the height, it is scrolled to the bottom.
+ * However, if the same response also adds height to the content area,
+ * scrolling to bottom only scrolls to the bottom of the previous content
+ * area.
+ * </p>
+ *
+ * @param scrollTop
+ * the yOffset.
+ */
+ public void setScrollTop(int scrollTop);
+
+}
diff --git a/server/src/com/vaadin/terminal/Sizeable.java b/server/src/com/vaadin/terminal/Sizeable.java
new file mode 100644
index 0000000000..e3c98e0fa9
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Sizeable.java
@@ -0,0 +1,242 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+/**
+ * Interface to be implemented by components wishing to display some object that
+ * may be dynamically resized during runtime.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Sizeable extends Serializable {
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#PIXELS} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_PIXELS = Unit.PIXELS;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#POINTS} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_POINTS = Unit.POINTS;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#PICAS} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_PICAS = Unit.PICAS;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#EM} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_EM = Unit.EM;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#EX} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_EX = Unit.EX;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#MM} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_MM = Unit.MM;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#CM} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_CM = Unit.CM;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#INCH} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_INCH = Unit.INCH;
+
+ /**
+ * @deprecated from 7.0, use {@link Unit#PERCENTAGE} instead    
+ */
+ @Deprecated
+ public static final Unit UNITS_PERCENTAGE = Unit.PERCENTAGE;
+
+ public static final float SIZE_UNDEFINED = -1;
+
+ public enum Unit {
+ /**
+ * Unit code representing pixels.
+ */
+ PIXELS("px"),
+ /**
+ * Unit code representing points (1/72nd of an inch).
+ */
+ POINTS("pt"),
+ /**
+ * Unit code representing picas (12 points).
+ */
+ PICAS("pc"),
+ /**
+ * Unit code representing the font-size of the relevant font.
+ */
+ EM("em"),
+ /**
+ * Unit code representing the x-height of the relevant font.
+ */
+ EX("ex"),
+ /**
+ * Unit code representing millimeters.
+ */
+ MM("mm"),
+ /**
+ * Unit code representing centimeters.
+ */
+ CM("cm"),
+ /**
+ * Unit code representing inches.
+ */
+ INCH("in"),
+ /**
+ * Unit code representing in percentage of the containing element
+ * defined by terminal.
+ */
+ PERCENTAGE("%");
+
+ private String symbol;
+
+ private Unit(String symbol) {
+ this.symbol = symbol;
+ }
+
+ public String getSymbol() {
+ return symbol;
+ }
+
+ @Override
+ public String toString() {
+ return symbol;
+ }
+
+ public static Unit getUnitFromSymbol(String symbol) {
+ if (symbol == null) {
+ return Unit.PIXELS; // Defaults to pixels
+ }
+ for (Unit unit : Unit.values()) {
+ if (symbol.equals(unit.getSymbol())) {
+ return unit;
+ }
+ }
+ return Unit.PIXELS; // Defaults to pixels
+ }
+ }
+
+ /**
+ * Gets the width of the object. Negative number implies unspecified size
+ * (terminal is free to set the size).
+ *
+ * @return width of the object in units specified by widthUnits property.
+ */
+ public float getWidth();
+
+ /**
+ * Gets the height of the object. Negative number implies unspecified size
+ * (terminal is free to set the size).
+ *
+ * @return height of the object in units specified by heightUnits property.
+ */
+ public float getHeight();
+
+ /**
+ * Gets the width property units.
+ *
+ * @return units used in width property.
+ */
+ public Unit getWidthUnits();
+
+ /**
+ * Gets the height property units.
+ *
+ * @return units used in height property.
+ */
+ public Unit getHeightUnits();
+
+ /**
+ * Sets the height of the component using String presentation.
+ *
+ * String presentation is similar to what is used in Cascading Style Sheets.
+ * Size can be length or percentage of available size.
+ *
+ * The empty string ("") or null will unset the height and set the units to
+ * pixels.
+ *
+ * See <a
+ * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS
+ * specification</a> for more details.
+ *
+ * @param height
+ * in CSS style string representation
+ */
+ public void setHeight(String height);
+
+ /**
+ * Sets the width of the object. Negative number implies unspecified size
+ * (terminal is free to set the size).
+ *
+ * @param width
+ * the width of the object.
+ * @param unit
+ * the unit used for the width.
+ */
+ public void setWidth(float width, Unit unit);
+
+ /**
+ * Sets the height of the object. Negative number implies unspecified size
+ * (terminal is free to set the size).
+ *
+ * @param height
+ * the height of the object.
+ * @param unit
+ * the unit used for the width.
+ */
+ public void setHeight(float height, Unit unit);
+
+ /**
+ * Sets the width of the component using String presentation.
+ *
+ * String presentation is similar to what is used in Cascading Style Sheets.
+ * Size can be length or percentage of available size.
+ *
+ * The empty string ("") or null will unset the width and set the units to
+ * pixels.
+ *
+ * See <a
+ * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS
+ * specification</a> for more details.
+ *
+ * @param width
+ * in CSS style string representation, null or empty string to
+ * reset
+ */
+ public void setWidth(String width);
+
+ /**
+ * Sets the size to 100% x 100%.
+ */
+ public void setSizeFull();
+
+ /**
+ * Clears any size settings.
+ */
+ public void setSizeUndefined();
+
+}
diff --git a/server/src/com/vaadin/terminal/StreamResource.java b/server/src/com/vaadin/terminal/StreamResource.java
new file mode 100644
index 0000000000..1afd91dc08
--- /dev/null
+++ b/server/src/com/vaadin/terminal/StreamResource.java
@@ -0,0 +1,222 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.InputStream;
+import java.io.Serializable;
+
+import com.vaadin.Application;
+import com.vaadin.service.FileTypeResolver;
+
+/**
+ * <code>StreamResource</code> is a resource provided to the client directly by
+ * the application. The strean resource is fetched from URI that is most often
+ * in the context of the application or window. The resource is automatically
+ * registered to window in creation.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class StreamResource implements ApplicationResource {
+
+ /**
+ * Source stream the downloaded content is fetched from.
+ */
+ private StreamSource streamSource = null;
+
+ /**
+ * Explicit mime-type.
+ */
+ private String MIMEType = null;
+
+ /**
+ * Filename.
+ */
+ private String filename;
+
+ /**
+ * Application.
+ */
+ private final Application application;
+
+ /**
+ * Default buffer size for this stream resource.
+ */
+ private int bufferSize = 0;
+
+ /**
+ * Default cache time for this stream resource.
+ */
+ private long cacheTime = DEFAULT_CACHETIME;
+
+ /**
+ * Creates a new stream resource for downloading from stream.
+ *
+ * @param streamSource
+ * the source Stream.
+ * @param filename
+ * the name of the file.
+ * @param application
+ * the Application object.
+ */
+ public StreamResource(StreamSource streamSource, String filename,
+ Application application) {
+
+ this.application = application;
+ setFilename(filename);
+ setStreamSource(streamSource);
+
+ // Register to application
+ application.addResource(this);
+
+ }
+
+ /**
+ * @see com.vaadin.terminal.Resource#getMIMEType()
+ */
+ @Override
+ public String getMIMEType() {
+ if (MIMEType != null) {
+ return MIMEType;
+ }
+ return FileTypeResolver.getMIMEType(filename);
+ }
+
+ /**
+ * Sets the mime type of the resource.
+ *
+ * @param MIMEType
+ * the MIME type to be set.
+ */
+ public void setMIMEType(String MIMEType) {
+ this.MIMEType = MIMEType;
+ }
+
+ /**
+ * Returns the source for this <code>StreamResource</code>. StreamSource is
+ * queried when the resource is about to be streamed to the client.
+ *
+ * @return Source of the StreamResource.
+ */
+ public StreamSource getStreamSource() {
+ return streamSource;
+ }
+
+ /**
+ * Sets the source for this <code>StreamResource</code>.
+ * <code>StreamSource</code> is queried when the resource is about to be
+ * streamed to the client.
+ *
+ * @param streamSource
+ * the source to set.
+ */
+ public void setStreamSource(StreamSource streamSource) {
+ this.streamSource = streamSource;
+ }
+
+ /**
+ * Gets the filename.
+ *
+ * @return the filename.
+ */
+ @Override
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * Sets the filename.
+ *
+ * @param filename
+ * the filename to set.
+ */
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+
+ /**
+ * @see com.vaadin.terminal.ApplicationResource#getApplication()
+ */
+ @Override
+ public Application getApplication() {
+ return application;
+ }
+
+ /**
+ * @see com.vaadin.terminal.ApplicationResource#getStream()
+ */
+ @Override
+ public DownloadStream getStream() {
+ final StreamSource ss = getStreamSource();
+ if (ss == null) {
+ return null;
+ }
+ final DownloadStream ds = new DownloadStream(ss.getStream(),
+ getMIMEType(), getFilename());
+ ds.setBufferSize(getBufferSize());
+ ds.setCacheTime(cacheTime);
+ return ds;
+ }
+
+ /**
+ * Interface implemented by the source of a StreamResource.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface StreamSource extends Serializable {
+
+ /**
+ * Returns new input stream that is used for reading the resource.
+ */
+ public InputStream getStream();
+ }
+
+ /* documented in superclass */
+ @Override
+ public int getBufferSize() {
+ return bufferSize;
+ }
+
+ /**
+ * Sets the size of the download buffer used for this resource.
+ *
+ * @param bufferSize
+ * the size of the buffer in bytes.
+ */
+ public void setBufferSize(int bufferSize) {
+ this.bufferSize = bufferSize;
+ }
+
+ /* documented in superclass */
+ @Override
+ public long getCacheTime() {
+ return cacheTime;
+ }
+
+ /**
+ * Sets the length of cache expiration time.
+ *
+ * <p>
+ * This gives the adapter the possibility cache streams sent to the client.
+ * The caching may be made in adapter or at the client if the client
+ * supports caching. Zero or negavive value disbales the caching of this
+ * stream.
+ * </p>
+ *
+ * @param cacheTime
+ * the cache time in milliseconds.
+ *
+ */
+ public void setCacheTime(long cacheTime) {
+ this.cacheTime = cacheTime;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/StreamVariable.java b/server/src/com/vaadin/terminal/StreamVariable.java
new file mode 100644
index 0000000000..63763a5751
--- /dev/null
+++ b/server/src/com/vaadin/terminal/StreamVariable.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal;
+
+import java.io.OutputStream;
+import java.io.Serializable;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.StreamVariable.StreamingEndEvent;
+import com.vaadin.terminal.StreamVariable.StreamingErrorEvent;
+import com.vaadin.terminal.StreamVariable.StreamingStartEvent;
+
+/**
+ * StreamVariable is a special kind of variable whose value is streamed to an
+ * {@link OutputStream} provided by the {@link #getOutputStream()} method. E.g.
+ * in web terminals {@link StreamVariable} can be used to send large files from
+ * browsers to the server without consuming large amounts of memory.
+ * <p>
+ * Note, writing to the {@link OutputStream} is not synchronized by the terminal
+ * (to avoid stalls in other operations when eg. streaming to a slow network
+ * service or file system). If UI is changed as a side effect of writing to the
+ * output stream, developer must handle synchronization manually.
+ * <p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.5
+ * @see PaintTarget#addVariable(VariableOwner, String, StreamVariable)
+ */
+public interface StreamVariable extends Serializable {
+
+ /**
+ * Invoked by the terminal when a new upload arrives, after
+ * {@link #streamingStarted(StreamingStartEvent)} method has been called.
+ * The terminal implementation will write the streamed variable to the
+ * returned output stream.
+ *
+ * @return Stream to which the uploaded file should be written.
+ */
+ public OutputStream getOutputStream();
+
+ /**
+ * Whether the {@link #onProgress(long, long)} method should be called
+ * during the upload.
+ * <p>
+ * {@link #onProgress(long, long)} is called in a synchronized block when
+ * the content is being received. This is potentially bit slow, so we are
+ * calling that method only if requested. The value is requested after the
+ * {@link #uploadStarted(StreamingStartEvent)} event, but not after reading
+ * each buffer.
+ *
+ * @return true if this {@link StreamVariable} wants to by notified during
+ * the upload of the progress of streaming.
+ * @see #onProgress(StreamingProgressEvent)
+ */
+ public boolean listenProgress();
+
+ /**
+ * This method is called by the terminal if {@link #listenProgress()}
+ * returns true when the streaming starts.
+ */
+ public void onProgress(StreamingProgressEvent event);
+
+ public void streamingStarted(StreamingStartEvent event);
+
+ public void streamingFinished(StreamingEndEvent event);
+
+ public void streamingFailed(StreamingErrorEvent event);
+
+ /*
+ * Not synchronized to avoid stalls (caused by UIDL requests) while
+ * streaming the content. Implementations also most commonly atomic even
+ * without the restriction.
+ */
+ /**
+ * If this method returns true while the content is being streamed the
+ * Terminal to stop receiving current upload.
+ * <p>
+ * Note, the usage of this method is not synchronized over the Application
+ * instance by the terminal like other methods. The implementation should
+ * only return a boolean field and especially not modify UI or implement a
+ * synchronization by itself.
+ *
+ * @return true if the streaming should be interrupted as soon as possible.
+ */
+ public boolean isInterrupted();
+
+ public interface StreamingEvent extends Serializable {
+
+ /**
+ * @return the file name of the streamed file if known
+ */
+ public String getFileName();
+
+ /**
+ * @return the mime type of the streamed file if known
+ */
+ public String getMimeType();
+
+ /**
+ * @return the length of the stream (in bytes) if known, else -1
+ */
+ public long getContentLength();
+
+ /**
+ * @return then number of bytes streamed to StreamVariable
+ */
+ public long getBytesReceived();
+ }
+
+ /**
+ * Event passed to {@link #uploadStarted(StreamingStartEvent)} method before
+ * the streaming of the content to {@link StreamVariable} starts.
+ */
+ public interface StreamingStartEvent extends StreamingEvent {
+ /**
+ * The owner of the StreamVariable can call this method to inform the
+ * terminal implementation that this StreamVariable will not be used to
+ * accept more post.
+ */
+ public void disposeStreamVariable();
+ }
+
+ /**
+ * Event passed to {@link #onProgress(StreamingProgressEvent)} method during
+ * the streaming progresses.
+ */
+ public interface StreamingProgressEvent extends StreamingEvent {
+ }
+
+ /**
+ * Event passed to {@link #uploadFinished(StreamingEndEvent)} method the
+ * contents have been streamed to StreamVariable successfully.
+ */
+ public interface StreamingEndEvent extends StreamingEvent {
+ }
+
+ /**
+ * Event passed to {@link #uploadFailed(StreamingErrorEvent)} method when
+ * the streaming ended before the end of the input. The streaming may fail
+ * due an interruption by {@link } or due an other unknown exception in
+ * communication. In the latter case the exception is also passed to
+ * {@link Application#terminalError(com.vaadin.terminal.Terminal.ErrorEvent)}
+ * .
+ */
+ public interface StreamingErrorEvent extends StreamingEvent {
+
+ /**
+ * @return the exception that caused the receiving not to finish cleanly
+ */
+ public Exception getException();
+
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/SystemError.java b/server/src/com/vaadin/terminal/SystemError.java
new file mode 100644
index 0000000000..bae135ee6b
--- /dev/null
+++ b/server/src/com/vaadin/terminal/SystemError.java
@@ -0,0 +1,82 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import com.vaadin.terminal.gwt.server.AbstractApplicationServlet;
+
+/**
+ * <code>SystemError</code> is an error message for a problem caused by error in
+ * system, not the user application code. The system error can contain technical
+ * information such as stack trace and exception.
+ *
+ * SystemError does not support HTML in error messages or stack traces. If HTML
+ * messages are required, use {@link UserError} or a custom implementation of
+ * {@link ErrorMessage}.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class SystemError extends AbstractErrorMessage {
+
+ /**
+ * Constructor for SystemError with error message specified.
+ *
+ * @param message
+ * the Textual error description.
+ */
+ public SystemError(String message) {
+ super(message);
+ setErrorLevel(ErrorLevel.SYSTEMERROR);
+ setMode(ContentMode.XHTML);
+ setMessage(getHtmlMessage());
+ }
+
+ /**
+ * Constructor for SystemError with causing exception and error message.
+ *
+ * @param message
+ * the Textual error description.
+ * @param cause
+ * the throwable causing the system error.
+ */
+ public SystemError(String message, Throwable cause) {
+ this(message);
+ addCause(AbstractErrorMessage.getErrorMessageForException(cause));
+ }
+
+ /**
+ * Constructor for SystemError with cause.
+ *
+ * @param cause
+ * the throwable causing the system error.
+ */
+ public SystemError(Throwable cause) {
+ this(null, cause);
+ }
+
+ /**
+ * Returns the message of the error in HTML.
+ *
+ * Note that this API may change in future versions.
+ */
+ protected String getHtmlMessage() {
+ // TODO wrapping div with namespace? See the old code:
+ // target.addXMLSection("div", message,
+ // "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd");
+
+ StringBuilder sb = new StringBuilder();
+ if (getMessage() != null) {
+ sb.append("<h2>");
+ sb.append(AbstractApplicationServlet
+ .safeEscapeForHtml(getMessage()));
+ sb.append("</h2>");
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/Terminal.java b/server/src/com/vaadin/terminal/Terminal.java
new file mode 100644
index 0000000000..9dc6ced6a7
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Terminal.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+
+/**
+ * An interface that provides information about the user's terminal.
+ * Implementors typically provide additional information using methods not in
+ * this interface. </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Terminal extends Serializable {
+
+ /**
+ * Gets the name of the default theme for this terminal.
+ *
+ * @return the name of the theme that is used by default by this terminal.
+ */
+ public String getDefaultTheme();
+
+ /**
+ * Gets the width of the terminal screen in pixels. This is the width of the
+ * screen and not the width available for the application.
+ * <p>
+ * Note that the screen width is typically not available in the
+ * {@link com.vaadin.Application#init()} method as this is called before the
+ * browser has a chance to report the screen size to the server.
+ * </p>
+ *
+ * @return the width of the terminal screen.
+ */
+ public int getScreenWidth();
+
+ /**
+ * Gets the height of the terminal screen in pixels. This is the height of
+ * the screen and not the height available for the application.
+ *
+ * <p>
+ * Note that the screen height is typically not available in the
+ * {@link com.vaadin.Application#init()} method as this is called before the
+ * browser has a chance to report the screen size to the server.
+ * </p>
+ *
+ * @return the height of the terminal screen.
+ */
+ public int getScreenHeight();
+
+ /**
+ * An error event implementation for Terminal.
+ */
+ public interface ErrorEvent extends Serializable {
+
+ /**
+ * Gets the contained throwable, the cause of the error.
+ */
+ public Throwable getThrowable();
+
+ }
+
+ /**
+ * Interface for listening to Terminal errors.
+ */
+ public interface ErrorListener extends Serializable {
+
+ /**
+ * Invoked when a terminal error occurs.
+ *
+ * @param event
+ * the fired event.
+ */
+ public void terminalError(Terminal.ErrorEvent event);
+ }
+}
diff --git a/server/src/com/vaadin/terminal/ThemeResource.java b/server/src/com/vaadin/terminal/ThemeResource.java
new file mode 100644
index 0000000000..41674b2373
--- /dev/null
+++ b/server/src/com/vaadin/terminal/ThemeResource.java
@@ -0,0 +1,96 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import com.vaadin.service.FileTypeResolver;
+
+/**
+ * <code>ThemeResource</code> is a named theme dependant resource provided and
+ * managed by a theme. The actual resource contents are dynamically resolved to
+ * comply with the used theme by the terminal adapter. This is commonly used to
+ * provide static images, flash, java-applets, etc for the terminals.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class ThemeResource implements Resource {
+
+ /**
+ * Id of the terminal managed resource.
+ */
+ private String resourceID = null;
+
+ /**
+ * Creates a resource.
+ *
+ * @param resourceId
+ * the Id of the resource.
+ */
+ public ThemeResource(String resourceId) {
+ if (resourceId == null) {
+ throw new NullPointerException("Resource ID must not be null");
+ }
+ if (resourceId.length() == 0) {
+ throw new IllegalArgumentException("Resource ID can not be empty");
+ }
+ if (resourceId.charAt(0) == '/') {
+ throw new IllegalArgumentException(
+ "Resource ID must be relative (can not begin with /)");
+ }
+
+ resourceID = resourceId;
+ }
+
+ /**
+ * Tests if the given object equals this Resource.
+ *
+ * @param obj
+ * the object to be tested for equality.
+ * @return <code>true</code> if the given object equals this Icon,
+ * <code>false</code> if not.
+ * @see java.lang.Object#equals(Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof ThemeResource
+ && resourceID.equals(((ThemeResource) obj).resourceID);
+ }
+
+ /**
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return resourceID.hashCode();
+ }
+
+ /**
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return resourceID.toString();
+ }
+
+ /**
+ * Gets the resource id.
+ *
+ * @return the resource id.
+ */
+ public String getResourceId() {
+ return resourceID;
+ }
+
+ /**
+ * @see com.vaadin.terminal.Resource#getMIMEType()
+ */
+ @Override
+ public String getMIMEType() {
+ return FileTypeResolver.getMIMEType(getResourceId());
+ }
+}
diff --git a/server/src/com/vaadin/terminal/UserError.java b/server/src/com/vaadin/terminal/UserError.java
new file mode 100644
index 0000000000..a7a4fd89e2
--- /dev/null
+++ b/server/src/com/vaadin/terminal/UserError.java
@@ -0,0 +1,70 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+/**
+ * <code>UserError</code> is a controlled error occurred in application. User
+ * errors are occur in normal usage of the application and guide the user.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class UserError extends AbstractErrorMessage {
+
+ /**
+ * @deprecated from 7.0, use {@link ContentMode#TEXT} instead    
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_TEXT = ContentMode.TEXT;
+
+ /**
+ * @deprecated from 7.0, use {@link ContentMode#PREFORMATTED} instead    
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED;
+
+ /**
+ * @deprecated from 7.0, use {@link ContentMode#XHTML} instead    
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_XHTML = ContentMode.XHTML;
+
+ /**
+ * Creates a textual error message of level ERROR.
+ *
+ * @param textErrorMessage
+ * the text of the error message.
+ */
+ public UserError(String textErrorMessage) {
+ super(textErrorMessage);
+ }
+
+ /**
+ * Creates an error message with level and content mode.
+ *
+ * @param message
+ * the error message.
+ * @param contentMode
+ * the content Mode.
+ * @param errorLevel
+ * the level of error.
+ */
+ public UserError(String message, ContentMode contentMode,
+ ErrorLevel errorLevel) {
+ super(message);
+ if (contentMode == null) {
+ contentMode = ContentMode.TEXT;
+ }
+ if (errorLevel == null) {
+ errorLevel = ErrorLevel.ERROR;
+ }
+ setMode(contentMode);
+ setErrorLevel(errorLevel);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/Vaadin6Component.java b/server/src/com/vaadin/terminal/Vaadin6Component.java
new file mode 100644
index 0000000000..59cbf956ca
--- /dev/null
+++ b/server/src/com/vaadin/terminal/Vaadin6Component.java
@@ -0,0 +1,44 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal;
+
+import java.util.EventListener;
+
+import com.vaadin.ui.Component;
+
+/**
+ * Interface provided to ease porting of Vaadin 6 components to Vaadin 7. By
+ * implementing this interface your Component will be able to use
+ * {@link #paintContent(PaintTarget)} and
+ * {@link #changeVariables(Object, java.util.Map)} just like in Vaadin 6.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public interface Vaadin6Component extends VariableOwner, Component,
+ EventListener {
+
+ /**
+ * <p>
+ * Paints the Paintable into a UIDL stream. This method creates the UIDL
+ * sequence describing it and outputs it to the given UIDL stream.
+ * </p>
+ *
+ * <p>
+ * It is called when the contents of the component should be painted in
+ * response to the component first being shown or having been altered so
+ * that its visual representation is changed.
+ * </p>
+ *
+ * @param target
+ * the target UIDL stream where the component should paint itself
+ * to.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void paintContent(PaintTarget target) throws PaintException;
+
+}
diff --git a/server/src/com/vaadin/terminal/VariableOwner.java b/server/src/com/vaadin/terminal/VariableOwner.java
new file mode 100644
index 0000000000..c52e04c008
--- /dev/null
+++ b/server/src/com/vaadin/terminal/VariableOwner.java
@@ -0,0 +1,85 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * <p>
+ * Listener interface for UI variable changes. The user communicates with the
+ * application using the so-called <i>variables</i>. When the user makes a
+ * change using the UI the terminal trasmits the changed variables to the
+ * application, and the components owning those variables may then process those
+ * changes.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ * @deprecated in 7.0. Only provided to ease porting of Vaadin 6 components. Do
+ * not implement this directly, implement {@link Vaadin6Component}.
+ */
+@Deprecated
+public interface VariableOwner extends Serializable {
+
+ /**
+ * Called when one or more variables handled by the implementing class are
+ * changed.
+ *
+ * @param source
+ * the Source of the variable change. This is the origin of the
+ * event. For example in Web Adapter this is the request.
+ * @param variables
+ * the Mapping from variable names to new variable values.
+ */
+ public void changeVariables(Object source, Map<String, Object> variables);
+
+ /**
+ * <p>
+ * Tests if the variable owner is enabled or not. The terminal should not
+ * send any variable changes to disabled variable owners.
+ * </p>
+ *
+ * @return <code>true</code> if the variable owner is enabled,
+ * <code>false</code> if not
+ */
+ public boolean isEnabled();
+
+ /**
+ * <p>
+ * Tests if the variable owner is in immediate mode or not. Being in
+ * immediate mode means that all variable changes are required to be sent
+ * back from the terminal immediately when they occur.
+ * </p>
+ *
+ * <p>
+ * <strong>Note:</strong> <code>VariableOwner</code> does not include a set-
+ * method for the immediateness property. This is because not all
+ * VariableOwners wish to offer the functionality. Such VariableOwners are
+ * never in the immediate mode, thus they always return <code>false</code>
+ * in {@link #isImmediate()}.
+ * </p>
+ *
+ * @return <code>true</code> if the component is in immediate mode,
+ * <code>false</code> if not.
+ */
+ public boolean isImmediate();
+
+ /**
+ * VariableOwner error event.
+ */
+ public interface ErrorEvent extends Terminal.ErrorEvent {
+
+ /**
+ * Gets the source VariableOwner.
+ *
+ * @return the variable owner.
+ */
+ public VariableOwner getVariableOwner();
+
+ }
+}
diff --git a/server/src/com/vaadin/terminal/WrappedRequest.java b/server/src/com/vaadin/terminal/WrappedRequest.java
new file mode 100644
index 0000000000..a27213d921
--- /dev/null
+++ b/server/src/com/vaadin/terminal/WrappedRequest.java
@@ -0,0 +1,277 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.portlet.PortletRequest;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+
+import com.vaadin.Application;
+import com.vaadin.RootRequiresMoreInformationException;
+import com.vaadin.annotations.EagerInit;
+import com.vaadin.terminal.gwt.server.WebBrowser;
+import com.vaadin.ui.Root;
+
+/**
+ * A generic request to the server, wrapping a more specific request type, e.g.
+ * HttpServletReqest or PortletRequest.
+ *
+ * @since 7.0
+ */
+public interface WrappedRequest extends Serializable {
+
+ /**
+ * Detailed information extracted from the browser.
+ *
+ * @see WrappedRequest#getBrowserDetails()
+ */
+ public interface BrowserDetails extends Serializable {
+ /**
+ * Gets the URI hash fragment for the request. This is typically used to
+ * encode navigation within an application.
+ *
+ * @return the URI hash fragment
+ */
+ public String getUriFragment();
+
+ /**
+ * Gets the value of window.name from the browser. This can be used to
+ * keep track of the specific window between browser reloads.
+ *
+ * @return the string value of window.name in the browser
+ */
+ public String getWindowName();
+
+ /**
+ * Gets a reference to the {@link WebBrowser} object containing
+ * additional information, e.g. screen size and the time zone offset.
+ *
+ * @return the web browser object
+ */
+ public WebBrowser getWebBrowser();
+ }
+
+ /**
+ * Gets the named request parameter This is typically a HTTP GET or POST
+ * parameter, though other request types might have other ways of
+ * representing parameters.
+ *
+ * @see javax.servlet.ServletRequest#getParameter(String)
+ * @see javax.portlet.PortletRequest#getParameter(String)
+ *
+ * @param parameter
+ * the name of the parameter
+ * @return The paramter value, or <code>null</code> if no parameter with the
+ * given name is present
+ */
+ public String getParameter(String parameter);
+
+ /**
+ * Gets all the parameters of the request.
+ *
+ * @see #getParameter(String)
+ *
+ * @see javax.servlet.ServletRequest#getParameterMap()
+ * @see javax.portlet.PortletRequest#getParameter(String)
+ *
+ * @return A mapping of parameter names to arrays of parameter values
+ */
+ public Map<String, String[]> getParameterMap();
+
+ /**
+ * Returns the length of the request content that can be read from the input
+ * stream returned by {@link #getInputStream()}.
+ *
+ * @see javax.servlet.ServletRequest#getContentLength()
+ * @see javax.portlet.ClientDataRequest#getContentLength()
+ *
+ * @return content length in bytes
+ */
+ public int getContentLength();
+
+ /**
+ * Returns an input stream from which the request content can be read. The
+ * request content length can be obtained with {@link #getContentLength()}
+ * without reading the full stream contents.
+ *
+ * @see javax.servlet.ServletRequest#getInputStream()
+ * @see javax.portlet.ClientDataRequest#getPortletInputStream()
+ *
+ * @return the input stream from which the contents of the request can be
+ * read
+ * @throws IOException
+ * if the input stream can not be opened
+ */
+ public InputStream getInputStream() throws IOException;
+
+ /**
+ * Gets a request attribute.
+ *
+ * @param name
+ * the name of the attribute
+ * @return the value of the attribute, or <code>null</code> if there is no
+ * attribute with the given name
+ *
+ * @see javax.servlet.ServletRequest#getAttribute(String)
+ * @see javax.portlet.PortletRequest#getAttribute(String)
+ */
+ public Object getAttribute(String name);
+
+ /**
+ * Defines a request attribute.
+ *
+ * @param name
+ * the name of the attribute
+ * @param value
+ * the attribute value
+ *
+ * @see javax.servlet.ServletRequest#setAttribute(String, Object)
+ * @see javax.portlet.PortletRequest#setAttribute(String, Object)
+ */
+ public void setAttribute(String name, Object value);
+
+ /**
+ * Gets the path of the requested resource relative to the application. The
+ * path be <code>null</code> if no path information is available. Does
+ * always start with / if the path isn't <code>null</code>.
+ *
+ * @return a string with the path relative to the application.
+ *
+ * @see javax.servlet.http.HttpServletRequest#getPathInfo()
+ */
+ public String getRequestPathInfo();
+
+ /**
+ * Returns the maximum time interval, in seconds, that the session
+ * associated with this request will be kept open between client accesses.
+ *
+ * @return an integer specifying the number of seconds the session
+ * associated with this request remains open between client requests
+ *
+ * @see javax.servlet.http.HttpSession#getMaxInactiveInterval()
+ * @see javax.portlet.PortletSession#getMaxInactiveInterval()
+ */
+ public int getSessionMaxInactiveInterval();
+
+ /**
+ * Gets an attribute from the session associated with this request.
+ *
+ * @param name
+ * the name of the attribute
+ * @return the attribute value, or <code>null</code> if the attribute is not
+ * defined in the session
+ *
+ * @see javax.servlet.http.HttpSession#getAttribute(String)
+ * @see javax.portlet.PortletSession#getAttribute(String)
+ */
+ public Object getSessionAttribute(String name);
+
+ /**
+ * Saves an attribute value in the session associated with this request.
+ *
+ * @param name
+ * the name of the attribute
+ * @param attribute
+ * the attribute value
+ *
+ * @see javax.servlet.http.HttpSession#setAttribute(String, Object)
+ * @see javax.portlet.PortletSession#setAttribute(String, Object)
+ */
+ public void setSessionAttribute(String name, Object attribute);
+
+ /**
+ * Returns the MIME type of the body of the request, or null if the type is
+ * not known.
+ *
+ * @return a string containing the name of the MIME type of the request, or
+ * null if the type is not known
+ *
+ * @see javax.servlet.ServletRequest#getContentType()
+ * @see javax.portlet.ResourceRequest#getContentType()
+ *
+ */
+ public String getContentType();
+
+ /**
+ * Gets detailed information about the browser from which the request
+ * originated. This consists of information that is not available from
+ * normal HTTP requests, but requires additional information to be extracted
+ * for instance using javascript in the browser.
+ *
+ * This information is only guaranteed to be available in some special
+ * cases, for instance when {@link Application#getRoot} is called again
+ * after throwing {@link RootRequiresMoreInformationException} or in
+ * {@link Root#init(WrappedRequest)} for a Root class not annotated with
+ * {@link EagerInit}
+ *
+ * @return the browser details, or <code>null</code> if details are not
+ * available
+ *
+ * @see BrowserDetails
+ */
+ public BrowserDetails getBrowserDetails();
+
+ /**
+ * Gets locale information from the query, e.g. using the Accept-Language
+ * header.
+ *
+ * @return the preferred Locale
+ *
+ * @see ServletRequest#getLocale()
+ * @see PortletRequest#getLocale()
+ */
+ public Locale getLocale();
+
+ /**
+ * Returns the IP address from which the request came. This might also be
+ * the address of a proxy between the server and the original requester.
+ *
+ * @return a string containing the IP address, or <code>null</code> if the
+ * address is not available
+ *
+ * @see ServletRequest#getRemoteAddr()
+ */
+ public String getRemoteAddr();
+
+ /**
+ * Checks whether the request was made using a secure channel, e.g. using
+ * https.
+ *
+ * @return a boolean indicating if the request is secure
+ *
+ * @see ServletRequest#isSecure()
+ * @see PortletRequest#isSecure()
+ */
+ public boolean isSecure();
+
+ /**
+ * Gets the value of a request header, e.g. a http header for a
+ * {@link HttpServletRequest}.
+ *
+ * @param headerName
+ * the name of the header
+ * @return the header value, or <code>null</code> if the header is not
+ * present in the request
+ *
+ * @see HttpServletRequest#getHeader(String)
+ */
+ public String getHeader(String headerName);
+
+ /**
+ * Gets the deployment configuration for the context of this request.
+ *
+ * @return the deployment configuration
+ *
+ * @see DeploymentConfiguration
+ */
+ public DeploymentConfiguration getDeploymentConfiguration();
+
+}
diff --git a/server/src/com/vaadin/terminal/WrappedResponse.java b/server/src/com/vaadin/terminal/WrappedResponse.java
new file mode 100644
index 0000000000..995133a269
--- /dev/null
+++ b/server/src/com/vaadin/terminal/WrappedResponse.java
@@ -0,0 +1,147 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+
+import javax.portlet.MimeResponse;
+import javax.portlet.PortletResponse;
+import javax.portlet.ResourceResponse;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A generic response from the server, wrapping a more specific response type,
+ * e.g. HttpServletResponse or PortletResponse.
+ *
+ * @since 7.0
+ */
+public interface WrappedResponse extends Serializable {
+
+ /**
+ * Sets the (http) status code for the response. If you want to include an
+ * error message along the status code, use {@link #sendError(int, String)}
+ * instead.
+ *
+ * @param statusCode
+ * the status code to set
+ * @see HttpServletResponse#setStatus(int)
+ *
+ * @see ResourceResponse#HTTP_STATUS_CODE
+ */
+ public void setStatus(int statusCode);
+
+ /**
+ * Sets the content type of this response. If the content type including a
+ * charset is set before {@link #getWriter()} is invoked, the returned
+ * PrintWriter will automatically use the defined charset.
+ *
+ * @param contentType
+ * a string specifying the MIME type of the content
+ *
+ * @see ServletResponse#setContentType(String)
+ * @see MimeResponse#setContentType(String)
+ */
+ public void setContentType(String contentType);
+
+ /**
+ * Sets the value of a generic response header. If the header had already
+ * been set, the new value overwrites the previous one.
+ *
+ * @param name
+ * the name of the header
+ * @param value
+ * the header value.
+ *
+ * @see HttpServletResponse#setHeader(String, String)
+ * @see PortletResponse#setProperty(String, String)
+ */
+ public void setHeader(String name, String value);
+
+ /**
+ * Properly formats a timestamp as a date header. If the header had already
+ * been set, the new value overwrites the previous one.
+ *
+ * @param name
+ * the name of the header
+ * @param timestamp
+ * the number of milliseconds since epoch
+ *
+ * @see HttpServletResponse#setDateHeader(String, long)
+ */
+ public void setDateHeader(String name, long timestamp);
+
+ /**
+ * Returns a <code>OutputStream</code> for writing binary data in the
+ * response.
+ * <p>
+ * Either this method or getWriter() may be called to write the response,
+ * not both.
+ *
+ * @return a <code>OutputStream</code> for writing binary data
+ * @throws IOException
+ * if an input or output exception occurred
+ *
+ * @see #getWriter()
+ * @see ServletResponse#getOutputStream()
+ * @see MimeResponse#getPortletOutputStream()
+ */
+ public OutputStream getOutputStream() throws IOException;
+
+ /**
+ * Returns a <code>PrintWriter</code> object that can send character text to
+ * the client. The PrintWriter uses the character encoding defined using
+ * setContentType.
+ * <p>
+ * Either this method or getOutputStream() may be called to write the
+ * response, not both.
+ *
+ * @return a <code>PrintWriter</code> for writing character text
+ * @throws IOException
+ * if an input or output exception occurred
+ *
+ * @see #getOutputStream()
+ * @see ServletResponse#getWriter()
+ * @see MimeResponse#getWriter()
+ */
+ public PrintWriter getWriter() throws IOException;
+
+ /**
+ * Sets cache time in milliseconds, -1 means no cache at all. All required
+ * headers related to caching in the response are set based on the time.
+ *
+ * @param milliseconds
+ * Cache time in milliseconds
+ */
+ public void setCacheTime(long milliseconds);
+
+ /**
+ * Sends an error response to the client using the specified status code and
+ * clears the buffer. In some configurations, this can cause a predefined
+ * error page to be displayed.
+ *
+ * @param errorCode
+ * the HTTP status code
+ * @param message
+ * a message to accompany the error
+ * @throws IOException
+ * if an input or output exception occurs
+ *
+ * @see HttpServletResponse#sendError(int, String)
+ */
+ public void sendError(int errorCode, String message) throws IOException;
+
+ /**
+ * Gets the deployment configuration for the context of this response.
+ *
+ * @return the deployment configuration
+ *
+ * @see DeploymentConfiguration
+ */
+ public DeploymentConfiguration getDeploymentConfiguration();
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
new file mode 100644
index 0000000000..40958e2868
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
@@ -0,0 +1,1079 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+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.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.security.GeneralSecurityException;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.EventRequest;
+import javax.portlet.EventResponse;
+import javax.portlet.GenericPortlet;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletContext;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletResponse;
+import javax.portlet.PortletSession;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+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.PortalClassInvoker;
+import com.liferay.portal.kernel.util.PropsUtil;
+import com.vaadin.Application;
+import com.vaadin.Application.ApplicationStartEvent;
+import com.vaadin.Application.SystemMessages;
+import com.vaadin.RootRequiresMoreInformationException;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback;
+import com.vaadin.ui.Root;
+
+/**
+ * Portlet 2.0 base class. This replaces the servlet in servlet/portlet 1.0
+ * deployments and handles various portlet requests from the browser.
+ *
+ * TODO Document me!
+ *
+ * @author peholmst
+ */
+public abstract class AbstractApplicationPortlet extends GenericPortlet
+ implements Constants {
+
+ public static final String RESOURCE_URL_ID = "APP";
+
+ public static class WrappedHttpAndPortletRequest extends
+ WrappedPortletRequest {
+
+ public WrappedHttpAndPortletRequest(PortletRequest request,
+ HttpServletRequest originalRequest,
+ DeploymentConfiguration deploymentConfiguration) {
+ super(request, deploymentConfiguration);
+ this.originalRequest = originalRequest;
+ }
+
+ private final HttpServletRequest originalRequest;
+
+ @Override
+ public String getParameter(String name) {
+ String parameter = super.getParameter(name);
+ if (parameter == null) {
+ parameter = originalRequest.getParameter(name);
+ }
+ return parameter;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return originalRequest.getRemoteAddr();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ String header = super.getHeader(name);
+ if (header == null) {
+ header = originalRequest.getHeader(name);
+ }
+ return header;
+ }
+
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ Map<String, String[]> parameterMap = super.getParameterMap();
+ if (parameterMap == null) {
+ parameterMap = originalRequest.getParameterMap();
+ }
+ return parameterMap;
+ }
+ }
+
+ public static class WrappedGateinRequest extends
+ WrappedHttpAndPortletRequest {
+ public WrappedGateinRequest(PortletRequest request,
+ DeploymentConfiguration deploymentConfiguration) {
+ super(request, getOriginalRequest(request), deploymentConfiguration);
+ }
+
+ private static final HttpServletRequest getOriginalRequest(
+ PortletRequest request) {
+ try {
+ Method getRealReq = request.getClass().getMethod(
+ "getRealRequest");
+ HttpServletRequestWrapper origRequest = (HttpServletRequestWrapper) getRealReq
+ .invoke(request);
+ return origRequest;
+ } catch (Exception e) {
+ throw new IllegalStateException("GateIn request not detected",
+ e);
+ }
+ }
+ }
+
+ public static class WrappedLiferayRequest extends
+ WrappedHttpAndPortletRequest {
+
+ public WrappedLiferayRequest(PortletRequest request,
+ DeploymentConfiguration deploymentConfiguration) {
+ super(request, getOriginalRequest(request), deploymentConfiguration);
+ }
+
+ @Override
+ public String getPortalProperty(String name) {
+ return PropsUtil.get(name);
+ }
+
+ private static HttpServletRequest getOriginalRequest(
+ PortletRequest request) {
+ try {
+ // httpRequest = PortalUtil.getHttpServletRequest(request);
+ HttpServletRequest httpRequest = (HttpServletRequest) PortalClassInvoker
+ .invoke("com.liferay.portal.util.PortalUtil",
+ "getHttpServletRequest", request);
+
+ // httpRequest =
+ // PortalUtil.getOriginalServletRequest(httpRequest);
+ httpRequest = (HttpServletRequest) PortalClassInvoker.invoke(
+ "com.liferay.portal.util.PortalUtil",
+ "getOriginalServletRequest", httpRequest);
+ return httpRequest;
+ } catch (Exception e) {
+ throw new IllegalStateException("Liferay request not detected",
+ e);
+ }
+ }
+
+ }
+
+ public static class AbstractApplicationPortletWrapper implements Callback {
+
+ private final AbstractApplicationPortlet portlet;
+
+ public AbstractApplicationPortletWrapper(
+ AbstractApplicationPortlet portlet) {
+ this.portlet = portlet;
+ }
+
+ @Override
+ public void criticalNotification(WrappedRequest request,
+ WrappedResponse response, String cap, String msg,
+ String details, String outOfSyncURL) throws IOException {
+ portlet.criticalNotification(WrappedPortletRequest.cast(request),
+ (WrappedPortletResponse) 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.
+ */
+ public static final String PORTLET_PARAMETER_STYLE = "style";
+
+ /**
+ * This portal parameter is used to define the name of the Vaadin theme that
+ * is used for all Vaadin applications in the portal.
+ */
+ public static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme";
+
+ public static final String WRITE_AJAX_PAGE_SCRIPT_WIDGETSET_SHOULD_WRITE = "writeAjaxPageScriptWidgetsetShouldWrite";
+
+ // TODO some parts could be shared with AbstractApplicationServlet
+
+ // TODO Can we close the application when the portlet is removed? Do we know
+ // when the portlet is removed?
+
+ private boolean productionMode = false;
+
+ private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration(
+ getClass()) {
+ @Override
+ public String getConfiguredWidgetset(WrappedRequest request) {
+
+ String widgetset = getApplicationOrSystemProperty(
+ PARAMETER_WIDGETSET, null);
+
+ if (widgetset == null) {
+ // If no widgetset defined for the application, check the
+ // portal
+ // property
+ widgetset = WrappedPortletRequest.cast(request)
+ .getPortalProperty(PORTAL_PARAMETER_VAADIN_WIDGETSET);
+ }
+
+ if (widgetset == null) {
+ // If no widgetset defined for the portal, use the default
+ widgetset = DEFAULT_WIDGETSET;
+ }
+
+ return widgetset;
+ }
+
+ @Override
+ public String getConfiguredTheme(WrappedRequest request) {
+
+ // is the default theme defined by the portal?
+ String themeName = WrappedPortletRequest.cast(request)
+ .getPortalProperty(Constants.PORTAL_PARAMETER_VAADIN_THEME);
+
+ if (themeName == null) {
+ // no, using the default theme defined by Vaadin
+ themeName = DEFAULT_THEME_NAME;
+ }
+
+ return themeName;
+ }
+
+ @Override
+ public boolean isStandalone(WrappedRequest request) {
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.DeploymentConfiguration#getStaticFileLocation
+ * (com.vaadin.terminal.WrappedRequest)
+ *
+ * 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.
+ *
+ * @return The location of static resources (inside which there should
+ * be a VAADIN directory). Does not end with a slash (/).
+ */
+
+ @Override
+ public String getStaticFileLocation(WrappedRequest request) {
+ String staticFileLocation = WrappedPortletRequest.cast(request)
+ .getPortalProperty(
+ Constants.PORTAL_PARAMETER_VAADIN_RESOURCE_PATH);
+ if (staticFileLocation != null) {
+ // remove trailing slash if any
+ while (staticFileLocation.endsWith(".")) {
+ staticFileLocation = staticFileLocation.substring(0,
+ staticFileLocation.length() - 1);
+ }
+ return staticFileLocation;
+ } else {
+ // default for Liferay
+ return "/html";
+ }
+ }
+
+ @Override
+ public String getMimeType(String resourceName) {
+ return getPortletContext().getMimeType(resourceName);
+ }
+ };
+
+ private final AddonContext addonContext = new AddonContext(
+ getDeploymentConfiguration());
+
+ @Override
+ public void init(PortletConfig config) throws PortletException {
+ super.init(config);
+ Properties applicationProperties = getDeploymentConfiguration()
+ .getInitParameters();
+
+ // Read default parameters from the context
+ final PortletContext context = config.getPortletContext();
+ for (final Enumeration<String> e = context.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = e.nextElement();
+ applicationProperties.setProperty(name,
+ context.getInitParameter(name));
+ }
+
+ // Override with application settings from portlet.xml
+ for (final Enumeration<String> e = config.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = e.nextElement();
+ applicationProperties.setProperty(name,
+ config.getInitParameter(name));
+ }
+
+ checkProductionMode();
+ checkCrossSiteProtection();
+
+ addonContext.init();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+
+ addonContext.destroy();
+ }
+
+ private void checkCrossSiteProtection() {
+ if (getDeploymentConfiguration().getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals(
+ "true")) {
+ /*
+ * Print an information/warning message about running with xsrf
+ * protection disabled
+ */
+ getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED);
+ }
+ }
+
+ private void checkProductionMode() {
+ // TODO Identical code in AbstractApplicationServlet -> refactor
+ // Check if the application is in production mode.
+ // We are in production mode if productionMode=true
+ if (getDeploymentConfiguration().getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) {
+ productionMode = true;
+ }
+
+ if (!productionMode) {
+ /* Print an information/warning message about running in debug mode */
+ // TODO Maybe we need a different message for portlets?
+ getLogger().warning(NOT_PRODUCTION_MODE_INFO);
+ }
+ }
+
+ protected enum RequestType {
+ FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE;
+ }
+
+ protected RequestType getRequestType(WrappedPortletRequest wrappedRequest) {
+ PortletRequest request = wrappedRequest.getPortletRequest();
+ if (request instanceof RenderRequest) {
+ return RequestType.RENDER;
+ } else if (request instanceof ResourceRequest) {
+ ResourceRequest resourceRequest = (ResourceRequest) request;
+ if (ServletPortletHelper.isUIDLRequest(wrappedRequest)) {
+ return RequestType.UIDL;
+ } else if (isBrowserDetailsRequest(resourceRequest)) {
+ return RequestType.BROWSER_DETAILS;
+ } else if (ServletPortletHelper.isFileUploadRequest(wrappedRequest)) {
+ return RequestType.FILE_UPLOAD;
+ } else if (ServletPortletHelper
+ .isConnectorResourceRequest(wrappedRequest)) {
+ return RequestType.CONNECTOR_RESOURCE;
+ } else if (ServletPortletHelper
+ .isApplicationResourceRequest(wrappedRequest)) {
+ return RequestType.APPLICATION_RESOURCE;
+ } else if (isDummyRequest(resourceRequest)) {
+ return RequestType.DUMMY;
+ } else {
+ return RequestType.STATIC_FILE;
+ }
+ } else if (request instanceof ActionRequest) {
+ return RequestType.ACTION;
+ } else if (request instanceof EventRequest) {
+ return RequestType.EVENT;
+ }
+ return RequestType.UNKNOWN;
+ }
+
+ private boolean isBrowserDetailsRequest(ResourceRequest request) {
+ return request.getResourceID() != null
+ && request.getResourceID().equals("browserDetails");
+ }
+
+ private boolean isDummyRequest(ResourceRequest request) {
+ return request.getResourceID() != null
+ && request.getResourceID().equals("DUMMY");
+ }
+
+ /**
+ * Returns true if the servlet is running in production mode. Production
+ * mode disables all debug facilities.
+ *
+ * @return true if in production mode, false if in debug mode
+ */
+ public boolean isProductionMode() {
+ return productionMode;
+ }
+
+ protected void handleRequest(PortletRequest request,
+ PortletResponse response) throws PortletException, IOException {
+ RequestTimer requestTimer = new RequestTimer();
+ requestTimer.start();
+
+ AbstractApplicationPortletWrapper portletWrapper = new AbstractApplicationPortletWrapper(
+ this);
+
+ WrappedPortletRequest wrappedRequest = createWrappedRequest(request);
+
+ WrappedPortletResponse wrappedResponse = new WrappedPortletResponse(
+ response, getDeploymentConfiguration());
+
+ RequestType requestType = getRequestType(wrappedRequest);
+
+ 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 if (requestType == RequestType.STATIC_FILE) {
+ serveStaticResources((ResourceRequest) request,
+ (ResourceResponse) response);
+ } else {
+ Application application = null;
+ boolean transactionStarted = false;
+ boolean requestStarted = false;
+
+ try {
+ // TODO What about PARAM_UNLOADBURST & redirectToApplication??
+
+ /* Find out which application this request is related to */
+ application = findApplicationInstance(wrappedRequest,
+ requestType);
+ if (application == null) {
+ return;
+ }
+ Application.setCurrent(application);
+
+ /*
+ * Get or create an application context and an application
+ * manager for the session
+ */
+ PortletApplicationContext2 applicationContext = getApplicationContext(request
+ .getPortletSession());
+ applicationContext.setResponse(response);
+ applicationContext.setPortletConfig(getPortletConfig());
+
+ PortletCommunicationManager applicationManager = applicationContext
+ .getApplicationManager(application);
+
+ if (requestType == RequestType.CONNECTOR_RESOURCE) {
+ applicationManager.serveConnectorResource(wrappedRequest,
+ wrappedResponse);
+ return;
+ }
+
+ /* Update browser information from request */
+ applicationContext.getBrowser().updateRequestDetails(
+ wrappedRequest);
+
+ /*
+ * Call application requestStart before Application.init() is
+ * called (bypasses the limitation in TransactionListener)
+ */
+ if (application instanceof PortletRequestListener) {
+ ((PortletRequestListener) application).onRequestStart(
+ request, response);
+ requestStarted = true;
+ }
+
+ /* Start the newly created application */
+ startApplication(request, application, applicationContext);
+
+ /*
+ * Transaction starts. Call transaction listeners. Transaction
+ * end is called in the finally block below.
+ */
+ applicationContext.startTransaction(application, request);
+ transactionStarted = true;
+
+ /* Notify listeners */
+
+ // Finds the window within the application
+ Root root = null;
+ synchronized (application) {
+ if (application.isRunning()) {
+ switch (requestType) {
+ case RENDER:
+ case ACTION:
+ // Both action requests and render requests are ok
+ // without a Root as they render the initial HTML
+ // and then do a second request
+ try {
+ root = application
+ .getRootForRequest(wrappedRequest);
+ } catch (RootRequiresMoreInformationException e) {
+ // Ignore problem and continue without root
+ }
+ break;
+ case BROWSER_DETAILS:
+ // Should not try to find a root here as the
+ // combined request details might change the root
+ break;
+ case FILE_UPLOAD:
+ // no window
+ break;
+ case APPLICATION_RESOURCE:
+ // use main window - should not need any window
+ // root = application.getRoot();
+ break;
+ default:
+ root = application
+ .getRootForRequest(wrappedRequest);
+ }
+ // if window not found, not a problem - use null
+ }
+ }
+
+ // TODO Should this happen before or after the transaction
+ // starts?
+ if (request instanceof RenderRequest) {
+ applicationContext.firePortletRenderRequest(application,
+ root, (RenderRequest) request,
+ (RenderResponse) response);
+ } else if (request instanceof ActionRequest) {
+ applicationContext.firePortletActionRequest(application,
+ root, (ActionRequest) request,
+ (ActionResponse) response);
+ } else if (request instanceof EventRequest) {
+ applicationContext.firePortletEventRequest(application,
+ root, (EventRequest) request,
+ (EventResponse) response);
+ } else if (request instanceof ResourceRequest) {
+ applicationContext.firePortletResourceRequest(application,
+ root, (ResourceRequest) request,
+ (ResourceResponse) response);
+ }
+
+ /* Handle the request */
+ if (requestType == RequestType.FILE_UPLOAD) {
+ // Root is resolved in handleFileUpload by
+ // PortletCommunicationManager
+ applicationManager.handleFileUpload(application,
+ wrappedRequest, wrappedResponse);
+ return;
+ } else if (requestType == RequestType.BROWSER_DETAILS) {
+ applicationManager.handleBrowserDetailsRequest(
+ wrappedRequest, wrappedResponse, application);
+ return;
+ } else if (requestType == RequestType.UIDL) {
+ // Handles AJAX UIDL requests
+ applicationManager.handleUidlRequest(wrappedRequest,
+ wrappedResponse, portletWrapper, root);
+ return;
+ } else {
+ /*
+ * Removes the application if it has stopped
+ */
+ if (!application.isRunning()) {
+ endApplication(request, response, application);
+ return;
+ }
+
+ handleOtherRequest(wrappedRequest, wrappedResponse,
+ requestType, application, applicationContext,
+ applicationManager);
+ }
+ } 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(wrappedRequest, wrappedResponse,
+ application, e);
+ } finally {
+ // Notifies transaction end
+ try {
+ if (transactionStarted) {
+ ((PortletApplicationContext2) application.getContext())
+ .endTransaction(application, request);
+ }
+ } finally {
+ try {
+ if (requestStarted) {
+ ((PortletRequestListener) application)
+ .onRequestEnd(request, response);
+
+ }
+ } finally {
+ Root.setCurrent(null);
+ Application.setCurrent(null);
+
+ PortletSession session = request
+ .getPortletSession(false);
+ if (session != null) {
+ requestTimer.stop(getApplicationContext(session));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Wraps the request in a (possibly portal specific) wrapped portlet
+ * request.
+ *
+ * @param request
+ * The original PortletRequest
+ * @return A wrapped version of the PorletRequest
+ */
+ protected WrappedPortletRequest createWrappedRequest(PortletRequest request) {
+ String portalInfo = request.getPortalContext().getPortalInfo()
+ .toLowerCase();
+ if (portalInfo.contains("liferay")) {
+ return new WrappedLiferayRequest(request,
+ getDeploymentConfiguration());
+ } else if (portalInfo.contains("gatein")) {
+ return new WrappedGateinRequest(request,
+ getDeploymentConfiguration());
+ } else {
+ return new WrappedPortletRequest(request,
+ getDeploymentConfiguration());
+ }
+
+ }
+
+ protected DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+
+ private void handleUnknownRequest(PortletRequest request,
+ PortletResponse response) {
+ getLogger().warning("Unknown request type");
+ }
+
+ /**
+ * 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 application
+ * @param applicationContext
+ * @param applicationManager
+ * @throws PortletException
+ * @throws IOException
+ * @throws MalformedURLException
+ */
+ private void handleOtherRequest(WrappedPortletRequest request,
+ WrappedResponse response, RequestType requestType,
+ Application application,
+ PortletApplicationContext2 applicationContext,
+ PortletCommunicationManager applicationManager)
+ throws PortletException, IOException, MalformedURLException {
+ if (requestType == RequestType.APPLICATION_RESOURCE
+ || requestType == RequestType.RENDER) {
+ if (!applicationManager.handleApplicationRequest(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!");
+ }
+ }
+
+ @Override
+ public void processEvent(EventRequest request, EventResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ private void serveStaticResources(ResourceRequest request,
+ ResourceResponse response) throws IOException, PortletException {
+ final String resourceID = request.getResourceID();
+ final PortletContext pc = getPortletContext();
+
+ InputStream is = pc.getResourceAsStream(resourceID);
+ if (is != null) {
+ final String mimetype = pc.getMimeType(resourceID);
+ if (mimetype != null) {
+ response.setContentType(mimetype);
+ }
+ final OutputStream os = response.getPortletOutputStream();
+ final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE];
+ int bytes;
+ while ((bytes = is.read(buffer)) >= 0) {
+ os.write(buffer, 0, bytes);
+ }
+ } else {
+ getLogger().info(
+ "Requested resource [" + resourceID
+ + "] could not be found");
+ response.setProperty(ResourceResponse.HTTP_STATUS_CODE,
+ Integer.toString(HttpServletResponse.SC_NOT_FOUND));
+ }
+ }
+
+ @Override
+ public void processAction(ActionRequest request, ActionResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ @Override
+ protected void doDispatch(RenderRequest request, RenderResponse response)
+ throws PortletException, IOException {
+ try {
+ // try to let super handle - it'll call methods annotated for
+ // handling, the default doXYZ(), or throw if a handler for the mode
+ // is not found
+ super.doDispatch(request, response);
+
+ } catch (PortletException e) {
+ if (e.getCause() == null) {
+ // No cause interpreted as 'unknown mode' - pass that trough
+ // so that the application can handle
+ handleRequest(request, response);
+
+ } else {
+ // Something else failed, pass on
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void serveResource(ResourceRequest request, ResourceResponse response)
+ throws PortletException, IOException {
+ handleRequest(request, response);
+ }
+
+ boolean requestCanCreateApplication(PortletRequest request,
+ RequestType requestType) {
+ if (requestType == RequestType.UIDL && isRepaintAll(request)) {
+ return true;
+ } else if (requestType == RequestType.RENDER) {
+ // In most cases the first request is a render request that renders
+ // the HTML fragment. This should create an application instance.
+ return true;
+ } else if (requestType == RequestType.EVENT) {
+ // 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.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isRepaintAll(PortletRequest request) {
+ return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null)
+ && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1"));
+ }
+
+ private void startApplication(PortletRequest request,
+ Application application, PortletApplicationContext2 context)
+ throws PortletException, MalformedURLException {
+ if (!application.isRunning()) {
+ Locale locale = request.getLocale();
+ application.setLocale(locale);
+ // No application URL when running inside a portlet
+ application.start(new ApplicationStartEvent(null,
+ getDeploymentConfiguration().getInitParameters(), context,
+ isProductionMode()));
+ addonContext.applicationStarted(application);
+ }
+ }
+
+ private void endApplication(PortletRequest request,
+ PortletResponse response, Application application)
+ throws IOException {
+ final PortletSession session = request.getPortletSession();
+ if (session != null) {
+ getApplicationContext(session).removeApplication(application);
+ }
+ // Do not send any redirects when running inside a portlet.
+ }
+
+ private Application findApplicationInstance(
+ WrappedPortletRequest wrappedRequest, RequestType requestType)
+ throws PortletException, SessionExpiredException,
+ MalformedURLException {
+ PortletRequest request = wrappedRequest.getPortletRequest();
+
+ boolean requestCanCreateApplication = requestCanCreateApplication(
+ request, requestType);
+
+ /* Find an existing application for this request. */
+ Application application = getExistingApplication(request,
+ requestCanCreateApplication);
+
+ if (application != null) {
+ /*
+ * There is an existing application. We can use this as long as the
+ * user not specifically requested to close or restart it.
+ */
+
+ final boolean restartApplication = (wrappedRequest
+ .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null);
+ final boolean closeApplication = (wrappedRequest
+ .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null);
+
+ if (restartApplication) {
+ closeApplication(application, request.getPortletSession(false));
+ return createApplication(request);
+ } else if (closeApplication) {
+ closeApplication(application, request.getPortletSession(false));
+ return null;
+ } else {
+ return application;
+ }
+ }
+
+ // No existing application was found
+
+ if (requestCanCreateApplication) {
+ return createApplication(request);
+ } else {
+ throw new SessionExpiredException();
+ }
+ }
+
+ private void closeApplication(Application application,
+ PortletSession session) {
+ if (application == null) {
+ return;
+ }
+
+ application.close();
+ if (session != null) {
+ PortletApplicationContext2 context = getApplicationContext(session);
+ context.removeApplication(application);
+ }
+ }
+
+ private Application createApplication(PortletRequest request)
+ throws PortletException, MalformedURLException {
+ Application newApplication = getNewApplication(request);
+ final PortletApplicationContext2 context = getApplicationContext(request
+ .getPortletSession());
+ context.addApplication(newApplication, request.getWindowID());
+ return newApplication;
+ }
+
+ private Application getExistingApplication(PortletRequest request,
+ boolean allowSessionCreation) throws MalformedURLException,
+ SessionExpiredException {
+
+ final PortletSession session = request
+ .getPortletSession(allowSessionCreation);
+
+ if (session == null) {
+ throw new SessionExpiredException();
+ }
+
+ PortletApplicationContext2 context = getApplicationContext(session);
+ Application application = context.getApplicationForWindowId(request
+ .getWindowID());
+ if (application == null) {
+ return null;
+ }
+ if (application.isRunning()) {
+ return application;
+ }
+ // application found but not running
+ context.removeApplication(application);
+
+ return null;
+ }
+
+ protected abstract Class<? extends Application> getApplicationClass()
+ throws ClassNotFoundException;
+
+ protected Application getNewApplication(PortletRequest request)
+ throws PortletException {
+ try {
+ final Application application = getApplicationClass().newInstance();
+ application.setRootPreserved(true);
+ return application;
+ } catch (final IllegalAccessException e) {
+ throw new PortletException("getNewApplication failed", e);
+ } catch (final InstantiationException e) {
+ throw new PortletException("getNewApplication failed", e);
+ } catch (final ClassNotFoundException e) {
+ throw new PortletException("getNewApplication failed", e);
+ }
+ }
+
+ /**
+ * Get system messages from the current application class
+ *
+ * @return
+ */
+ protected SystemMessages getSystemMessages() {
+ try {
+ Class<? extends Application> appCls = getApplicationClass();
+ Method m = appCls.getMethod("getSystemMessages", (Class[]) null);
+ return (Application.SystemMessages) m.invoke(null, (Object[]) null);
+ } catch (ClassNotFoundException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ } catch (SecurityException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (NoSuchMethodException e) {
+ // This is completely ok and should be silently ignored
+ } catch (IllegalArgumentException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ } catch (IllegalAccessException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (InvocationTargetException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ }
+ return Application.getSystemMessages();
+ }
+
+ private void handleServiceException(WrappedPortletRequest request,
+ WrappedPortletResponse response, Application application,
+ 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
+ if (getRequestType(request) == RequestType.UIDL) {
+ Application.SystemMessages ci = getSystemMessages();
+ criticalNotification(request, response,
+ ci.getInternalErrorCaption(), ci.getInternalErrorMessage(),
+ null, ci.getInternalErrorURL());
+ if (application != null) {
+ application.getErrorHandler()
+ .terminalError(new RequestError(e));
+ } else {
+ throw new PortletException(e);
+ }
+ } else {
+ // Re-throw other exceptions
+ throw new PortletException(e);
+ }
+
+ }
+
+ @SuppressWarnings("serial")
+ public class RequestError implements Terminal.ErrorEvent, Serializable {
+
+ private final Throwable throwable;
+
+ public RequestError(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+
+ /**
+ * 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.
+ */
+ void criticalNotification(WrappedPortletRequest request,
+ WrappedPortletResponse 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();
+ }
+
+ /**
+ *
+ * Gets the application context for a PortletSession. If no context is
+ * currently stored in a session a new context is created and stored in the
+ * session.
+ *
+ * @param portletSession
+ * the portlet session.
+ * @return the application context for the session.
+ */
+ protected PortletApplicationContext2 getApplicationContext(
+ PortletSession portletSession) {
+ return PortletApplicationContext2.getApplicationContext(portletSession);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(AbstractApplicationPortlet.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
new file mode 100644
index 0000000000..603bc74a21
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
@@ -0,0 +1,1623 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+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.InvocationTargetException;
+import java.lang.reflect.Method;
+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;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.logging.Level;
+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 javax.servlet.http.HttpSession;
+
+import com.vaadin.Application;
+import com.vaadin.Application.ApplicationStartEvent;
+import com.vaadin.Application.SystemMessages;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.ThemeResource;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.server.AbstractCommunicationManager.Callback;
+import com.vaadin.ui.Root;
+
+/**
+ * Abstract implementation of the ApplicationServlet which handles all
+ * communication between the client and the server.
+ *
+ * It is possible to extend this class to provide own functionality but in most
+ * cases this is unnecessary.
+ *
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.0
+ */
+
+@SuppressWarnings("serial")
+public abstract class AbstractApplicationServlet extends HttpServlet implements
+ Constants {
+
+ private static class AbstractApplicationServletWrapper implements Callback {
+
+ private final AbstractApplicationServlet servlet;
+
+ public AbstractApplicationServletWrapper(
+ AbstractApplicationServlet servlet) {
+ this.servlet = servlet;
+ }
+
+ @Override
+ public void criticalNotification(WrappedRequest request,
+ WrappedResponse response, String cap, String msg,
+ String details, String outOfSyncURL) throws IOException {
+ servlet.criticalNotification(
+ WrappedHttpServletRequest.cast(request),
+ ((WrappedHttpServletResponse) response), cap, msg, details,
+ outOfSyncURL);
+ }
+ }
+
+ // TODO Move some (all?) of the constants to a separate interface (shared
+ // with portlet)
+
+ private boolean productionMode = false;
+
+ private final String resourcePath = null;
+
+ private int resourceCacheTime = 3600;
+
+ private DeploymentConfiguration deploymentConfiguration = new AbstractDeploymentConfiguration(
+ getClass()) {
+
+ @Override
+ public String getStaticFileLocation(WrappedRequest request) {
+ HttpServletRequest servletRequest = WrappedHttpServletRequest
+ .cast(request);
+ return AbstractApplicationServlet.this
+ .getStaticFilesLocation(servletRequest);
+ }
+
+ @Override
+ public String getConfiguredWidgetset(WrappedRequest request) {
+ return getApplicationOrSystemProperty(
+ AbstractApplicationServlet.PARAMETER_WIDGETSET,
+ AbstractApplicationServlet.DEFAULT_WIDGETSET);
+ }
+
+ @Override
+ public String getConfiguredTheme(WrappedRequest request) {
+ // Use the default
+ return AbstractApplicationServlet.getDefaultTheme();
+ }
+
+ @Override
+ public boolean isStandalone(WrappedRequest request) {
+ return true;
+ }
+
+ @Override
+ public String getMimeType(String resourceName) {
+ return getServletContext().getMimeType(resourceName);
+ }
+ };
+
+ private final AddonContext addonContext = new AddonContext(
+ getDeploymentConfiguration());
+
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being placed into service.
+ *
+ * @param servletConfig
+ * the object containing the servlet's configuration and
+ * initialization parameters
+ * @throws javax.servlet.ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ @Override
+ public void init(javax.servlet.ServletConfig servletConfig)
+ throws javax.servlet.ServletException {
+ super.init(servletConfig);
+ Properties applicationProperties = getDeploymentConfiguration()
+ .getInitParameters();
+
+ // Read default parameters from server.xml
+ final ServletContext context = servletConfig.getServletContext();
+ for (final Enumeration<String> e = context.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = e.nextElement();
+ applicationProperties.setProperty(name,
+ context.getInitParameter(name));
+ }
+
+ // Override with application config from web.xml
+ for (final Enumeration<String> e = servletConfig
+ .getInitParameterNames(); e.hasMoreElements();) {
+ final String name = e.nextElement();
+ applicationProperties.setProperty(name,
+ servletConfig.getInitParameter(name));
+ }
+
+ checkProductionMode();
+ checkCrossSiteProtection();
+ checkResourceCacheTime();
+
+ addonContext.init();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+
+ addonContext.destroy();
+ }
+
+ private void checkCrossSiteProtection() {
+ if (getDeploymentConfiguration().getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION, "false").equals(
+ "true")) {
+ /*
+ * Print an information/warning message about running with xsrf
+ * protection disabled
+ */
+ getLogger().warning(WARNING_XSRF_PROTECTION_DISABLED);
+ }
+ }
+
+ private void checkProductionMode() {
+ // Check if the application is in production mode.
+ // We are in production mode if productionMode=true
+ if (getDeploymentConfiguration().getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) {
+ productionMode = true;
+ }
+
+ if (!productionMode) {
+ /* Print an information/warning message about running in debug mode */
+ getLogger().warning(NOT_PRODUCTION_MODE_INFO);
+ }
+
+ }
+
+ private void checkResourceCacheTime() {
+ // Check if the browser caching time has been set in web.xml
+ try {
+ String rct = getDeploymentConfiguration()
+ .getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_RESOURCE_CACHE_TIME, "3600");
+ resourceCacheTime = Integer.parseInt(rct);
+ } catch (NumberFormatException nfe) {
+ // Default is 1h
+ resourceCacheTime = 3600;
+ getLogger().warning(WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC);
+ }
+ }
+
+ /**
+ * Returns true if the servlet is running in production mode. Production
+ * mode disables all debug facilities.
+ *
+ * @return true if in production mode, false if in debug mode
+ */
+ public boolean isProductionMode() {
+ return productionMode;
+ }
+
+ /**
+ * Returns the amount of milliseconds the browser should cache a file.
+ * Default is 1 hour (3600 ms).
+ *
+ * @return The amount of milliseconds files are cached in the browser
+ */
+ public int getResourceCacheTime() {
+ return resourceCacheTime;
+ }
+
+ /**
+ * Receives standard HTTP requests from the public service method and
+ * dispatches them.
+ *
+ * @param request
+ * the object that contains the request the client made of the
+ * servlet.
+ * @param response
+ * the object that contains the response the servlet returns to
+ * the client.
+ * @throws ServletException
+ * if an input or output error occurs while the servlet is
+ * handling the TRACE request.
+ * @throws IOException
+ * if the request for the TRACE cannot be handled.
+ */
+
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ service(createWrappedRequest(request), createWrappedResponse(response));
+ }
+
+ private void service(WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response) throws ServletException,
+ IOException {
+ RequestTimer requestTimer = new RequestTimer();
+ requestTimer.start();
+
+ AbstractApplicationServletWrapper servletWrapper = new AbstractApplicationServletWrapper(
+ this);
+
+ RequestType requestType = getRequestType(request);
+ if (!ensureCookiesEnabled(requestType, request, response)) {
+ return;
+ }
+
+ if (requestType == RequestType.STATIC_FILE) {
+ serveStaticResources(request, response);
+ return;
+ }
+
+ Application application = null;
+ boolean transactionStarted = false;
+ boolean requestStarted = false;
+
+ 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(
+ ApplicationConnection.PARAM_UNLOADBURST)
+ && request.getContentLength() < 1
+ && getExistingApplication(request, false) == null) {
+ redirectToApplication(request, response);
+ return;
+ }
+
+ // Find out which application this request is related to
+ application = findApplicationInstance(request, requestType);
+ if (application == null) {
+ return;
+ }
+ Application.setCurrent(application);
+
+ /*
+ * Get or create a WebApplicationContext and an ApplicationManager
+ * for the session
+ */
+ WebApplicationContext webApplicationContext = getApplicationContext(request
+ .getSession());
+ CommunicationManager applicationManager = webApplicationContext
+ .getApplicationManager(application, this);
+
+ if (requestType == RequestType.CONNECTOR_RESOURCE) {
+ applicationManager.serveConnectorResource(request, response);
+ return;
+ }
+
+ /* Update browser information from the request */
+ webApplicationContext.getBrowser().updateRequestDetails(request);
+
+ /*
+ * Call application requestStart before Application.init() is called
+ * (bypasses the limitation in TransactionListener)
+ */
+ if (application instanceof HttpServletRequestListener) {
+ ((HttpServletRequestListener) application).onRequestStart(
+ request, response);
+ requestStarted = true;
+ }
+
+ // Start the application if it's newly created
+ startApplication(request, application, webApplicationContext);
+
+ /*
+ * Transaction starts. Call transaction listeners. Transaction end
+ * is called in the finally block below.
+ */
+ webApplicationContext.startTransaction(application, request);
+ transactionStarted = true;
+
+ /* Handle the request */
+ if (requestType == RequestType.FILE_UPLOAD) {
+ // Root is resolved in communication manager
+ applicationManager.handleFileUpload(application, request,
+ response);
+ return;
+ } else if (requestType == RequestType.UIDL) {
+ Root root = application.getRootForRequest(request);
+ if (root == null) {
+ throw new ServletException(ERROR_NO_ROOT_FOUND);
+ }
+ // Handles AJAX UIDL requests
+ applicationManager.handleUidlRequest(request, response,
+ servletWrapper, root);
+ return;
+ } else if (requestType == RequestType.BROWSER_DETAILS) {
+ // Browser details - not related to a specific root
+ applicationManager.handleBrowserDetailsRequest(request,
+ response, application);
+ return;
+ }
+
+ // Removes application if it has stopped (maybe by thread or
+ // transactionlistener)
+ if (!application.isRunning()) {
+ endApplication(request, response, application);
+ return;
+ }
+
+ if (applicationManager.handleApplicationRequest(request, response)) {
+ return;
+ }
+ // TODO Should return 404 error here and not do anything more
+
+ } 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, application, e);
+ } finally {
+ // Notifies transaction end
+ try {
+ if (transactionStarted) {
+ ((WebApplicationContext) application.getContext())
+ .endTransaction(application, request);
+
+ }
+
+ } finally {
+ try {
+ if (requestStarted) {
+ ((HttpServletRequestListener) application)
+ .onRequestEnd(request, response);
+ }
+ } finally {
+ Root.setCurrent(null);
+ Application.setCurrent(null);
+
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ requestTimer.stop(getApplicationContext(session));
+ }
+ }
+ }
+
+ }
+ }
+
+ private WrappedHttpServletResponse createWrappedResponse(
+ HttpServletResponse response) {
+ WrappedHttpServletResponse wrappedResponse = new WrappedHttpServletResponse(
+ response, getDeploymentConfiguration());
+ return wrappedResponse;
+ }
+
+ /**
+ * Create a wrapped request for a http servlet request. This method can be
+ * overridden if the wrapped request should have special properties.
+ *
+ * @param request
+ * the original http servlet request
+ * @return a wrapped request for the original request
+ */
+ protected WrappedHttpServletRequest createWrappedRequest(
+ HttpServletRequest request) {
+ return new WrappedHttpServletRequest(request,
+ getDeploymentConfiguration());
+ }
+
+ /**
+ * Gets a the deployment configuration for this servlet.
+ *
+ * @return the deployment configuration
+ */
+ protected DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+
+ /**
+ * Check that cookie support is enabled in the browser. Only checks UIDL
+ * requests.
+ *
+ * @param requestType
+ * Type of the request as returned by
+ * {@link #getRequestType(HttpServletRequest)}
+ * @param request
+ * The request from the browser
+ * @param response
+ * The response to which an error can be written
+ * @return false if cookies are disabled, true otherwise
+ * @throws IOException
+ */
+ private boolean ensureCookiesEnabled(RequestType requestType,
+ WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response) throws IOException {
+ if (requestType == RequestType.UIDL && !isRepaintAll(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
+ if (request.getRequestedSessionId() == null) {
+ // User has cookies disabled
+ criticalNotification(request, response, getSystemMessages()
+ .getCookiesDisabledCaption(), getSystemMessages()
+ .getCookiesDisabledMessage(), null, getSystemMessages()
+ .getCookiesDisabledURL());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Send a notification to client's application. Used to notify client of
+ * critical errors, session expiration and more. Server has no knowledge of
+ * what application client refers to.
+ *
+ * @param request
+ * the HTTP request instance.
+ * @param response
+ * the HTTP response to write to.
+ * @param caption
+ * the notification caption
+ * @param message
+ * to notification body
+ * @param details
+ * a detail message to show in addition to the message. Currently
+ * shown directly below the message but could be hidden behind a
+ * details drop down in the future. Mainly used to give
+ * additional information not necessarily useful to the end user.
+ * @param url
+ * url to load when the message is dismissed. Null will reload
+ * the current page.
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ protected void criticalNotification(WrappedHttpServletRequest request,
+ HttpServletResponse 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);
+ } else {
+ // Create an HTML reponse with the error
+ String output = "";
+
+ if (url != null) {
+ output += "<a href=\"" + url + "\">";
+ }
+ if (caption != null) {
+ output += "<b>" + caption + "</b><br/>";
+ }
+ if (message != null) {
+ output += message;
+ output += "<br/><br/>";
+ }
+
+ if (details != null) {
+ output += details;
+ output += "<br/><br/>";
+ }
+ if (url != null) {
+ output += "</a>";
+ }
+ writeResponse(response, "text/html; charset=UTF-8", output);
+
+ }
+
+ }
+
+ /**
+ * Writes the response in {@code output} using the contentType given in
+ * {@code contentType} to the provided {@link HttpServletResponse}
+ *
+ * @param response
+ * @param contentType
+ * @param output
+ * Output to write (UTF-8 encoded)
+ * @throws IOException
+ */
+ private void writeResponse(HttpServletResponse response,
+ String contentType, String output) throws IOException {
+ response.setContentType(contentType);
+ final ServletOutputStream out = response.getOutputStream();
+ // Set the response type
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+ outWriter.print(output);
+ outWriter.flush();
+ outWriter.close();
+ out.flush();
+
+ }
+
+ /**
+ * Returns the application instance to be used for the request. If an
+ * existing instance is not found a new one is created or null is returned
+ * to indicate that the application is not available.
+ *
+ * @param request
+ * @param requestType
+ * @return
+ * @throws MalformedURLException
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ * @throws ServletException
+ * @throws SessionExpiredException
+ */
+ private Application findApplicationInstance(HttpServletRequest request,
+ RequestType requestType) throws MalformedURLException,
+ ServletException, SessionExpiredException {
+
+ boolean requestCanCreateApplication = requestCanCreateApplication(
+ request, requestType);
+
+ /* Find an existing application for this request. */
+ Application application = getExistingApplication(request,
+ requestCanCreateApplication);
+
+ if (application != null) {
+ /*
+ * There is an existing application. We can use this as long as the
+ * user 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);
+
+ if (restartApplication) {
+ closeApplication(application, request.getSession(false));
+ return createApplication(request);
+ } else if (closeApplication) {
+ closeApplication(application, request.getSession(false));
+ return null;
+ } else {
+ return application;
+ }
+ }
+
+ // No existing application was found
+
+ if (requestCanCreateApplication) {
+ /*
+ * If the request is such that it should create a new application if
+ * one as not found, we do that.
+ */
+ return createApplication(request);
+ } else {
+ /*
+ * The application was not found and a new one should not be
+ * created. Assume the session has expired.
+ */
+ throw new SessionExpiredException();
+ }
+
+ }
+
+ /**
+ * Check if the request should create an application if an existing
+ * application is not found.
+ *
+ * @param request
+ * @param requestType
+ * @return true if an application should be created, false otherwise
+ */
+ boolean requestCanCreateApplication(HttpServletRequest request,
+ RequestType requestType) {
+ if (requestType == RequestType.UIDL && isRepaintAll(request)) {
+ /*
+ * UIDL request contains valid repaintAll=1 event, the user probably
+ * wants to initiate a new application through a custom index.html
+ * without using the bootstrap page.
+ */
+ return true;
+
+ } else if (requestType == RequestType.OTHER) {
+ /*
+ * I.e URIs that are not application resources or static (theme)
+ * files.
+ */
+ return true;
+
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets resource path using different implementations. Required to
+ * supporting different servlet container implementations (application
+ * servers).
+ *
+ * @param servletContext
+ * @param path
+ * the resource path.
+ * @return the resource path.
+ */
+ protected static String getResourcePath(ServletContext servletContext,
+ String path) {
+ String resultPath = null;
+ resultPath = servletContext.getRealPath(path);
+ if (resultPath != null) {
+ return resultPath;
+ } else {
+ try {
+ final URL url = servletContext.getResource(path);
+ resultPath = url.getFile();
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.INFO,
+ "Could not find resource path " + path, e);
+ }
+ }
+ return resultPath;
+ }
+
+ /**
+ * Creates a new application and registers it into WebApplicationContext
+ * (aka session). This is not meant to be overridden. Override
+ * getNewApplication to create the application instance in a custom way.
+ *
+ * @param request
+ * @return
+ * @throws ServletException
+ * @throws MalformedURLException
+ */
+ private Application createApplication(HttpServletRequest request)
+ throws ServletException, MalformedURLException {
+ Application newApplication = getNewApplication(request);
+
+ final WebApplicationContext context = getApplicationContext(request
+ .getSession());
+ context.addApplication(newApplication);
+
+ return newApplication;
+ }
+
+ private void handleServiceException(WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response, Application application,
+ Throwable e) throws IOException, ServletException {
+ // if this was an UIDL request, response UIDL back to client
+ if (getRequestType(request) == RequestType.UIDL) {
+ Application.SystemMessages ci = getSystemMessages();
+ criticalNotification(request, response,
+ ci.getInternalErrorCaption(), ci.getInternalErrorMessage(),
+ null, ci.getInternalErrorURL());
+ if (application != null) {
+ application.getErrorHandler()
+ .terminalError(new RequestError(e));
+ } else {
+ throw new ServletException(e);
+ }
+ } else {
+ // 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
+ * eg. ( and ), so values should be safe in javascript too.
+ *
+ * @param themeName
+ * @return
+ */
+ protected static String stripSpecialChars(String themeName) {
+ StringBuilder sb = new StringBuilder();
+ char[] charArray = themeName.toCharArray();
+ for (int i = 0; i < charArray.length; i++) {
+ char c = charArray[i];
+ if (!CHAR_BLACKLIST.contains(c)) {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static final Collection<Character> CHAR_BLACKLIST = new HashSet<Character>(
+ Arrays.asList(new Character[] { '&', '"', '\'', '<', '>', '(', ')',
+ ';' }));
+
+ /**
+ * Returns the default theme. Must never return null.
+ *
+ * @return
+ */
+ public static String getDefaultTheme() {
+ return DEFAULT_THEME_NAME;
+ }
+
+ void handleServiceSessionExpired(WrappedHttpServletRequest request,
+ WrappedHttpServletResponse 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 {
+ Application.SystemMessages ci = getSystemMessages();
+ if (getRequestType(request) != RequestType.UIDL) {
+ // 'plain' http req - e.g. browser reload;
+ // just go ahead redirect the browser
+ response.sendRedirect(ci.getSessionExpiredURL());
+ } else {
+ /*
+ * 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());
+
+ }
+ } catch (SystemMessageException ee) {
+ throw new ServletException(ee);
+ }
+
+ }
+
+ private void handleServiceSecurityException(
+ WrappedHttpServletRequest request,
+ WrappedHttpServletResponse 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 {
+ Application.SystemMessages ci = getSystemMessages();
+ if (getRequestType(request) != RequestType.UIDL) {
+ // 'plain' http req - e.g. browser reload;
+ // just go ahead redirect the browser
+ response.sendRedirect(ci.getCommunicationErrorURL());
+ } else {
+ // 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();
+ }
+ } catch (SystemMessageException ee) {
+ throw new ServletException(ee);
+ }
+
+ log("Invalid security key received from " + request.getRemoteHost());
+ }
+
+ /**
+ * Creates a new application for the given request.
+ *
+ * @param request
+ * the HTTP request.
+ * @return A new Application instance.
+ * @throws ServletException
+ */
+ protected abstract Application getNewApplication(HttpServletRequest request)
+ throws ServletException;
+
+ /**
+ * Starts the application if it is not already running.
+ *
+ * @param request
+ * @param application
+ * @param webApplicationContext
+ * @throws ServletException
+ * @throws MalformedURLException
+ */
+ private void startApplication(HttpServletRequest request,
+ Application application, WebApplicationContext webApplicationContext)
+ throws ServletException, MalformedURLException {
+
+ if (!application.isRunning()) {
+ // Create application
+ final URL applicationUrl = getApplicationUrl(request);
+
+ // Initial locale comes from the request
+ Locale locale = request.getLocale();
+ application.setLocale(locale);
+ application.start(new ApplicationStartEvent(applicationUrl,
+ getDeploymentConfiguration().getInitParameters(),
+ webApplicationContext, isProductionMode()));
+ addonContext.applicationStarted(application);
+ }
+ }
+
+ /**
+ * Check if this is a request for a static resource and, if it is, serve the
+ * resource to the client.
+ *
+ * @param request
+ * @param response
+ * @return true if a file was served and the request has been handled, false
+ * otherwise.
+ * @throws IOException
+ * @throws ServletException
+ */
+ 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) {
+ return false;
+ }
+
+ if ((request.getContextPath() != null)
+ && (request.getRequestURI().startsWith("/VAADIN/"))) {
+ serveStaticResourcesInVAADIN(request.getRequestURI(), request,
+ response);
+ return true;
+ } else if (request.getRequestURI().startsWith(
+ request.getContextPath() + "/VAADIN/")) {
+ serveStaticResourcesInVAADIN(
+ request.getRequestURI().substring(
+ request.getContextPath().length()), request,
+ response);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Serve resources from VAADIN directory.
+ *
+ * @param filename
+ * The filename to serve. Should always start with /VAADIN/.
+ * @param request
+ * @param response
+ * @throws IOException
+ * @throws ServletException
+ */
+ private void serveStaticResourcesInVAADIN(String filename,
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ServletException {
+
+ final ServletContext sc = getServletContext();
+ URL resourceUrl = sc.getResource(filename);
+ if (resourceUrl == null) {
+ // try if requested file is found from classloader
+
+ // strip leading "/" otherwise stream from JAR wont work
+ filename = filename.substring(1);
+ resourceUrl = getDeploymentConfiguration().getClassLoader()
+ .getResource(filename);
+
+ if (resourceUrl == null) {
+ // cannot serve requested file
+ getLogger()
+ .info("Requested resource ["
+ + filename
+ + "] not found from filesystem or through class loader."
+ + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/VAADIN folder.");
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ // security check: do not permit navigation out of the VAADIN
+ // directory
+ if (!isAllowedVAADINResourceUrl(request, resourceUrl)) {
+ getLogger()
+ .info("Requested resource ["
+ + filename
+ + "] not accessible in the VAADIN directory or access to it is forbidden.");
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ }
+
+ // Find the modification timestamp
+ long lastModifiedTime = 0;
+ URLConnection connection = null;
+ try {
+ connection = resourceUrl.openConnection();
+ lastModifiedTime = connection.getLastModified();
+ // Remove milliseconds to avoid comparison problems (milliseconds
+ // are not returned by the browser in the "If-Modified-Since"
+ // header).
+ lastModifiedTime = lastModifiedTime - lastModifiedTime % 1000;
+
+ if (browserHasNewestVersion(request, lastModifiedTime)) {
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ return;
+ }
+ } catch (Exception e) {
+ // Failed to find out last modified timestamp. Continue without it.
+ getLogger()
+ .log(Level.FINEST,
+ "Failed to find out last modified timestamp. Continuing without it.",
+ e);
+ } finally {
+ if (connection instanceof URLConnection) {
+ try {
+ // Explicitly close the input stream to prevent it
+ // from remaining hanging
+ // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4257700
+ InputStream is = connection.getInputStream();
+ if (is != null) {
+ is.close();
+ }
+ } catch (IOException e) {
+ getLogger().log(Level.INFO,
+ "Error closing URLConnection input stream", e);
+ }
+ }
+ }
+
+ // Set type mime type if we can determine it based on the filename
+ final String mimetype = sc.getMimeType(filename);
+ if (mimetype != null) {
+ response.setContentType(mimetype);
+ }
+
+ // Provide modification timestamp to the browser if it is known.
+ if (lastModifiedTime > 0) {
+ response.setDateHeader("Last-Modified", lastModifiedTime);
+ /*
+ * The browser is allowed to cache for 1 hour without checking if
+ * the file has changed. This forces browsers to fetch a new version
+ * when the Vaadin version is updated. This will cause more requests
+ * to the servlet than without this but for high volume sites the
+ * static files should never be served through the servlet. The
+ * cache timeout can be configured by setting the resourceCacheTime
+ * parameter in web.xml
+ */
+ response.setHeader("Cache-Control",
+ "max-age= " + String.valueOf(resourceCacheTime));
+ }
+
+ // Write the resource to the client.
+ final OutputStream os = response.getOutputStream();
+ final byte buffer[] = new byte[DEFAULT_BUFFER_SIZE];
+ int bytes;
+ InputStream is = resourceUrl.openStream();
+ while ((bytes = is.read(buffer)) >= 0) {
+ os.write(buffer, 0, bytes);
+ }
+ is.close();
+ }
+
+ /**
+ * Check whether a URL obtained from a classloader refers to a valid static
+ * resource in the directory VAADIN.
+ *
+ * Warning: Overriding of this method is not recommended, but is possible to
+ * support non-default classloaders or servers that may produce URLs
+ * different from the normal ones. The method prototype may change in the
+ * future. Care should be taken not to expose class files or other resources
+ * outside the VAADIN directory if the method is overridden.
+ *
+ * @param request
+ * @param resourceUrl
+ * @return
+ *
+ * @since 6.6.7
+ */
+ protected boolean isAllowedVAADINResourceUrl(HttpServletRequest request,
+ URL resourceUrl) {
+ if ("jar".equals(resourceUrl.getProtocol())) {
+ // This branch is used for accessing resources directly from the
+ // Vaadin JAR in development environments and in similar cases.
+
+ // Inside a JAR, a ".." would mean a real directory named ".." so
+ // using it in paths should just result in the file not being found.
+ // However, performing a check in case some servers or class loaders
+ // try to normalize the path by collapsing ".." before the class
+ // loader sees it.
+
+ if (!resourceUrl.getPath().contains("!/VAADIN/")) {
+ getLogger().info(
+ "Blocked attempt to access a JAR entry not starting with /VAADIN/: "
+ + resourceUrl);
+ return false;
+ }
+ getLogger().fine(
+ "Accepted access to a JAR entry using a class loader: "
+ + resourceUrl);
+ return true;
+ } else {
+ // Some servers such as GlassFish extract files from JARs (file:)
+ // and e.g. JBoss 5+ use protocols vsf: and vfsfile: .
+
+ // Check that the URL is in a VAADIN directory and does not contain
+ // "/../"
+ if (!resourceUrl.getPath().contains("/VAADIN/")
+ || resourceUrl.getPath().contains("/../")) {
+ getLogger().info(
+ "Blocked attempt to access file: " + resourceUrl);
+ return false;
+ }
+ getLogger().fine(
+ "Accepted access to a file using a class loader: "
+ + resourceUrl);
+ return true;
+ }
+ }
+
+ /**
+ * Checks if the browser has an up to date cached version of requested
+ * resource. Currently the check is performed using the "If-Modified-Since"
+ * header. Could be expanded if needed.
+ *
+ * @param request
+ * The HttpServletRequest from the browser.
+ * @param resourceLastModifiedTimestamp
+ * The timestamp when the resource was last modified. 0 if the
+ * last modification time is unknown.
+ * @return true if the If-Modified-Since header tells the cached version in
+ * the browser is up to date, false otherwise
+ */
+ private boolean browserHasNewestVersion(HttpServletRequest request,
+ long resourceLastModifiedTimestamp) {
+ if (resourceLastModifiedTimestamp < 1) {
+ // We do not know when it was modified so the browser cannot have an
+ // up-to-date version
+ return false;
+ }
+ /*
+ * The browser can request the resource conditionally using an
+ * If-Modified-Since header. Check this against the last modification
+ * time.
+ */
+ try {
+ // If-Modified-Since represents the timestamp of the version cached
+ // in the browser
+ long headerIfModifiedSince = request
+ .getDateHeader("If-Modified-Since");
+
+ if (headerIfModifiedSince >= resourceLastModifiedTimestamp) {
+ // Browser has this an up-to-date version of the resource
+ return true;
+ }
+ } catch (Exception e) {
+ // Failed to parse header. Fail silently - the browser does not have
+ // an up-to-date version in its cache.
+ }
+ return false;
+ }
+
+ protected enum RequestType {
+ FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE;
+ }
+
+ protected RequestType getRequestType(WrappedHttpServletRequest request) {
+ if (ServletPortletHelper.isFileUploadRequest(request)) {
+ return RequestType.FILE_UPLOAD;
+ } else if (ServletPortletHelper.isConnectorResourceRequest(request)) {
+ return RequestType.CONNECTOR_RESOURCE;
+ } else if (isBrowserDetailsRequest(request)) {
+ return RequestType.BROWSER_DETAILS;
+ } else if (ServletPortletHelper.isUIDLRequest(request)) {
+ return RequestType.UIDL;
+ } else if (isStaticResourceRequest(request)) {
+ return RequestType.STATIC_FILE;
+ } else if (ServletPortletHelper.isApplicationResourceRequest(request)) {
+ return RequestType.APPLICATION_RESOURCE;
+ }
+ return RequestType.OTHER;
+
+ }
+
+ private static boolean isBrowserDetailsRequest(HttpServletRequest request) {
+ return "POST".equals(request.getMethod())
+ && request.getParameter("browserDetails") != null;
+ }
+
+ private boolean isStaticResourceRequest(HttpServletRequest request) {
+ String pathInfo = request.getPathInfo();
+ if (pathInfo == null || pathInfo.length() <= 10) {
+ return false;
+ }
+
+ if ((request.getContextPath() != null)
+ && (request.getRequestURI().startsWith("/VAADIN/"))) {
+ return true;
+ } else if (request.getRequestURI().startsWith(
+ request.getContextPath() + "/VAADIN/")) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isOnUnloadRequest(HttpServletRequest request) {
+ return request.getParameter(ApplicationConnection.PARAM_UNLOADBURST) != null;
+ }
+
+ /**
+ * Get system messages from the current application class
+ *
+ * @return
+ */
+ protected SystemMessages getSystemMessages() {
+ Class<? extends Application> appCls = null;
+ try {
+ appCls = getApplicationClass();
+ } catch (ClassNotFoundException e) {
+ // Previous comment claimed that this should never happen
+ throw new SystemMessageException(e);
+ }
+ return getSystemMessages(appCls);
+ }
+
+ public static SystemMessages getSystemMessages(
+ Class<? extends Application> appCls) {
+ try {
+ if (appCls != null) {
+ Method m = appCls
+ .getMethod("getSystemMessages", (Class[]) null);
+ return (Application.SystemMessages) m.invoke(null,
+ (Object[]) null);
+ }
+ } catch (SecurityException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (NoSuchMethodException e) {
+ // This is completely ok and should be silently ignored
+ } catch (IllegalArgumentException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ } catch (IllegalAccessException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (InvocationTargetException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ }
+ return Application.getSystemMessages();
+ }
+
+ protected abstract Class<? extends Application> getApplicationClass()
+ throws ClassNotFoundException;
+
+ /**
+ * 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.
+ *
+ * The returned folder is usually the same as the context path and
+ * independent of the application.
+ *
+ * @param request
+ * @return The location of static resources (should contain the VAADIN
+ * directory). Never ends with a slash (/).
+ */
+ protected String getStaticFilesLocation(HttpServletRequest request) {
+
+ return getWebApplicationsStaticFileLocation(request);
+ }
+
+ /**
+ * The default method to fetch static files location (URL). This method does
+ * not check for request attribute {@value #REQUEST_VAADIN_STATIC_FILE_PATH}
+ *
+ * @param request
+ * @return
+ */
+ private String getWebApplicationsStaticFileLocation(
+ HttpServletRequest request) {
+ String staticFileLocation;
+ // if property is defined in configurations, use that
+ staticFileLocation = getDeploymentConfiguration()
+ .getApplicationOrSystemProperty(PARAMETER_VAADIN_RESOURCES,
+ null);
+ if (staticFileLocation != null) {
+ return staticFileLocation;
+ }
+
+ // the last (but most common) option is to generate default location
+ // from request
+
+ // if context is specified add it to widgetsetUrl
+ String ctxPath = request.getContextPath();
+
+ // FIXME: ctxPath.length() == 0 condition is probably unnecessary and
+ // might even be wrong.
+
+ if (ctxPath.length() == 0
+ && request.getAttribute("javax.servlet.include.context_path") != null) {
+ // include request (e.g portlet), get context path from
+ // attribute
+ ctxPath = (String) request
+ .getAttribute("javax.servlet.include.context_path");
+ }
+
+ // Remove heading and trailing slashes from the context path
+ ctxPath = removeHeadingOrTrailing(ctxPath, "/");
+
+ if (ctxPath.equals("")) {
+ return "";
+ } else {
+ return "/" + ctxPath;
+ }
+ }
+
+ /**
+ * Remove any heading or trailing "what" from the "string".
+ *
+ * @param string
+ * @param what
+ * @return
+ */
+ private static String removeHeadingOrTrailing(String string, String what) {
+ while (string.startsWith(what)) {
+ string = string.substring(1);
+ }
+
+ while (string.endsWith(what)) {
+ string = string.substring(0, string.length() - 1);
+ }
+
+ return string;
+ }
+
+ /**
+ * Write a redirect response to the main page of the application.
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ * if sending the redirect fails due to an input/output error or
+ * a bad application URL
+ */
+ private void redirectToApplication(HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+ String applicationUrl = getApplicationUrl(request).toExternalForm();
+ response.sendRedirect(response.encodeRedirectURL(applicationUrl));
+ }
+
+ /**
+ * Gets the current application URL from request.
+ *
+ * @param request
+ * the HTTP request.
+ * @throws MalformedURLException
+ * if the application is denied access to the persistent data
+ * store represented by the given URL.
+ */
+ protected URL getApplicationUrl(HttpServletRequest request)
+ throws MalformedURLException {
+ final URL reqURL = new URL(
+ (request.isSecure() ? "https://" : "http://")
+ + request.getServerName()
+ + ((request.isSecure() && request.getServerPort() == 443)
+ || (!request.isSecure() && request
+ .getServerPort() == 80) ? "" : ":"
+ + request.getServerPort())
+ + request.getRequestURI());
+ String servletPath = "";
+ if (request.getAttribute("javax.servlet.include.servlet_path") != null) {
+ // this is an include request
+ servletPath = request.getAttribute(
+ "javax.servlet.include.context_path").toString()
+ + request
+ .getAttribute("javax.servlet.include.servlet_path");
+
+ } else {
+ servletPath = request.getContextPath() + request.getServletPath();
+ }
+
+ if (servletPath.length() == 0
+ || servletPath.charAt(servletPath.length() - 1) != '/') {
+ servletPath = servletPath + "/";
+ }
+ URL u = new URL(reqURL, servletPath);
+ return u;
+ }
+
+ /**
+ * Gets the existing application for given request. Looks for application
+ * instance for given request based on the requested URL.
+ *
+ * @param request
+ * the HTTP request.
+ * @param allowSessionCreation
+ * true if a session should be created if no session exists,
+ * false if no session should be created
+ * @return Application instance, or null if the URL does not map to valid
+ * application.
+ * @throws MalformedURLException
+ * if the application is denied access to the persistent data
+ * store represented by the given URL.
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ * @throws SessionExpiredException
+ */
+ protected Application getExistingApplication(HttpServletRequest request,
+ boolean allowSessionCreation) throws MalformedURLException,
+ SessionExpiredException {
+
+ // Ensures that the session is still valid
+ final HttpSession session = request.getSession(allowSessionCreation);
+ if (session == null) {
+ throw new SessionExpiredException();
+ }
+
+ WebApplicationContext context = getApplicationContext(session);
+
+ // Gets application list for the session.
+ final Collection<Application> applications = context.getApplications();
+
+ // Search for the application (using the application URI) from the list
+ for (final Iterator<Application> i = applications.iterator(); i
+ .hasNext();) {
+ final Application sessionApplication = i.next();
+ final String sessionApplicationPath = sessionApplication.getURL()
+ .getPath();
+ String requestApplicationPath = getApplicationUrl(request)
+ .getPath();
+
+ if (requestApplicationPath.equals(sessionApplicationPath)) {
+ // Found a running application
+ if (sessionApplication.isRunning()) {
+ return sessionApplication;
+ }
+ // Application has stopped, so remove it before creating a new
+ // application
+ getApplicationContext(session).removeApplication(
+ sessionApplication);
+ break;
+ }
+ }
+
+ // Existing application not found
+ return null;
+ }
+
+ /**
+ * Ends the application.
+ *
+ * @param request
+ * the HTTP request.
+ * @param response
+ * the HTTP response to write to.
+ * @param application
+ * the application to end.
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ private void endApplication(HttpServletRequest request,
+ HttpServletResponse response, Application application)
+ throws IOException {
+
+ String logoutUrl = application.getLogoutURL();
+ if (logoutUrl == null) {
+ logoutUrl = application.getURL().toString();
+ }
+
+ final HttpSession session = request.getSession();
+ if (session != null) {
+ getApplicationContext(session).removeApplication(application);
+ }
+
+ response.sendRedirect(response.encodeRedirectURL(logoutUrl));
+ }
+
+ /**
+ * Returns the path info; note that this _can_ be different than
+ * request.getPathInfo(). Examples where this might be useful:
+ * <ul>
+ * <li>An application runner servlet that runs different Vaadin applications
+ * based on an identifier.</li>
+ * <li>Providing a REST interface in the context root, while serving a
+ * Vaadin UI on a sub-URI using only one servlet (e.g. REST on
+ * http://example.com/foo, UI on http://example.com/foo/vaadin)</li>
+ *
+ * @param request
+ * @return
+ */
+ protected String getRequestPathInfo(HttpServletRequest request) {
+ return request.getPathInfo();
+ }
+
+ /**
+ * Gets relative location of a theme resource.
+ *
+ * @param theme
+ * the Theme name.
+ * @param resource
+ * the Theme resource.
+ * @return External URI specifying the resource
+ */
+ public String getResourceLocation(String theme, ThemeResource resource) {
+
+ if (resourcePath == null) {
+ return resource.getResourceId();
+ }
+ return resourcePath + theme + "/" + resource.getResourceId();
+ }
+
+ private boolean isRepaintAll(HttpServletRequest request) {
+ return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null)
+ && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1"));
+ }
+
+ private void closeApplication(Application application, HttpSession session) {
+ if (application == null) {
+ return;
+ }
+
+ application.close();
+ if (session != null) {
+ WebApplicationContext context = getApplicationContext(session);
+ context.removeApplication(application);
+ }
+ }
+
+ /**
+ *
+ * Gets the application context from an HttpSession. If no context is
+ * currently stored in a session a new context is created and stored in the
+ * session.
+ *
+ * @param session
+ * the HTTP session.
+ * @return the application context for HttpSession.
+ */
+ protected WebApplicationContext getApplicationContext(HttpSession session) {
+ /*
+ * TODO the ApplicationContext.getApplicationContext() should be removed
+ * and logic moved here. Now overriding context type is possible, but
+ * the whole creation logic should be here. MT 1101
+ */
+ return WebApplicationContext.getApplicationContext(session);
+ }
+
+ public class RequestError implements Terminal.ErrorEvent, Serializable {
+
+ private final Throwable throwable;
+
+ public RequestError(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+
+ /**
+ * Override this method if you need to use a specialized communicaiton
+ * mananger implementation.
+ *
+ * @deprecated Instead of overriding this method, override
+ * {@link WebApplicationContext} implementation via
+ * {@link AbstractApplicationServlet#getApplicationContext(HttpSession)}
+ * method and in that customized implementation return your
+ * CommunicationManager in
+ * {@link WebApplicationContext#getApplicationManager(Application, AbstractApplicationServlet)}
+ * method.
+ *
+ * @param application
+ * @return
+ */
+ @Deprecated
+ public CommunicationManager createCommunicationManager(
+ Application application) {
+ return new CommunicationManager(application);
+ }
+
+ /**
+ * Escapes characters to html entities. An exception is made for some
+ * "safe characters" to keep the text somewhat readable.
+ *
+ * @param unsafe
+ * @return a safe string to be added inside an html tag
+ */
+ public static final String safeEscapeForHtml(String unsafe) {
+ if (null == unsafe) {
+ return null;
+ }
+ StringBuilder safe = new StringBuilder();
+ char[] charArray = unsafe.toCharArray();
+ for (int i = 0; i < charArray.length; i++) {
+ char c = charArray[i];
+ if (isSafe(c)) {
+ safe.append(c);
+ } else {
+ safe.append("&#");
+ safe.append((int) c);
+ safe.append(";");
+ }
+ }
+
+ return safe.toString();
+ }
+
+ private static boolean isSafe(char c) {
+ return //
+ c > 47 && c < 58 || // alphanum
+ c > 64 && c < 91 || // A-Z
+ c > 96 && c < 123 // a-z
+ ;
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(AbstractApplicationServlet.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
new file mode 100644
index 0000000000..ba1b3cadb6
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
@@ -0,0 +1,2790 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.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.InvocationTargetException;
+import java.lang.reflect.Method;
+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 com.vaadin.Application;
+import com.vaadin.Application.SystemMessages;
+import com.vaadin.RootRequiresMoreInformationException;
+import com.vaadin.Version;
+import com.vaadin.annotations.JavaScript;
+import com.vaadin.annotations.StyleSheet;
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.external.json.JSONObject;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.communication.UidlValue;
+import com.vaadin.terminal.AbstractClientConnector;
+import com.vaadin.terminal.CombinedRequest;
+import com.vaadin.terminal.LegacyPaint;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.StreamVariable;
+import com.vaadin.terminal.StreamVariable.StreamingEndEvent;
+import com.vaadin.terminal.StreamVariable.StreamingErrorEvent;
+import com.vaadin.terminal.Terminal.ErrorEvent;
+import com.vaadin.terminal.Terminal.ErrorListener;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext;
+import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout;
+import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.AbstractField;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.HasComponents;
+import com.vaadin.ui.Root;
+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 ApplicationConnection}.
+ *
+ * TODO Document better!
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractCommunicationManager implements Serializable {
+
+ private static final String DASHDASH = "--";
+
+ private static final RequestHandler APP_RESOURCE_HANDLER = new ApplicationResourceHandler();
+
+ private static final RequestHandler UNSUPPORTED_BROWSER_HANDLER = new UnsupportedBrowserHandler();
+
+ /**
+ * TODO Document me!
+ *
+ * @author peholmst
+ */
+ public interface Callback extends Serializable {
+
+ public void criticalNotification(WrappedRequest request,
+ WrappedResponse response, String cap, String msg,
+ String details, String outOfSyncURL) throws IOException;
+ }
+
+ static class UploadInterruptedException extends Exception {
+ public UploadInterruptedException() {
+ super("Upload interrupted by other thread");
+ }
+ }
+
+ private static String GET_PARAM_REPAINT_ALL = "repaintAll";
+
+ // 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> rootToClientCache = 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;
+
+ private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts";
+
+ /**
+ * The application this communication manager is used for
+ */
+ private final Application application;
+
+ private List<String> locales;
+
+ private int pendingLocalesIndex;
+
+ private int timeoutInterval = -1;
+
+ private DragAndDropService dragAndDropService;
+
+ private String requestThemeName;
+
+ private int maxInactiveInterval;
+
+ private Connector highlightedConnector;
+
+ private Map<String, Class<?>> connectorResourceContexts = new HashMap<String, Class<?>>();
+
+ private Map<String, Map<String, StreamVariable>> pidToNameToStreamVariable;
+
+ private Map<StreamVariable, String> streamVariableToSeckey;
+
+ /**
+ * TODO New constructor - document me!
+ *
+ * @param application
+ */
+ public AbstractCommunicationManager(Application application) {
+ this.application = application;
+ application.addRequestHandler(getBootstrapHandler());
+ application.addRequestHandler(APP_RESOURCE_HANDLER);
+ application.addRequestHandler(UNSUPPORTED_BROWSER_HANDLER);
+ requireLocale(application.getLocale().toString());
+ }
+
+ protected Application getApplication() {
+ return application;
+ }
+
+ private static final int LF = "\n".getBytes()[0];
+
+ private static final String CRLF = "\r\n";
+
+ private static final String UTF8 = "UTF8";
+
+ private static final String GET_PARAM_HIGHLIGHT_COMPONENT = "highlightComponent";
+
+ 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(WrappedRequest request,
+ WrappedResponse 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.length() + 2);
+ if (readLine.startsWith("Content-Disposition:")
+ && readLine.indexOf("filename=") > 0) {
+ rawfilename = readLine.replaceAll(".*filename=", "");
+ String parenthesis = rawfilename.substring(0, 1);
+ rawfilename = rawfilename.substring(1);
+ rawfilename = rawfilename.substring(0,
+ rawfilename.indexOf(parenthesis));
+ 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() + 2); // 2 == CRLF
+
+ /*
+ * 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) {
+ synchronized (application) {
+ handleChangeVariablesError(application, (Component) owner, e,
+ new HashMap<String, Object>());
+ }
+ }
+ 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(WrappedRequest request,
+ WrappedResponse 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) {
+ synchronized (application) {
+ handleChangeVariablesError(application, (Component) owner, e,
+ new HashMap<String, Object>());
+ }
+ }
+ 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 Application application = getApplication();
+
+ OutputStream out = null;
+ int totalBytes = 0;
+ StreamingStartEventImpl startedEvent = new StreamingStartEventImpl(
+ filename, type, contentLength);
+ try {
+ boolean listenProgress;
+ synchronized (application) {
+ streamVariable.streamingStarted(startedEvent);
+ out = streamVariable.getOutputStream();
+ listenProgress = streamVariable.listenProgress();
+ }
+
+ // 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
+ synchronized (application) {
+ StreamingProgressEventImpl progressEvent = new StreamingProgressEventImpl(
+ filename, type, contentLength, totalBytes);
+ streamVariable.onProgress(progressEvent);
+ }
+ }
+ if (streamVariable.isInterrupted()) {
+ throw new UploadInterruptedException();
+ }
+ }
+
+ // upload successful
+ out.close();
+ StreamingEndEvent event = new StreamingEndEventImpl(filename, type,
+ totalBytes);
+ synchronized (application) {
+ streamVariable.streamingFinished(event);
+ }
+
+ } catch (UploadInterruptedException e) {
+ // Download interrupted by application code
+ tryToCloseStream(out);
+ StreamingErrorEvent event = new StreamingErrorEventImpl(filename,
+ type, contentLength, totalBytes, e);
+ synchronized (application) {
+ streamVariable.streamingFailed(event);
+ }
+ // 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);
+ synchronized (application) {
+ StreamingErrorEvent event = new StreamingErrorEventImpl(
+ filename, type, contentLength, totalBytes, e);
+ synchronized (application) {
+ streamVariable.streamingFailed(event);
+ }
+ // throw exception for terminal to be handled (to be passed to
+ // terminalErrorHandler)
+ throw new UploadException(e);
+ }
+ }
+ 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(WrappedRequest request,
+ WrappedResponse 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(WrappedRequest, WrappedResponse, Callback, Application, Root)}
+ * 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 application that
+ * has already been closed.
+ *
+ * The method handleUidlRequest(...) in subclasses should call this method.
+ *
+ * TODO better documentation
+ *
+ * @param request
+ * @param response
+ * @param callback
+ * @param root
+ * target window for the UIDL request, can be null if target not
+ * found
+ * @throws IOException
+ * @throws InvalidUIDLSecurityKeyException
+ * @throws JSONException
+ */
+ public void handleUidlRequest(WrappedRequest request,
+ WrappedResponse response, Callback callback, Root root)
+ throws IOException, InvalidUIDLSecurityKeyException, JSONException {
+
+ checkWidgetsetVersion(request);
+ requestThemeName = request.getParameter("theme");
+ maxInactiveInterval = request.getSessionMaxInactiveInterval();
+ // repaint requested or session has timed out and new one is created
+ boolean repaintAll;
+ final OutputStream out;
+
+ repaintAll = (request.getParameter(GET_PARAM_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(GET_PARAM_ANALYZE_LAYOUTS) != null);
+
+ if (request.getParameter(GET_PARAM_HIGHLIGHT_COMPONENT) != null) {
+ String pid = request
+ .getParameter(GET_PARAM_HIGHLIGHT_COMPONENT);
+ highlightedConnector = root.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 application
+ // in order to guarantee that no parallel variable handling is
+ // made
+ synchronized (application) {
+
+ // Finds the window within the application
+ if (application.isRunning()) {
+ // Returns if no window found
+ if (root == null) {
+ // This should not happen, no windows exists but
+ // application is still open.
+ getLogger().warning("Could not get root for application");
+ return;
+ }
+ } else {
+ // application has been closed
+ endApplication(request, response, application);
+ return;
+ }
+
+ // Change all variables based on request parameters
+ if (!handleVariables(request, response, callback, application, root)) {
+
+ // var inconsistency; the client is probably out-of-sync
+ SystemMessages ci = null;
+ try {
+ Method m = application.getClass().getMethod(
+ "getSystemMessages", (Class[]) null);
+ ci = (Application.SystemMessages) m.invoke(null,
+ (Object[]) null);
+ } catch (Exception e2) {
+ // FIXME: Handle exception
+ // Not critical, but something is still wrong; print
+ // stacktrace
+ getLogger().log(Level.WARNING,
+ "getSystemMessages() failed - continuing", e2);
+ }
+ if (ci != null) {
+ 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, root, analyzeLayouts);
+ postPaint(root);
+ }
+
+ outWriter.close();
+ requestThemeName = null;
+ }
+
+ /**
+ * Checks that the version reported by the client (widgetset) matches that
+ * of the server.
+ *
+ * @param request
+ */
+ private void checkWidgetsetVersion(WrappedRequest request) {
+ String widgetsetVersion = request.getParameter("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
+ * application
+ *
+ * @param root
+ *
+ */
+ protected void postPaint(Root root) {
+ // Remove connectors that have been detached from the application during
+ // handling of the request
+ root.getConnectorTracker().cleanConnectorMap();
+
+ if (pidToNameToStreamVariable != null) {
+ Iterator<String> iterator = pidToNameToStreamVariable.keySet()
+ .iterator();
+ while (iterator.hasNext()) {
+ String connectorId = iterator.next();
+ if (root.getConnectorTracker().getConnector(connectorId) == null) {
+ // Owner is no longer attached to the application
+ Map<String, StreamVariable> removed = pidToNameToStreamVariable
+ .get(connectorId);
+ for (String key : removed.keySet()) {
+ streamVariableToSeckey.remove(removed.get(key));
+ }
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ protected void highlightConnector(Connector highlightedConnector) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("*** Debug details of a component: *** \n");
+ sb.append("Type: ");
+ sb.append(highlightedConnector.getClass().getName());
+ if (highlightedConnector instanceof AbstractComponent) {
+ AbstractComponent component = (AbstractComponent) highlightedConnector;
+ sb.append("\nId:");
+ sb.append(highlightedConnector.getConnectorId());
+ if (component.getCaption() != null) {
+ sb.append("\nCaption:");
+ sb.append(component.getCaption());
+ }
+
+ printHighlightedComponentHierarchy(sb, component);
+ }
+ getLogger().info(sb.toString());
+ }
+
+ protected void printHighlightedComponentHierarchy(StringBuilder sb,
+ AbstractComponent component) {
+ LinkedList<Component> h = new LinkedList<Component>();
+ h.add(component);
+ Component parent = component.getParent();
+ while (parent != null) {
+ h.addFirst(parent);
+ parent = parent.getParent();
+ }
+
+ sb.append("\nComponent hierarchy:\n");
+ Application application2 = component.getApplication();
+ sb.append(application2.getClass().getName());
+ sb.append(".");
+ sb.append(application2.getClass().getSimpleName());
+ sb.append("(");
+ sb.append(application2.getClass().getSimpleName());
+ sb.append(".java");
+ sb.append(":1)");
+ int l = 1;
+ for (Component component2 : h) {
+ sb.append("\n");
+ for (int i = 0; i < l; i++) {
+ sb.append(" ");
+ }
+ l++;
+ Class<? extends Component> componentClass = component2.getClass();
+ Class<?> topClass = componentClass;
+ while (topClass.getEnclosingClass() != null) {
+ topClass = topClass.getEnclosingClass();
+ }
+ sb.append(componentClass.getName());
+ sb.append(".");
+ sb.append(componentClass.getSimpleName());
+ 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(WrappedRequest request,
+ WrappedResponse response, Callback callback, boolean repaintAll,
+ final PrintWriter outWriter, Root root, boolean analyzeLayouts)
+ throws PaintException, IOException, JSONException {
+
+ // Removes application if it has stopped during variable changes
+ if (!application.isRunning()) {
+ endApplication(request, response, application);
+ return;
+ }
+
+ openJsonMessage(outWriter, response);
+
+ // security key
+ Object writeSecurityTokenFlag = request
+ .getAttribute(WRITE_SECURITY_TOKEN_FLAG);
+
+ if (writeSecurityTokenFlag != null) {
+ outWriter.print(getSecurityKeyUIDL(request));
+ }
+
+ writeUidlResponse(request, repaintAll, outWriter, root, 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(WrappedRequest request) {
+ final String seckey = getSecurityKey(request);
+ if (seckey != null) {
+ return "\"" + ApplicationConnection.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(WrappedRequest request) {
+ String seckey = null;
+ seckey = (String) request
+ .getSessionAttribute(ApplicationConnection.UIDL_SECURITY_TOKEN_ID);
+ if (seckey == null) {
+ seckey = UUID.randomUUID().toString();
+ request.setSessionAttribute(
+ ApplicationConnection.UIDL_SECURITY_TOKEN_ID, seckey);
+ }
+
+ return seckey;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void writeUidlResponse(WrappedRequest request, boolean repaintAll,
+ final PrintWriter outWriter, Root root, boolean analyzeLayouts)
+ throws PaintException, JSONException {
+ ArrayList<ClientConnector> dirtyVisibleConnectors = new ArrayList<ClientConnector>();
+ Application application = root.getApplication();
+ // Paints components
+ ConnectorTracker rootConnectorTracker = root.getConnectorTracker();
+ getLogger().log(Level.FINE, "* Creating response to client");
+ if (repaintAll) {
+ getClientCache(root).clear();
+ rootConnectorTracker.markAllConnectorsDirty();
+
+ // Reset sent locales
+ locales = null;
+ requireLocale(application.getLocale().toString());
+ }
+
+ dirtyVisibleConnectors
+ .addAll(getDirtyVisibleConnectors(rootConnectorTracker));
+
+ getLogger().log(
+ Level.FINE,
+ "Found " + dirtyVisibleConnectors.size()
+ + " dirty connectors to paint");
+ for (ClientConnector connector : dirtyVisibleConnectors) {
+ if (connector instanceof Component) {
+ ((Component) connector).updateState();
+ }
+ }
+ rootConnectorTracker.markAllConnectorsClean();
+
+ outWriter.print("\"changes\":[");
+
+ List<InvalidLayout> invalidComponentRelativeSizes = null;
+
+ JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter,
+ !repaintAll);
+ legacyPaint(paintTarget, dirtyVisibleConnectors);
+
+ if (analyzeLayouts) {
+ invalidComponentRelativeSizes = ComponentSizeValidator
+ .validateComponentRelativeSizes(root.getContent(), null,
+ null);
+
+ // Also check any existing subwindows
+ if (root.getWindows() != null) {
+ for (Window subWindow : root.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) {
+ SharedState state = connector.getState();
+ if (null != state) {
+ // encode and send shared state
+ try {
+ Class<? extends SharedState> stateType = connector
+ .getStateType();
+ SharedState referenceState = null;
+ if (repaintAll) {
+ // Use an empty state object as reference for full
+ // repaints
+ try {
+ referenceState = stateType.newInstance();
+ } catch (Exception e) {
+ getLogger().log(
+ Level.WARNING,
+ "Error creating reference object for state of type "
+ + stateType.getName());
+ }
+ }
+ Object stateJson = JsonCodec.encode(state, referenceState,
+ stateType, root.getConnectorTracker());
+
+ 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 (isVisible(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
+
+ // send server to client RPC calls for components in the root, in call
+ // order
+
+ // collect RPC calls from components in the root 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());
+ // }
+ // }
+ paramJson.put(JsonCodec.encode(
+ invocation.getParameters()[i], referenceParameter,
+ parameterType, root.getConnectorTracker()));
+ }
+ 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 = null;
+ try {
+ Method m = application.getClass().getMethod("getSystemMessages",
+ (Class[]) null);
+ ci = (Application.SystemMessages) m.invoke(null, (Object[]) null);
+ } catch (NoSuchMethodException e) {
+ getLogger().log(Level.WARNING,
+ "getSystemMessages() failed - continuing", e);
+ } catch (IllegalArgumentException e) {
+ getLogger().log(Level.WARNING,
+ "getSystemMessages() failed - continuing", e);
+ } catch (IllegalAccessException e) {
+ getLogger().log(Level.WARNING,
+ "getSystemMessages() failed - continuing", e);
+ } catch (InvocationTargetException e) {
+ getLogger().log(Level.WARNING,
+ "getSystemMessages() failed - continuing", e);
+ }
+
+ // 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(root, getTheme(root), 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(root);
+
+ 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 resource : jsAnnotation.value()) {
+ scriptDependencies.add(registerResource(resource, class1));
+ }
+ }
+
+ StyleSheet styleAnnotation = class1.getAnnotation(StyleSheet.class);
+ if (styleAnnotation != null) {
+ for (String resource : styleAnnotation.value()) {
+ styleDependencies.add(registerResource(resource, 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);
+ }
+
+ writePerformanceData(outWriter);
+ }
+
+ /**
+ * Resolves a resource URI, registering the URI with this
+ * {@code AbstractCommunicationManager} if needed and returns a fully
+ * qualified URI.
+ */
+ private String registerResource(String resourceUri, Class<?> context) {
+ try {
+ URI uri = new URI(resourceUri);
+ String protocol = uri.getScheme();
+
+ if ("connector".equals(protocol)) {
+ // Strip initial slash
+ String resourceName = uri.getPath().substring(1);
+ return registerConnectorResource(resourceName, context);
+ }
+
+ if (protocol != null || uri.getHost() != null) {
+ return resourceUri;
+ }
+
+ // Bare path interpreted as connector resource
+ return registerConnectorResource(resourceUri, context);
+ } catch (URISyntaxException e) {
+ getLogger().log(Level.WARNING,
+ "Could not parse resource url " + resourceUri, e);
+ return resourceUri;
+ }
+ }
+
+ private String registerConnectorResource(String name, Class<?> context) {
+ synchronized (connectorResourceContexts) {
+ // Add to map of names accepted by serveConnectorResource
+ if (connectorResourceContexts.containsKey(name)) {
+ Class<?> oldContext = connectorResourceContexts.get(name);
+ if (oldContext != context) {
+ getLogger().warning(
+ "Resource " + name + " defined by both " + context
+ + " and " + oldContext + ". Resource from "
+ + oldContext + " will be used.");
+ }
+ } else {
+ connectorResourceContexts.put(name, context);
+ }
+ }
+
+ return ApplicationConnection.CONNECTOR_PROTOCOL_PREFIX + "/" + name;
+ }
+
+ /**
+ * Adds the performance timing data (used by TestBench 3) to the UIDL
+ * response.
+ */
+ private void writePerformanceData(final PrintWriter outWriter) {
+ AbstractWebApplicationContext ctx = (AbstractWebApplicationContext) application
+ .getContext();
+ outWriter.write(String.format(", \"timings\":[%d, %d]",
+ ctx.getTotalSessionTime(), ctx.getLastRequestTime()));
+ }
+
+ private void legacyPaint(PaintTarget paintTarget,
+ ArrayList<ClientConnector> dirtyVisibleConnectors)
+ throws PaintException {
+ List<Vaadin6Component> legacyComponents = new ArrayList<Vaadin6Component>();
+ for (Connector connector : dirtyVisibleConnectors) {
+ // All Components that want to use paintContent must implement
+ // Vaadin6Component
+ if (connector instanceof Vaadin6Component) {
+ legacyComponents.add((Vaadin6Component) connector);
+ }
+ }
+ sortByHierarchy((List) legacyComponents);
+ for (Vaadin6Component c : legacyComponents) {
+ getLogger().fine(
+ "Painting Vaadin6Component " + 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(Root root) {
+ Integer rootId = Integer.valueOf(root.getRootId());
+ ClientCache cache = rootToClientCache.get(rootId);
+ if (cache == null) {
+ cache = new ClientCache();
+ rootToClientCache.put(rootId, cache);
+ }
+ return cache;
+ }
+
+ /**
+ * Checks if the connector is visible in context. For Components,
+ * {@link #isVisible(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
+ */
+ static boolean isVisible(ClientConnector connector) {
+ if (connector instanceof Component) {
+ return isVisible((Component) connector);
+ } else {
+ ClientConnector parent = connector.getParent();
+ if (parent == null) {
+ return false;
+ } else {
+ return isVisible(parent);
+ }
+ }
+ }
+
+ /**
+ * Checks if the component is visible in context, i.e. returns false if the
+ * child is hidden, the parent is hidden or the parent says the child should
+ * not be rendered (using
+ * {@link HasComponents#isComponentVisible(Component)}
+ *
+ * @param child
+ * The child to check
+ * @return true if the child is visible to the client, false otherwise
+ */
+ static boolean isVisible(Component child) {
+ if (!child.isVisible()) {
+ return false;
+ }
+
+ HasComponents parent = child.getParent();
+ if (parent == null) {
+ if (child instanceof Root) {
+ return child.isVisible();
+ } else {
+ return false;
+ }
+ }
+
+ return parent.isComponentVisible(child) && isVisible(parent);
+ }
+
+ 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(Root root,
+ String themeName, String resource);
+
+ private int getTimeoutInterval() {
+ return maxInactiveInterval;
+ }
+
+ private String getTheme(Root root) {
+ String themeName = root.getApplication().getThemeForRoot(root);
+ String requestThemeName = getRequestTheme();
+
+ if (requestThemeName != null) {
+ themeName = requestThemeName;
+ }
+ if (themeName == null) {
+ themeName = AbstractApplicationServlet.getDefaultTheme();
+ }
+ return themeName;
+ }
+
+ private String getRequestTheme() {
+ return requestThemeName;
+ }
+
+ /**
+ * Returns false if the cross site request forgery protection is turned off.
+ *
+ * @param application
+ * @return false if the XSRF is turned off, true otherwise
+ */
+ public boolean isXSRFEnabled(Application application) {
+ return !"true"
+ .equals(application
+ .getProperty(AbstractApplicationServlet.SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION));
+ }
+
+ /**
+ * 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(WrappedRequest request,
+ WrappedResponse response, Callback callback,
+ Application application2, Root root) 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(application2)) {
+ 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
+ .getSessionAttribute(ApplicationConnection.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, root, 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, root, 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 root
+ * the root 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(WrappedRequest source, Root root,
+ final String burst) {
+ boolean success = true;
+ try {
+ Set<Connector> enabledConnectors = new HashSet<Connector>();
+
+ List<MethodInvocation> invocations = parseInvocations(
+ root.getConnectorTracker(), burst);
+ for (MethodInvocation invocation : invocations) {
+ final ClientConnector connector = getConnector(root,
+ 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(root,
+ invocation.getConnectorId());
+
+ if (connector == null) {
+ getLogger().log(
+ Level.WARNING,
+ "RPC call to " + invocation.getInterfaceName()
+ + "." + invocation.getMethodName()
+ + " received for connector "
+ + invocation.getConnectorId()
+ + " but no such connector could be found");
+ 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) {
+ Throwable realException = e.getCause();
+ Component errorComponent = null;
+ if (connector instanceof Component) {
+ errorComponent = (Component) connector;
+ }
+ handleChangeVariablesError(root.getApplication(),
+ errorComponent, realException, null);
+ }
+ } 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) {
+ Component errorComponent = null;
+ if (connector instanceof Component) {
+ errorComponent = (Component) connector;
+ } else if (connector instanceof DragAndDropService) {
+ Object dropHandlerOwner = changes.get("dhowner");
+ if (dropHandlerOwner instanceof Component) {
+ errorComponent = (Component) dropHandlerOwner;
+ }
+ }
+ handleChangeVariablesError(root.getApplication(),
+ errorComponent, e, changes);
+ }
+ }
+ }
+ } catch (JSONException e) {
+ getLogger().warning(
+ "Unable to parse RPC call from the client: "
+ + e.getMessage());
+ // TODO or return success = false?
+ throw new RuntimeException(e);
+ }
+
+ return success;
+ }
+
+ /**
+ * 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 iff the invocation was a legacy invocation and it
+ // was merged with the previous one
+ 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);
+
+ 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 {
+ ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
+ connectorId, interfaceName, 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(Root root, String connectorId) {
+ ClientConnector c = root.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(WrappedRequest 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;
+ }
+
+ public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable {
+ private final Throwable throwable;
+
+ public ErrorHandlerErrorEvent(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+
+ /**
+ * Handles an error (exception) that occurred when processing variable
+ * changes from the client or a failure of a file upload.
+ *
+ * For {@link AbstractField} components,
+ * {@link AbstractField#handleError(com.vaadin.ui.AbstractComponent.ComponentErrorEvent)}
+ * is called. In all other cases (or if the field does not handle the
+ * error), {@link ErrorListener#terminalError(ErrorEvent)} for the
+ * application error handler is called.
+ *
+ * @param application
+ * @param owner
+ * component that the error concerns
+ * @param e
+ * exception that occurred
+ * @param m
+ * map from variable names to values
+ */
+ private void handleChangeVariablesError(Application application,
+ Component owner, Throwable t, Map<String, Object> m) {
+ boolean handled = false;
+ ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent(
+ owner, t, m);
+
+ if (owner instanceof AbstractField) {
+ try {
+ handled = ((AbstractField<?>) owner).handleError(errorEvent);
+ } catch (Exception handlerException) {
+ /*
+ * If there is an error in the component error handler we pass
+ * the that error to the application error handler and continue
+ * processing the actual error
+ */
+ application.getErrorHandler().terminalError(
+ new ErrorHandlerErrorEvent(handlerException));
+ handled = false;
+ }
+ }
+
+ if (!handled) {
+ application.getErrorHandler().terminalError(errorEvent);
+ }
+
+ }
+
+ /**
+ * 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().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);
+ }
+
+ 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
+ }
+
+ /**
+ * Ends the Application.
+ *
+ * The browser is redirected to the Application logout URL set with
+ * {@link Application#setLogoutURL(String)}, or to the application URL if no
+ * logout URL is given.
+ *
+ * @param request
+ * the request instance.
+ * @param response
+ * the response to write to.
+ * @param application
+ * the Application to end.
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ private void endApplication(WrappedRequest request,
+ WrappedResponse response, Application application)
+ throws IOException {
+
+ String logoutUrl = application.getLogoutURL();
+ if (logoutUrl == null) {
+ logoutUrl = application.getURL().toString();
+ }
+ // clients JS app is still running, send a special json file to tell
+ // client that application has quit and where to point browser now
+ // Set the response type
+ final OutputStream out = response.getOutputStream();
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+ openJsonMessage(outWriter, response);
+ outWriter.print("\"redirect\":{");
+ outWriter.write("\"url\":\"" + logoutUrl + "\"}");
+ closeJsonMessage(outWriter);
+ outWriter.flush();
+ outWriter.close();
+ out.flush();
+ }
+
+ 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,
+ WrappedResponse 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
+ * root 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 (isVisible(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(application.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);
+ getLogger().log(Level.FINE,
+ "Mapping " + class1.getName() + " to " + 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/[ROOTID]/[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();
+ int rootId = owner.getRoot().getRootId();
+ String key = rootId + "/" + paintableId + "/" + name;
+
+ if (pidToNameToStreamVariable == null) {
+ pidToNameToStreamVariable = new HashMap<String, Map<String, StreamVariable>>();
+ }
+ Map<String, StreamVariable> nameToStreamVariable = pidToNameToStreamVariable
+ .get(paintableId);
+ if (nameToStreamVariable == null) {
+ nameToStreamVariable = new HashMap<String, StreamVariable>();
+ pidToNameToStreamVariable.put(paintableId, nameToStreamVariable);
+ }
+ nameToStreamVariable.put(name, value);
+
+ if (streamVariableToSeckey == null) {
+ streamVariableToSeckey = new HashMap<StreamVariable, String>();
+ }
+ String seckey = streamVariableToSeckey.get(value);
+ if (seckey == null) {
+ seckey = UUID.randomUUID().toString();
+ streamVariableToSeckey.put(value, seckey);
+ }
+
+ return ApplicationConnection.APP_PROTOCOL_PREFIX
+ + ServletPortletHelper.UPLOAD_URL_PREFIX + key + "/" + seckey;
+
+ }
+
+ public void cleanStreamVariable(ClientConnector owner, String name) {
+ Map<String, StreamVariable> nameToStreamVar = pidToNameToStreamVariable
+ .get(owner.getConnectorId());
+ nameToStreamVar.remove(name);
+ if (nameToStreamVar.isEmpty()) {
+ pidToNameToStreamVariable.remove(owner.getConnectorId());
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ protected abstract BootstrapHandler createBootstrapHandler();
+
+ protected boolean handleApplicationRequest(WrappedRequest request,
+ WrappedResponse response) throws IOException {
+ return application.handleRequest(request, response);
+ }
+
+ public void handleBrowserDetailsRequest(WrappedRequest request,
+ WrappedResponse response, Application application)
+ throws IOException {
+
+ // if we do not yet have a currentRoot, it should be initialized
+ // shortly, and we should send the initial UIDL
+ boolean sendUIDL = Root.getCurrent() == null;
+
+ try {
+ CombinedRequest combinedRequest = new CombinedRequest(request);
+
+ Root root = application.getRootForRequest(combinedRequest);
+ response.setContentType("application/json; charset=UTF-8");
+
+ // Use the same logic as for determined roots
+ BootstrapHandler bootstrapHandler = getBootstrapHandler();
+ BootstrapContext context = bootstrapHandler.createContext(
+ combinedRequest, response, application, root.getRootId());
+
+ String widgetset = context.getWidgetsetName();
+ String theme = context.getThemeName();
+ String themeUri = bootstrapHandler.getThemeUri(context, theme);
+
+ // TODO These are not required if it was only the init of the root
+ // that was delayed
+ JSONObject params = new JSONObject();
+ params.put("widgetset", widgetset);
+ params.put("themeUri", themeUri);
+ // Root id might have changed based on e.g. window.name
+ params.put(ApplicationConnection.ROOT_ID_PARAMETER,
+ root.getRootId());
+ if (sendUIDL) {
+ String initialUIDL = getInitialUIDL(combinedRequest, root);
+ 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 (RootRequiresMoreInformationException e) {
+ // Requiring more information at this point is not allowed
+ // TODO handle in a better way
+ throw new RuntimeException(e);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 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 root
+ * the root 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(WrappedRequest request, Root root)
+ throws PaintException, JSONException {
+ // TODO maybe unify writeUidlResponse()?
+ StringWriter sWriter = new StringWriter();
+ PrintWriter pWriter = new PrintWriter(sWriter);
+ pWriter.print("{");
+ if (isXSRFEnabled(root.getApplication())) {
+ pWriter.print(getSecurityKeyUIDL(request));
+ }
+ writeUidlResponse(request, true, pWriter, root, false);
+ pWriter.print("}");
+ String initialUIDL = sWriter.toString();
+ getLogger().log(Level.FINE, "Initial UIDL:" + initialUIDL);
+ return initialUIDL;
+ }
+
+ /**
+ * Serve a connector resource from the classpath if the resource has
+ * previously been registered by calling
+ * {@link #registerResource(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 serveConnectorResource(WrappedRequest request,
+ WrappedResponse response) throws IOException {
+
+ String pathInfo = request.getRequestPathInfo();
+ // + 2 to also remove beginning and ending slashes
+ String resourceName = pathInfo
+ .substring(ApplicationConnection.CONNECTOR_RESOURCE_PREFIX
+ .length() + 2);
+
+ final String mimetype = response.getDeploymentConfiguration()
+ .getMimeType(resourceName);
+
+ // Security check: avoid accidentally serving from the root of the
+ // classpath instead of relative to the context class
+ if (resourceName.startsWith("/")) {
+ getLogger().warning(
+ "Connector resource request starting with / rejected: "
+ + resourceName);
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName);
+ return;
+ }
+
+ // Check that the resource name has been registered
+ Class<?> context;
+ synchronized (connectorResourceContexts) {
+ context = connectorResourceContexts.get(resourceName);
+ }
+
+ // Security check: don't serve resource if the name hasn't been
+ // registered in the map
+ if (context == null) {
+ getLogger().warning(
+ "Connector resource request for unknown resource rejected: "
+ + resourceName);
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName);
+ return;
+ }
+
+ // Resolve file relative to the location of the context class
+ InputStream in = context.getResourceAsStream(resourceName);
+ if (in == null) {
+ getLogger().warning(
+ resourceName + " defined by " + context.getName()
+ + " not found. Verify that the file "
+ + context.getPackage().getName().replace('.', '/')
+ + '/' + resourceName
+ + " is available on the classpath.");
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, resourceName);
+ 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 root
+ * The root for this request
+ *
+ * @see #getStreamVariableTargetUrl(ReceiverOwner, String, StreamVariable)
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ * @throws InvalidUIDLSecurityKeyException
+ */
+ public void handleFileUpload(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException, InvalidUIDLSecurityKeyException {
+
+ /*
+ * URI pattern: APP/UPLOAD/[ROOTID]/[PID]/[NAME]/[SECKEY] See
+ * #createReceiverUrl
+ */
+
+ String pathInfo = request.getRequestPathInfo();
+ // 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= rootid, 1 = cid, 2= name, 3
+ // = sec key
+ String rootId = parts[0];
+ String connectorId = parts[1];
+ String variableName = parts[2];
+ Root root = application.getRootById(Integer.parseInt(rootId));
+ Root.setCurrent(root);
+
+ StreamVariable streamVariable = getStreamVariable(connectorId,
+ variableName);
+ String secKey = streamVariableToSeckey.get(streamVariable);
+ if (secKey.equals(parts[3])) {
+
+ ClientConnector source = getConnector(root, 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!");
+ }
+
+ }
+
+ public StreamVariable getStreamVariable(String connectorId,
+ String variableName) {
+ Map<String, StreamVariable> map = pidToNameToStreamVariable
+ .get(connectorId);
+ if (map == null) {
+ return null;
+ }
+ StreamVariable streamVariable = map.get(variableName);
+ return streamVariable;
+ }
+
+ /**
+ * 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/terminal/gwt/server/AbstractDeploymentConfiguration.java b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
new file mode 100644
index 0000000000..7b51712904
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java
@@ -0,0 +1,143 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.lang.reflect.Constructor;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+import com.vaadin.terminal.DeploymentConfiguration;
+
+public abstract class AbstractDeploymentConfiguration implements
+ DeploymentConfiguration {
+
+ private final Class<?> systemPropertyBaseClass;
+ private final Properties applicationProperties = new Properties();
+ private AddonContext addonContext;
+
+ public AbstractDeploymentConfiguration(Class<?> systemPropertyBaseClass) {
+ this.systemPropertyBaseClass = systemPropertyBaseClass;
+ }
+
+ @Override
+ public String getApplicationOrSystemProperty(String propertyName,
+ String defaultValue) {
+
+ String val = null;
+
+ // Try application properties
+ val = getApplicationProperty(propertyName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try system properties
+ val = getSystemProperty(propertyName);
+ if (val != null) {
+ return val;
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Gets an system property value.
+ *
+ * @param parameterName
+ * the Name or the parameter.
+ * @return String value or null if not found
+ */
+ protected String getSystemProperty(String parameterName) {
+ String val = null;
+
+ String pkgName;
+ final Package pkg = systemPropertyBaseClass.getPackage();
+ if (pkg != null) {
+ pkgName = pkg.getName();
+ } else {
+ final String className = systemPropertyBaseClass.getName();
+ pkgName = new String(className.toCharArray(), 0,
+ className.lastIndexOf('.'));
+ }
+ val = System.getProperty(pkgName + "." + parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try lowercased system properties
+ val = System.getProperty(pkgName + "." + parameterName.toLowerCase());
+ return val;
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ final String classLoaderName = getApplicationOrSystemProperty(
+ "ClassLoader", null);
+ ClassLoader classLoader;
+ if (classLoaderName == null) {
+ classLoader = getClass().getClassLoader();
+ } else {
+ try {
+ final Class<?> classLoaderClass = getClass().getClassLoader()
+ .loadClass(classLoaderName);
+ final Constructor<?> c = classLoaderClass
+ .getConstructor(new Class[] { ClassLoader.class });
+ classLoader = (ClassLoader) c
+ .newInstance(new Object[] { getClass().getClassLoader() });
+ } catch (final Exception e) {
+ throw new RuntimeException(
+ "Could not find specified class loader: "
+ + classLoaderName, e);
+ }
+ }
+ return classLoader;
+ }
+
+ /**
+ * Gets an application property value.
+ *
+ * @param parameterName
+ * the Name or the parameter.
+ * @return String value or null if not found
+ */
+ protected String getApplicationProperty(String parameterName) {
+
+ String val = applicationProperties.getProperty(parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try lower case application properties for backward compatibility with
+ // 3.0.2 and earlier
+ val = applicationProperties.getProperty(parameterName.toLowerCase());
+
+ return val;
+ }
+
+ @Override
+ public Properties getInitParameters() {
+ return applicationProperties;
+ }
+
+ @Override
+ public Iterator<AddonContextListener> getAddonContextListeners() {
+ // Called once for init and then no more, so there's no point in caching
+ // the instance
+ ServiceLoader<AddonContextListener> contextListenerLoader = ServiceLoader
+ .load(AddonContextListener.class, getClassLoader());
+ return contextListenerLoader.iterator();
+ }
+
+ @Override
+ public void setAddonContext(AddonContext addonContext) {
+ this.addonContext = addonContext;
+ }
+
+ @Override
+ public AddonContext getAddonContext() {
+ return addonContext;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java
new file mode 100644
index 0000000000..d3474e736e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java
@@ -0,0 +1,46 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.terminal.StreamVariable.StreamingEvent;
+
+/**
+ * Abstract base class for StreamingEvent implementations.
+ */
+@SuppressWarnings("serial")
+abstract class AbstractStreamingEvent implements StreamingEvent {
+ private final String type;
+ private final String filename;
+ private final long contentLength;
+ private final long bytesReceived;
+
+ @Override
+ public final String getFileName() {
+ return filename;
+ }
+
+ @Override
+ public final String getMimeType() {
+ return type;
+ }
+
+ protected AbstractStreamingEvent(String filename, String type, long length,
+ long bytesReceived) {
+ this.filename = filename;
+ this.type = type;
+ contentLength = length;
+ this.bytesReceived = bytesReceived;
+ }
+
+ @Override
+ public final long getContentLength() {
+ return contentLength;
+ }
+
+ @Override
+ public final long getBytesReceived() {
+ return bytesReceived;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java
new file mode 100644
index 0000000000..3a33621d10
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java
@@ -0,0 +1,268 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import com.vaadin.Application;
+import com.vaadin.service.ApplicationContext;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+/**
+ * Base class for web application contexts (including portlet contexts) that
+ * handles the common tasks.
+ */
+public abstract class AbstractWebApplicationContext implements
+ ApplicationContext, HttpSessionBindingListener, Serializable {
+
+ protected Collection<TransactionListener> listeners = Collections
+ .synchronizedList(new LinkedList<TransactionListener>());
+
+ protected final HashSet<Application> applications = new HashSet<Application>();
+
+ protected WebBrowser browser = new WebBrowser();
+
+ protected HashMap<Application, AbstractCommunicationManager> applicationToAjaxAppMgrMap = new HashMap<Application, AbstractCommunicationManager>();
+
+ private long totalSessionTime = 0;
+
+ private long lastRequestTime = -1;
+
+ @Override
+ public void addTransactionListener(TransactionListener listener) {
+ if (listener != null) {
+ listeners.add(listener);
+ }
+ }
+
+ @Override
+ public void removeTransactionListener(TransactionListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Sends a notification that a transaction is starting.
+ *
+ * @param application
+ * The application associated with the transaction.
+ * @param request
+ * the HTTP or portlet request that triggered the transaction.
+ */
+ protected void startTransaction(Application application, Object request) {
+ ArrayList<TransactionListener> currentListeners;
+ synchronized (listeners) {
+ currentListeners = new ArrayList<TransactionListener>(listeners);
+ }
+ for (TransactionListener listener : currentListeners) {
+ listener.transactionStart(application, request);
+ }
+ }
+
+ /**
+ * Sends a notification that a transaction has ended.
+ *
+ * @param application
+ * The application associated with the transaction.
+ * @param request
+ * the HTTP or portlet request that triggered the transaction.
+ */
+ protected void endTransaction(Application application, Object request) {
+ LinkedList<Exception> exceptions = null;
+
+ ArrayList<TransactionListener> currentListeners;
+ synchronized (listeners) {
+ currentListeners = new ArrayList<TransactionListener>(listeners);
+ }
+
+ for (TransactionListener listener : currentListeners) {
+ try {
+ listener.transactionEnd(application, request);
+ } catch (final RuntimeException t) {
+ if (exceptions == null) {
+ exceptions = new LinkedList<Exception>();
+ }
+ exceptions.add(t);
+ }
+ }
+
+ // If any runtime exceptions occurred, throw a combined exception
+ if (exceptions != null) {
+ final StringBuffer msg = new StringBuffer();
+ for (Exception e : exceptions) {
+ if (msg.length() == 0) {
+ msg.append("\n\n--------------------------\n\n");
+ }
+ msg.append(e.getMessage() + "\n");
+ final StringWriter trace = new StringWriter();
+ e.printStackTrace(new PrintWriter(trace, true));
+ msg.append(trace.toString());
+ }
+ throw new RuntimeException(msg.toString());
+ }
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)
+ */
+ @Override
+ public void valueBound(HttpSessionBindingEvent arg0) {
+ // We are not interested in bindings
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)
+ */
+ @Override
+ public void valueUnbound(HttpSessionBindingEvent event) {
+ // If we are going to be unbound from the session, the session must be
+ // closing
+ try {
+ while (!applications.isEmpty()) {
+ final Application app = applications.iterator().next();
+ app.close();
+ removeApplication(app);
+ }
+ } catch (Exception e) {
+ // This should never happen but is possible with rare
+ // configurations (e.g. robustness tests). If you have one
+ // thread doing HTTP socket write and another thread trying to
+ // remove same application here. Possible if you got e.g. session
+ // lifetime 1 min but socket write may take longer than 1 min.
+ // FIXME: Handle exception
+ getLogger().log(Level.SEVERE,
+ "Could not remove application, leaking memory.", e);
+ }
+ }
+
+ /**
+ * Get the web browser associated with this application context.
+ *
+ * Because application context is related to the http session and server
+ * maintains one session per browser-instance, each context has exactly one
+ * web browser associated with it.
+ *
+ * @return
+ */
+ public WebBrowser getBrowser() {
+ return browser;
+ }
+
+ @Override
+ public Collection<Application> getApplications() {
+ return Collections.unmodifiableCollection(applications);
+ }
+
+ protected void removeApplication(Application application) {
+ applications.remove(application);
+ applicationToAjaxAppMgrMap.remove(application);
+ }
+
+ @Override
+ public String generateApplicationResourceURL(ApplicationResource resource,
+ String mapKey) {
+
+ final String filename = resource.getFilename();
+ if (filename == null) {
+ return ApplicationConnection.APP_PROTOCOL_PREFIX
+ + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/";
+ } else {
+ // #7738 At least Tomcat and JBoss refuses requests containing
+ // encoded slashes or backslashes in URLs. Application resource URLs
+ // should really be passed in another way than as part of the path
+ // in the future.
+ String encodedFileName = urlEncode(filename).replace("%2F", "/")
+ .replace("%5C", "\\");
+ return ApplicationConnection.APP_PROTOCOL_PREFIX
+ + ApplicationConnection.APP_REQUEST_PATH + mapKey + "/"
+ + encodedFileName;
+ }
+
+ }
+
+ static String urlEncode(String filename) {
+ try {
+ return URLEncoder.encode(filename, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(
+ "UTF-8 charset not available (\"this should never happen\")",
+ e);
+ }
+ }
+
+ @Override
+ public boolean isApplicationResourceURL(URL context, String relativeUri) {
+ // If the relative uri is null, we are ready
+ if (relativeUri == null) {
+ return false;
+ }
+
+ // Resolves the prefix
+ String prefix = relativeUri;
+ final int index = relativeUri.indexOf('/');
+ if (index >= 0) {
+ prefix = relativeUri.substring(0, index);
+ }
+
+ // Handles the resource requests
+ return (prefix.equals("APP"));
+ }
+
+ @Override
+ public String getURLKey(URL context, String relativeUri) {
+ final int index = relativeUri.indexOf('/');
+ final int next = relativeUri.indexOf('/', index + 1);
+ if (next < 0) {
+ return null;
+ }
+ return relativeUri.substring(index + 1, next);
+ }
+
+ /**
+ * @return The total time spent servicing requests in this session.
+ */
+ public long getTotalSessionTime() {
+ return totalSessionTime;
+ }
+
+ /**
+ * Sets the time spent servicing the last request in the session and updates
+ * the total time spent servicing requests in this session.
+ *
+ * @param time
+ * the time spent in the last request.
+ */
+ public void setLastRequestTime(long time) {
+ lastRequestTime = time;
+ totalSessionTime += time;
+ }
+
+ /**
+ * @return the time spent servicing the last request in this session.
+ */
+ public long getLastRequestTime() {
+ return lastRequestTime;
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(AbstractWebApplicationContext.class.getName());
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContext.java b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java
new file mode 100644
index 0000000000..41e9046e22
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AddonContext.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.vaadin.Application;
+import com.vaadin.event.EventRouter;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.tools.ReflectTools;
+
+public class AddonContext {
+ private static final Method APPLICATION_STARTED_METHOD = ReflectTools
+ .findMethod(ApplicationStartedListener.class, "applicationStarted",
+ ApplicationStartedEvent.class);
+
+ private final DeploymentConfiguration deploymentConfiguration;
+
+ private final EventRouter eventRouter = new EventRouter();
+
+ private List<BootstrapListener> bootstrapListeners = new ArrayList<BootstrapListener>();
+
+ private List<AddonContextListener> initedListeners = new ArrayList<AddonContextListener>();
+
+ public AddonContext(DeploymentConfiguration deploymentConfiguration) {
+ this.deploymentConfiguration = deploymentConfiguration;
+ deploymentConfiguration.setAddonContext(this);
+ }
+
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+
+ public void init() {
+ AddonContextEvent event = new AddonContextEvent(this);
+ Iterator<AddonContextListener> listeners = deploymentConfiguration
+ .getAddonContextListeners();
+ while (listeners.hasNext()) {
+ AddonContextListener listener = listeners.next();
+ listener.contextCreated(event);
+ initedListeners.add(listener);
+ }
+ }
+
+ public void destroy() {
+ AddonContextEvent event = new AddonContextEvent(this);
+ for (AddonContextListener listener : initedListeners) {
+ listener.contextDestoryed(event);
+ }
+ }
+
+ public void addBootstrapListener(BootstrapListener listener) {
+ bootstrapListeners.add(listener);
+ }
+
+ public void applicationStarted(Application application) {
+ eventRouter.fireEvent(new ApplicationStartedEvent(this, application));
+ for (BootstrapListener l : bootstrapListeners) {
+ application.addBootstrapListener(l);
+ }
+ }
+
+ public void addApplicationStartedListener(
+ ApplicationStartedListener applicationStartListener) {
+ eventRouter.addListener(ApplicationStartedEvent.class,
+ applicationStartListener, APPLICATION_STARTED_METHOD);
+ }
+
+ public void removeApplicationStartedListener(
+ ApplicationStartedListener applicationStartListener) {
+ eventRouter.removeListener(ApplicationStartedEvent.class,
+ applicationStartListener, APPLICATION_STARTED_METHOD);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java
new file mode 100644
index 0000000000..33f681499f
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventObject;
+
+public class AddonContextEvent extends EventObject {
+
+ public AddonContextEvent(AddonContext source) {
+ super(source);
+ }
+
+ public AddonContext getAddonContext() {
+ return (AddonContext) getSource();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java
new file mode 100644
index 0000000000..93e7df4ede
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java
@@ -0,0 +1,13 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventListener;
+
+public interface AddonContextListener extends EventListener {
+ public void contextCreated(AddonContextEvent event);
+
+ public void contextDestoryed(AddonContextEvent event);
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java
new file mode 100644
index 0000000000..788c48267e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletException;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException;
+
+/**
+ * TODO Write documentation, fix JavaDoc tags.
+ *
+ * @author peholmst
+ */
+public class ApplicationPortlet2 extends AbstractApplicationPortlet {
+
+ private Class<? extends Application> applicationClass;
+
+ @Override
+ public void init(PortletConfig config) throws PortletException {
+ super.init(config);
+ try {
+ applicationClass = ServletPortletHelper
+ .getApplicationClass(getDeploymentConfiguration());
+ } catch (ApplicationClassException e) {
+ throw new PortletException(e);
+ }
+ }
+
+ @Override
+ protected Class<? extends Application> getApplicationClass() {
+ return applicationClass;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java
new file mode 100644
index 0000000000..42726c933e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java
@@ -0,0 +1,55 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.DownloadStream;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+
+public class ApplicationResourceHandler implements RequestHandler {
+ private static final Pattern APP_RESOURCE_PATTERN = Pattern
+ .compile("^/?APP/(\\d+)/.*");
+
+ @Override
+ public boolean handleRequest(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException {
+ // Check for application resources
+ String requestPath = request.getRequestPathInfo();
+ if (requestPath == null) {
+ return false;
+ }
+ Matcher resourceMatcher = APP_RESOURCE_PATTERN.matcher(requestPath);
+
+ if (resourceMatcher.matches()) {
+ ApplicationResource resource = application
+ .getResource(resourceMatcher.group(1));
+ if (resource != null) {
+ DownloadStream stream = resource.getStream();
+ if (stream != null) {
+ stream.setCacheTime(resource.getCacheTime());
+ stream.writeTo(response);
+ return true;
+ }
+ }
+ // We get here if the url looks like an application resource but no
+ // resource can be served
+ response.sendError(HttpServletResponse.SC_NOT_FOUND,
+ request.getRequestPathInfo() + " can not be found");
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java
new file mode 100644
index 0000000000..1af49e0da0
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java
@@ -0,0 +1,78 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.gwt.server.ServletPortletHelper.ApplicationClassException;
+
+/**
+ * This servlet connects a Vaadin Application to Web.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+
+@SuppressWarnings("serial")
+public class ApplicationServlet extends AbstractApplicationServlet {
+
+ // Private fields
+ private Class<? extends Application> applicationClass;
+
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being placed into service.
+ *
+ * @param servletConfig
+ * the object containing the servlet's configuration and
+ * initialization parameters
+ * @throws javax.servlet.ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ @Override
+ public void init(javax.servlet.ServletConfig servletConfig)
+ throws javax.servlet.ServletException {
+ super.init(servletConfig);
+
+ // Loads the application class using the classloader defined in the
+ // deployment configuration
+
+ try {
+ applicationClass = ServletPortletHelper
+ .getApplicationClass(getDeploymentConfiguration());
+ } catch (ApplicationClassException e) {
+ throw new ServletException(e);
+ }
+ }
+
+ @Override
+ protected Application getNewApplication(HttpServletRequest request)
+ throws ServletException {
+
+ // Creates a new application instance
+ try {
+ final Application application = getApplicationClass().newInstance();
+
+ return application;
+ } catch (final IllegalAccessException e) {
+ throw new ServletException("getNewApplication failed", e);
+ } catch (final InstantiationException e) {
+ throw new ServletException("getNewApplication failed", e);
+ } catch (ClassNotFoundException e) {
+ throw new ServletException("getNewApplication failed", e);
+ }
+ }
+
+ @Override
+ protected Class<? extends Application> getApplicationClass()
+ throws ClassNotFoundException {
+ return applicationClass;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java
new file mode 100644
index 0000000000..339b88222e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java
@@ -0,0 +1,28 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventObject;
+
+import com.vaadin.Application;
+
+public class ApplicationStartedEvent extends EventObject {
+ private final Application application;
+
+ public ApplicationStartedEvent(AddonContext context,
+ Application application) {
+ super(context);
+ this.application = application;
+ }
+
+ public AddonContext getContext() {
+ return (AddonContext) getSource();
+ }
+
+ public Application getApplication() {
+ return application;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java
new file mode 100644
index 0000000000..87884a0fda
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java
@@ -0,0 +1,11 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventListener;
+
+public interface ApplicationStartedListener extends EventListener {
+ public void applicationStarted(ApplicationStartedEvent event);
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java
new file mode 100644
index 0000000000..4731a5b79f
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java
@@ -0,0 +1,9 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+public class BootstrapDom {
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java
new file mode 100644
index 0000000000..bcf098b5aa
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java
@@ -0,0 +1,28 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.List;
+
+import org.jsoup.nodes.Node;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.WrappedRequest;
+
+public class BootstrapFragmentResponse extends BootstrapResponse {
+ private final List<Node> fragmentNodes;
+
+ public BootstrapFragmentResponse(BootstrapHandler handler,
+ WrappedRequest request, List<Node> fragmentNodes,
+ Application application, Integer rootId) {
+ super(handler, request, application, rootId);
+ this.fragmentNodes = fragmentNodes;
+ }
+
+ public List<Node> getFragmentNodes() {
+ return fragmentNodes;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java
new file mode 100644
index 0000000000..e89737337b
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java
@@ -0,0 +1,570 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.jsoup.nodes.DataNode;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.DocumentType;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.parser.Tag;
+
+import com.vaadin.Application;
+import com.vaadin.RootRequiresMoreInformationException;
+import com.vaadin.Version;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.external.json.JSONObject;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.ui.Root;
+
+public abstract class BootstrapHandler implements RequestHandler {
+
+ protected class BootstrapContext implements Serializable {
+
+ private final WrappedResponse response;
+ private final BootstrapFragmentResponse bootstrapResponse;
+
+ private String widgetsetName;
+ private String themeName;
+ private String appId;
+
+ public BootstrapContext(WrappedResponse response,
+ BootstrapFragmentResponse bootstrapResponse) {
+ this.response = response;
+ this.bootstrapResponse = bootstrapResponse;
+ }
+
+ public WrappedResponse getResponse() {
+ return response;
+ }
+
+ public WrappedRequest getRequest() {
+ return bootstrapResponse.getRequest();
+ }
+
+ public Application getApplication() {
+ return bootstrapResponse.getApplication();
+ }
+
+ public Integer getRootId() {
+ return bootstrapResponse.getRootId();
+ }
+
+ public Root getRoot() {
+ return bootstrapResponse.getRoot();
+ }
+
+ public String getWidgetsetName() {
+ if (widgetsetName == null) {
+ Root root = getRoot();
+ if (root != null) {
+ widgetsetName = getWidgetsetForRoot(this);
+ }
+ }
+ return widgetsetName;
+ }
+
+ public String getThemeName() {
+ if (themeName == null) {
+ Root root = getRoot();
+ if (root != null) {
+ themeName = findAndEscapeThemeName(this);
+ }
+ }
+ return themeName;
+ }
+
+ public String getAppId() {
+ if (appId == null) {
+ appId = getApplicationId(this);
+ }
+ return appId;
+ }
+
+ public BootstrapFragmentResponse getBootstrapResponse() {
+ return bootstrapResponse;
+ }
+
+ }
+
+ @Override
+ public boolean handleRequest(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException {
+
+ // TODO Should all urls be handled here?
+ Integer rootId = null;
+ try {
+ Root root = application.getRootForRequest(request);
+ if (root == null) {
+ writeError(response, new Throwable("No Root found"));
+ return true;
+ }
+
+ rootId = Integer.valueOf(root.getRootId());
+ } catch (RootRequiresMoreInformationException e) {
+ // Just keep going without rootId
+ }
+
+ try {
+ BootstrapContext context = createContext(request, response,
+ application, rootId);
+ setupMainDiv(context);
+
+ BootstrapFragmentResponse fragmentResponse = context
+ .getBootstrapResponse();
+ application.modifyBootstrapResponse(fragmentResponse);
+
+ String html = getBootstrapHtml(context);
+
+ writeBootstrapPage(response, html);
+ } catch (JSONException e) {
+ writeError(response, e);
+ }
+
+ return true;
+ }
+
+ private String getBootstrapHtml(BootstrapContext context) {
+ WrappedRequest request = context.getRequest();
+ WrappedResponse response = context.getResponse();
+ DeploymentConfiguration deploymentConfiguration = request
+ .getDeploymentConfiguration();
+
+ BootstrapFragmentResponse fragmentResponse = context
+ .getBootstrapResponse();
+
+ if (deploymentConfiguration.isStandalone(request)) {
+ Map<String, Object> headers = new LinkedHashMap<String, Object>();
+ Document document = Document.createShell("");
+ BootstrapPageResponse pageResponse = new BootstrapPageResponse(
+ this, request, document, headers, context.getApplication(),
+ context.getRootId());
+ List<Node> fragmentNodes = fragmentResponse.getFragmentNodes();
+ Element body = document.body();
+ for (Node node : fragmentNodes) {
+ body.appendChild(node);
+ }
+
+ setupStandaloneDocument(context, pageResponse);
+ context.getApplication().modifyBootstrapResponse(pageResponse);
+
+ sendBootstrapHeaders(response, headers);
+
+ return document.outerHtml();
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (Node node : fragmentResponse.getFragmentNodes()) {
+ if (sb.length() != 0) {
+ sb.append('\n');
+ }
+ sb.append(node.outerHtml());
+ }
+
+ return sb.toString();
+ }
+ }
+
+ private void sendBootstrapHeaders(WrappedResponse response,
+ Map<String, Object> headers) {
+ Set<Entry<String, Object>> entrySet = headers.entrySet();
+ for (Entry<String, Object> header : entrySet) {
+ Object value = header.getValue();
+ if (value instanceof String) {
+ response.setHeader(header.getKey(), (String) value);
+ } else if (value instanceof Long) {
+ response.setDateHeader(header.getKey(),
+ ((Long) value).longValue());
+ } else {
+ throw new RuntimeException("Unsupported header value: " + value);
+ }
+ }
+ }
+
+ private void writeBootstrapPage(WrappedResponse response, String html)
+ throws IOException {
+ response.setContentType("text/html");
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
+ response.getOutputStream(), "UTF-8"));
+ writer.append(html);
+ writer.close();
+ }
+
+ private void setupStandaloneDocument(BootstrapContext context,
+ BootstrapPageResponse response) {
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Pragma", "no-cache");
+ response.setDateHeader("Expires", 0);
+
+ Document document = response.getDocument();
+
+ DocumentType doctype = new DocumentType("html",
+ "-//W3C//DTD XHTML 1.0 Transitional//EN",
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd",
+ document.baseUri());
+ document.child(0).before(doctype);
+ document.body().parent().attr("xmlns", "http://www.w3.org/1999/xhtml");
+
+ Element head = document.head();
+ head.appendElement("meta").attr("http-equiv", "Content-Type")
+ .attr("content", "text/html; charset=utf-8");
+
+ // Chrome frame in all versions of IE (only if Chrome frame is
+ // installed)
+ head.appendElement("meta").attr("http-equiv", "X-UA-Compatible")
+ .attr("content", "chrome=1");
+
+ Root root = context.getRoot();
+ String title = ((root == null || root.getCaption() == null) ? "" : root
+ .getCaption());
+ head.appendElement("title").appendText(title);
+
+ head.appendElement("style").attr("type", "text/css")
+ .appendText("html, body {height:100%;margin:0;}");
+
+ // Add favicon links
+ String themeName = context.getThemeName();
+ if (themeName != null) {
+ String themeUri = getThemeUri(context, themeName);
+ head.appendElement("link").attr("rel", "shortcut icon")
+ .attr("type", "image/vnd.microsoft.icon")
+ .attr("href", themeUri + "/favicon.ico");
+ head.appendElement("link").attr("rel", "icon")
+ .attr("type", "image/vnd.microsoft.icon")
+ .attr("href", themeUri + "/favicon.ico");
+ }
+
+ Element body = document.body();
+ body.attr("scroll", "auto");
+ body.addClass(ApplicationConnection.GENERATED_BODY_CLASSNAME);
+ }
+
+ public BootstrapContext createContext(WrappedRequest request,
+ WrappedResponse response, Application application, Integer rootId) {
+ BootstrapContext context = new BootstrapContext(response,
+ new BootstrapFragmentResponse(this, request,
+ new ArrayList<Node>(), application, rootId));
+ return context;
+ }
+
+ protected String getMainDivStyle(BootstrapContext context) {
+ return null;
+ }
+
+ /**
+ * Creates and returns a unique ID for the DIV where the application is to
+ * be rendered.
+ *
+ * @param context
+ *
+ * @return the id to use in the DOM
+ */
+ protected abstract String getApplicationId(BootstrapContext context);
+
+ public String getWidgetsetForRoot(BootstrapContext context) {
+ Root root = context.getRoot();
+ WrappedRequest request = context.getRequest();
+
+ String widgetset = root.getApplication().getWidgetsetForRoot(root);
+ if (widgetset == null) {
+ widgetset = request.getDeploymentConfiguration()
+ .getConfiguredWidgetset(request);
+ }
+
+ widgetset = AbstractApplicationServlet.stripSpecialChars(widgetset);
+ return widgetset;
+ }
+
+ /**
+ * Method to write the div element into which that actual Vaadin application
+ * is rendered.
+ * <p>
+ * Override this method if you want to add some custom html around around
+ * the div element into which the actual Vaadin application will be
+ * rendered.
+ *
+ * @param context
+ *
+ * @throws IOException
+ * @throws JSONException
+ */
+ private void setupMainDiv(BootstrapContext context) throws IOException,
+ JSONException {
+ String style = getMainDivStyle(context);
+
+ /*- Add classnames;
+ * .v-app
+ * .v-app-loading
+ * .v-app-<simpleName for app class>
+ *- Additionally added from javascript:
+ * .v-theme-<themeName, remove non-alphanum>
+ */
+
+ String appClass = "v-app-"
+ + context.getApplication().getClass().getSimpleName();
+
+ String classNames = "v-app " + appClass;
+ List<Node> fragmentNodes = context.getBootstrapResponse()
+ .getFragmentNodes();
+
+ Element mainDiv = new Element(Tag.valueOf("div"), "");
+ mainDiv.attr("id", context.getAppId());
+ mainDiv.addClass(classNames);
+ if (style != null && style.length() != 0) {
+ mainDiv.attr("style", style);
+ }
+ mainDiv.appendElement("div").addClass("v-app-loading");
+ mainDiv.appendElement("noscript")
+ .append("You have to enable javascript in your browser to use an application built with Vaadin.");
+ fragmentNodes.add(mainDiv);
+
+ WrappedRequest request = context.getRequest();
+
+ DeploymentConfiguration deploymentConfiguration = request
+ .getDeploymentConfiguration();
+ String staticFileLocation = deploymentConfiguration
+ .getStaticFileLocation(request);
+
+ fragmentNodes
+ .add(new Element(Tag.valueOf("iframe"), "")
+ .attr("tabIndex", "-1")
+ .attr("id", "__gwt_historyFrame")
+ .attr("style",
+ "position:absolute;width:0;height:0;border:0;overflow:hidden")
+ .attr("src", "javascript:false"));
+
+ String bootstrapLocation = staticFileLocation
+ + "/VAADIN/vaadinBootstrap.js";
+ fragmentNodes.add(new Element(Tag.valueOf("script"), "").attr("type",
+ "text/javascript").attr("src", bootstrapLocation));
+ Element mainScriptTag = new Element(Tag.valueOf("script"), "").attr(
+ "type", "text/javascript");
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("//<![CDATA[\n");
+ builder.append("if (!window.vaadin) alert("
+ + JSONObject.quote("Failed to load the bootstrap javascript: "
+ + bootstrapLocation) + ");\n");
+
+ appendMainScriptTagContents(context, builder);
+
+ builder.append("//]]>");
+ mainScriptTag.appendChild(new DataNode(builder.toString(),
+ mainScriptTag.baseUri()));
+ fragmentNodes.add(mainScriptTag);
+
+ }
+
+ protected void appendMainScriptTagContents(BootstrapContext context,
+ StringBuilder builder) throws JSONException, IOException {
+ JSONObject defaults = getDefaultParameters(context);
+ JSONObject appConfig = getApplicationParameters(context);
+
+ boolean isDebug = !context.getApplication().isProductionMode();
+
+ builder.append("vaadin.setDefaults(");
+ appendJsonObject(builder, defaults, isDebug);
+ builder.append(");\n");
+
+ builder.append("vaadin.initApplication(\"");
+ builder.append(context.getAppId());
+ builder.append("\",");
+ appendJsonObject(builder, appConfig, isDebug);
+ builder.append(");\n");
+ }
+
+ private static void appendJsonObject(StringBuilder builder,
+ JSONObject jsonObject, boolean isDebug) throws JSONException {
+ if (isDebug) {
+ builder.append(jsonObject.toString(4));
+ } else {
+ builder.append(jsonObject.toString());
+ }
+ }
+
+ protected JSONObject getApplicationParameters(BootstrapContext context)
+ throws JSONException, PaintException {
+ Application application = context.getApplication();
+ Integer rootId = context.getRootId();
+
+ JSONObject appConfig = new JSONObject();
+
+ if (rootId != null) {
+ appConfig.put(ApplicationConnection.ROOT_ID_PARAMETER, rootId);
+ }
+
+ if (context.getThemeName() != null) {
+ appConfig.put("themeUri",
+ getThemeUri(context, context.getThemeName()));
+ }
+
+ JSONObject versionInfo = new JSONObject();
+ versionInfo.put("vaadinVersion", Version.getFullVersion());
+ versionInfo.put("applicationVersion", application.getVersion());
+ appConfig.put("versionInfo", versionInfo);
+
+ appConfig.put("widgetset", context.getWidgetsetName());
+
+ if (rootId == null || application.isRootInitPending(rootId.intValue())) {
+ appConfig.put("initialPath", context.getRequest()
+ .getRequestPathInfo());
+
+ Map<String, String[]> parameterMap = context.getRequest()
+ .getParameterMap();
+ appConfig.put("initialParams", parameterMap);
+ } else {
+ // write the initial UIDL into the config
+ appConfig.put("uidl",
+ getInitialUIDL(context.getRequest(), context.getRoot()));
+ }
+
+ return appConfig;
+ }
+
+ protected JSONObject getDefaultParameters(BootstrapContext context)
+ throws JSONException {
+ JSONObject defaults = new JSONObject();
+
+ WrappedRequest request = context.getRequest();
+ Application application = context.getApplication();
+
+ // Get system messages
+ Application.SystemMessages systemMessages = AbstractApplicationServlet
+ .getSystemMessages(application.getClass());
+ if (systemMessages != null) {
+ // Write the CommunicationError -message to client
+ JSONObject comErrMsg = new JSONObject();
+ comErrMsg.put("caption",
+ systemMessages.getCommunicationErrorCaption());
+ comErrMsg.put("message",
+ systemMessages.getCommunicationErrorMessage());
+ comErrMsg.put("url", systemMessages.getCommunicationErrorURL());
+
+ defaults.put("comErrMsg", comErrMsg);
+
+ JSONObject authErrMsg = new JSONObject();
+ authErrMsg.put("caption",
+ systemMessages.getAuthenticationErrorCaption());
+ authErrMsg.put("message",
+ systemMessages.getAuthenticationErrorMessage());
+ authErrMsg.put("url", systemMessages.getAuthenticationErrorURL());
+
+ defaults.put("authErrMsg", authErrMsg);
+ }
+
+ DeploymentConfiguration deploymentConfiguration = request
+ .getDeploymentConfiguration();
+ String staticFileLocation = deploymentConfiguration
+ .getStaticFileLocation(request);
+ String widgetsetBase = staticFileLocation + "/"
+ + AbstractApplicationServlet.WIDGETSET_DIRECTORY_PATH;
+ defaults.put("widgetsetBase", widgetsetBase);
+
+ if (!application.isProductionMode()) {
+ defaults.put("debug", true);
+ }
+
+ if (deploymentConfiguration.isStandalone(request)) {
+ defaults.put("standalone", true);
+ }
+
+ defaults.put("appUri", getAppUri(context));
+
+ return defaults;
+ }
+
+ protected abstract String getAppUri(BootstrapContext context);
+
+ /**
+ * Get the URI for the application theme.
+ *
+ * A portal-wide default theme is fetched from the portal shared resource
+ * directory (if any), other themes from the portlet.
+ *
+ * @param context
+ * @param themeName
+ *
+ * @return
+ */
+ public String getThemeUri(BootstrapContext context, String themeName) {
+ WrappedRequest request = context.getRequest();
+ final String staticFilePath = request.getDeploymentConfiguration()
+ .getStaticFileLocation(request);
+ return staticFilePath + "/"
+ + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName;
+ }
+
+ /**
+ * Override if required
+ *
+ * @param context
+ * @return
+ */
+ public String getThemeName(BootstrapContext context) {
+ return context.getApplication().getThemeForRoot(context.getRoot());
+ }
+
+ /**
+ * Don not override.
+ *
+ * @param context
+ * @return
+ */
+ public String findAndEscapeThemeName(BootstrapContext context) {
+ String themeName = getThemeName(context);
+ if (themeName == null) {
+ WrappedRequest request = context.getRequest();
+ themeName = request.getDeploymentConfiguration()
+ .getConfiguredTheme(request);
+ }
+
+ // XSS preventation, theme names shouldn't contain special chars anyway.
+ // The servlet denies them via url parameter.
+ themeName = AbstractApplicationServlet.stripSpecialChars(themeName);
+
+ return themeName;
+ }
+
+ protected void writeError(WrappedResponse response, Throwable e)
+ throws IOException {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ e.getLocalizedMessage());
+ }
+
+ /**
+ * Gets the initial UIDL message to send to the client.
+ *
+ * @param request
+ * the originating request
+ * @param root
+ * the root for which the UIDL should be generated
+ * @return a string with the initial UIDL message
+ * @throws PaintException
+ * if an exception occurs while painting the components
+ * @throws JSONException
+ * if an exception occurs while formatting the output
+ */
+ protected abstract String getInitialUIDL(WrappedRequest request, Root root)
+ throws PaintException, JSONException;
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java
new file mode 100644
index 0000000000..d80e626cc1
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java
@@ -0,0 +1,13 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventListener;
+
+public interface BootstrapListener extends EventListener {
+ public void modifyBootstrapFragment(BootstrapFragmentResponse response);
+
+ public void modifyBootstrapPage(BootstrapPageResponse response);
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java
new file mode 100644
index 0000000000..802238ac62
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Map;
+
+import org.jsoup.nodes.Document;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.WrappedRequest;
+
+public class BootstrapPageResponse extends BootstrapResponse {
+
+ private final Map<String, Object> headers;
+ private final Document document;
+
+ public BootstrapPageResponse(BootstrapHandler handler,
+ WrappedRequest request, Document document,
+ Map<String, Object> headers, Application application, Integer rootId) {
+ super(handler, request, application, rootId);
+ this.headers = headers;
+ this.document = document;
+ }
+
+ public void setHeader(String name, String value) {
+ headers.put(name, value);
+ }
+
+ public void setDateHeader(String name, long timestamp) {
+ headers.put(name, Long.valueOf(timestamp));
+ }
+
+ public Document getDocument() {
+ return document;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java
new file mode 100644
index 0000000000..88bd58593d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.EventObject;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.ui.Root;
+
+public abstract class BootstrapResponse extends EventObject {
+ private final WrappedRequest request;
+ private final Application application;
+ private final Integer rootId;
+
+ public BootstrapResponse(BootstrapHandler handler, WrappedRequest request,
+ Application application, Integer rootId) {
+ super(handler);
+ this.request = request;
+ this.application = application;
+ this.rootId = rootId;
+ }
+
+ public BootstrapHandler getBootstrapHandler() {
+ return (BootstrapHandler) getSource();
+ }
+
+ public WrappedRequest getRequest() {
+ return request;
+ }
+
+ public Application getApplication() {
+ return application;
+ }
+
+ public Integer getRootId() {
+ return rootId;
+ }
+
+ public Root getRoot() {
+ return Root.getCurrent();
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java
new file mode 100644
index 0000000000..8f0c80332f
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java
@@ -0,0 +1,39 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Map;
+
+import com.vaadin.ui.AbstractComponent.ComponentErrorEvent;
+import com.vaadin.ui.Component;
+
+@SuppressWarnings("serial")
+public class ChangeVariablesErrorEvent implements ComponentErrorEvent {
+
+ private Throwable throwable;
+ private Component component;
+
+ private Map<String, Object> variableChanges;
+
+ public ChangeVariablesErrorEvent(Component component, Throwable throwable,
+ Map<String, Object> variableChanges) {
+ this.component = component;
+ this.throwable = throwable;
+ this.variableChanges = variableChanges;
+ }
+
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ public Component getComponent() {
+ return component;
+ }
+
+ public Map<String, Object> getVariableChanges() {
+ return variableChanges;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java
new file mode 100644
index 0000000000..4f74cfe4bb
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ClientConnector.java
@@ -0,0 +1,149 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Collection;
+import java.util.List;
+
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.terminal.AbstractClientConnector;
+import com.vaadin.terminal.Extension;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.Root;
+
+/**
+ * Interface implemented by all connectors that are capable of communicating
+ * with the client side
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public interface ClientConnector extends Connector, RpcTarget {
+ /**
+ * Returns the list of pending server to client RPC calls and clears the
+ * list.
+ *
+ * @return an unmodifiable ordered list of pending server to client method
+ * calls (not null)
+ */
+ public List<ClientMethodInvocation> retrievePendingRpcCalls();
+
+ /**
+ * Checks if the communicator is enabled. An enabled communicator is allowed
+ * to receive messages from its counter-part.
+ *
+ * @return true if the connector can receive messages, false otherwise
+ */
+ public boolean isConnectorEnabled();
+
+ /**
+ * Returns the type of the shared state for this connector
+ *
+ * @return The type of the state. Must never return null.
+ */
+ public Class<? extends SharedState> getStateType();
+
+ @Override
+ public ClientConnector getParent();
+
+ /**
+ * Requests that the connector should be repainted as soon as possible.
+ */
+ public void requestRepaint();
+
+ /**
+ * Causes a repaint of this connector, and all connectors below it.
+ *
+ * This should only be used in special cases, e.g when the state of a
+ * descendant depends on the state of an ancestor.
+ */
+ public void requestRepaintAll();
+
+ /**
+ * Sets the parent connector of the connector.
+ *
+ * <p>
+ * This method automatically calls {@link #attach()} if the connector
+ * becomes attached to the application, regardless of whether it was
+ * attached previously. Conversely, if the parent is {@code null} and the
+ * connector is attached to the application, {@link #detach()} is called for
+ * the connector.
+ * </p>
+ * <p>
+ * This method is rarely called directly. One of the
+ * {@link ComponentContainer#addComponent(Component)} or
+ * {@link AbstractClientConnector#addExtension(Extension)} methods are
+ * normally used for adding connectors to a parent and they will call this
+ * method implicitly.
+ * </p>
+ *
+ * <p>
+ * It is not possible to change the parent without first setting the parent
+ * to {@code null}.
+ * </p>
+ *
+ * @param parent
+ * the parent connector
+ * @throws IllegalStateException
+ * if a parent is given even though the connector already has a
+ * parent
+ */
+ public void setParent(ClientConnector parent);
+
+ /**
+ * Notifies the connector that it is connected to an application.
+ *
+ * <p>
+ * The caller of this method is {@link #setParent(ClientConnector)} if the
+ * parent is itself already attached to the application. If not, the parent
+ * will call the {@link #attach()} for all its children when it is attached
+ * to the application. This method is always called before the connector's
+ * data is sent to the client-side for the first time.
+ * </p>
+ *
+ * <p>
+ * The attachment logic is implemented in {@link AbstractClientConnector}.
+ * </p>
+ */
+ public void attach();
+
+ /**
+ * Notifies the component that it is detached from the application.
+ *
+ * <p>
+ * The caller of this method is {@link #setParent(ClientConnector)} if the
+ * parent is in the application. When the parent is detached from the
+ * application it is its response to call {@link #detach()} for all the
+ * children and to detach itself from the terminal.
+ * </p>
+ */
+ public void detach();
+
+ /**
+ * Get a read-only collection of all extensions attached to this connector.
+ *
+ * @return a collection of extensions
+ */
+ public Collection<Extension> getExtensions();
+
+ /**
+ * Remove an extension from this connector.
+ *
+ * @param extension
+ * the extension to remove.
+ */
+ public void removeExtension(Extension extension);
+
+ /**
+ * Returns the root this connector is attached to
+ *
+ * @return The Root this connector is attached to or null if it is not
+ * attached to any Root
+ */
+ public Root getRoot();
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java
new file mode 100644
index 0000000000..64ea288665
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java
@@ -0,0 +1,71 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+
+/**
+ * Internal class for keeping track of pending server to client method
+ * invocations for a Connector.
+ *
+ * @since 7.0
+ */
+public class ClientMethodInvocation implements Serializable,
+ Comparable<ClientMethodInvocation> {
+ private final ClientConnector connector;
+ private final String interfaceName;
+ private final String methodName;
+ private final Object[] parameters;
+ private Type[] parameterTypes;
+
+ // used for sorting calls between different connectors in the same Root
+ private final long sequenceNumber;
+ // TODO may cause problems when clustering etc.
+ private static long counter = 0;
+
+ public ClientMethodInvocation(ClientConnector connector,
+ String interfaceName, Method method, Object[] parameters) {
+ this.connector = connector;
+ this.interfaceName = interfaceName;
+ methodName = method.getName();
+ parameterTypes = method.getGenericParameterTypes();
+ this.parameters = (null != parameters) ? parameters : new Object[0];
+ sequenceNumber = ++counter;
+ }
+
+ public Type[] getParameterTypes() {
+ return parameterTypes;
+ }
+
+ public ClientConnector getConnector() {
+ return connector;
+ }
+
+ public String getInterfaceName() {
+ return interfaceName;
+ }
+
+ public String getMethodName() {
+ return methodName;
+ }
+
+ public Object[] getParameters() {
+ return parameters;
+ }
+
+ protected long getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ @Override
+ public int compareTo(ClientMethodInvocation o) {
+ if (null == o) {
+ return 0;
+ }
+ return Long.signum(getSequenceNumber() - o.getSequenceNumber());
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java
new file mode 100644
index 0000000000..3cc3a8cb64
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java
@@ -0,0 +1,122 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.servlet.ServletContext;
+
+import com.vaadin.Application;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.ui.Root;
+
+/**
+ * Application manager processes changes and paints for single application
+ * instance.
+ *
+ * This class handles applications running as servlets.
+ *
+ * @see AbstractCommunicationManager
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class CommunicationManager extends AbstractCommunicationManager {
+
+ /**
+ * @deprecated use {@link #CommunicationManager(Application)} instead
+ * @param application
+ * @param applicationServlet
+ */
+ @Deprecated
+ public CommunicationManager(Application application,
+ AbstractApplicationServlet applicationServlet) {
+ super(application);
+ }
+
+ /**
+ * TODO New constructor - document me!
+ *
+ * @param application
+ */
+ public CommunicationManager(Application application) {
+ super(application);
+ }
+
+ @Override
+ protected BootstrapHandler createBootstrapHandler() {
+ return new BootstrapHandler() {
+ @Override
+ protected String getApplicationId(BootstrapContext context) {
+ String appUrl = getAppUri(context);
+
+ String appId = appUrl;
+ if ("".equals(appUrl)) {
+ appId = "ROOT";
+ }
+ appId = appId.replaceAll("[^a-zA-Z0-9]", "");
+ // Add hashCode to the end, so that it is still (sort of)
+ // predictable, but indicates that it should not be used in CSS
+ // and
+ // such:
+ int hashCode = appId.hashCode();
+ if (hashCode < 0) {
+ hashCode = -hashCode;
+ }
+ appId = appId + "-" + hashCode;
+ return appId;
+ }
+
+ @Override
+ protected String getAppUri(BootstrapContext context) {
+ /* Fetch relative url to application */
+ // don't use server and port in uri. It may cause problems with
+ // some
+ // virtual server configurations which lose the server name
+ Application application = context.getApplication();
+ URL url = application.getURL();
+ String appUrl = url.getPath();
+ if (appUrl.endsWith("/")) {
+ appUrl = appUrl.substring(0, appUrl.length() - 1);
+ }
+ return appUrl;
+ }
+
+ @Override
+ public String getThemeName(BootstrapContext context) {
+ String themeName = context.getRequest().getParameter(
+ AbstractApplicationServlet.URL_PARAMETER_THEME);
+ if (themeName == null) {
+ themeName = super.getThemeName(context);
+ }
+ return themeName;
+ }
+
+ @Override
+ protected String getInitialUIDL(WrappedRequest request, Root root)
+ throws PaintException, JSONException {
+ return CommunicationManager.this.getInitialUIDL(request, root);
+ }
+ };
+ }
+
+ @Override
+ protected InputStream getThemeResourceAsStream(Root root, String themeName,
+ String resource) {
+ WebApplicationContext context = (WebApplicationContext) root
+ .getApplication().getContext();
+ ServletContext servletContext = context.getHttpSession()
+ .getServletContext();
+ return servletContext.getResourceAsStream("/"
+ + AbstractApplicationServlet.THEME_DIRECTORY_PATH + themeName
+ + "/" + resource);
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java
new file mode 100644
index 0000000000..171d440796
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java
@@ -0,0 +1,664 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.Vector;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.terminal.Sizeable.Unit;
+import com.vaadin.ui.AbstractOrderedLayout;
+import com.vaadin.ui.AbstractSplitPanel;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.CustomComponent;
+import com.vaadin.ui.Form;
+import com.vaadin.ui.GridLayout;
+import com.vaadin.ui.GridLayout.Area;
+import com.vaadin.ui.Layout;
+import com.vaadin.ui.Panel;
+import com.vaadin.ui.TabSheet;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+
+@SuppressWarnings({ "serial", "deprecation" })
+public class ComponentSizeValidator implements Serializable {
+
+ private final static int LAYERS_SHOWN = 4;
+
+ /**
+ * Recursively checks given component and its subtree for invalid layout
+ * setups. Prints errors to std err stream.
+ *
+ * @param component
+ * component to check
+ * @return set of first level errors found
+ */
+ public static List<InvalidLayout> validateComponentRelativeSizes(
+ Component component, List<InvalidLayout> errors,
+ InvalidLayout parent) {
+
+ boolean invalidHeight = !checkHeights(component);
+ boolean invalidWidth = !checkWidths(component);
+
+ if (invalidHeight || invalidWidth) {
+ InvalidLayout error = new InvalidLayout(component, invalidHeight,
+ invalidWidth);
+ if (parent != null) {
+ parent.addError(error);
+ } else {
+ if (errors == null) {
+ errors = new LinkedList<InvalidLayout>();
+ }
+ errors.add(error);
+ }
+ parent = error;
+ }
+
+ if (component instanceof Panel) {
+ Panel panel = (Panel) component;
+ errors = validateComponentRelativeSizes(panel.getContent(), errors,
+ parent);
+ } else if (component instanceof ComponentContainer) {
+ ComponentContainer lo = (ComponentContainer) component;
+ Iterator<Component> it = lo.getComponentIterator();
+ while (it.hasNext()) {
+ errors = validateComponentRelativeSizes(it.next(), errors,
+ parent);
+ }
+ } else if (component instanceof Form) {
+ Form form = (Form) component;
+ if (form.getLayout() != null) {
+ errors = validateComponentRelativeSizes(form.getLayout(),
+ errors, parent);
+ }
+ if (form.getFooter() != null) {
+ errors = validateComponentRelativeSizes(form.getFooter(),
+ errors, parent);
+ }
+ }
+
+ return errors;
+ }
+
+ private static void printServerError(String msg,
+ Stack<ComponentInfo> attributes, boolean widthError,
+ PrintStream errorStream) {
+ StringBuffer err = new StringBuffer();
+ err.append("Vaadin DEBUG\n");
+
+ StringBuilder indent = new StringBuilder("");
+ ComponentInfo ci;
+ if (attributes != null) {
+ while (attributes.size() > LAYERS_SHOWN) {
+ attributes.pop();
+ }
+ while (!attributes.empty()) {
+ ci = attributes.pop();
+ showComponent(ci.component, ci.info, err, indent, widthError);
+ }
+ }
+
+ err.append("Layout problem detected: ");
+ err.append(msg);
+ err.append("\n");
+ err.append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n");
+ errorStream.println(err);
+
+ }
+
+ public static boolean checkHeights(Component component) {
+ try {
+ if (!hasRelativeHeight(component)) {
+ return true;
+ }
+ if (component instanceof Window) {
+ return true;
+ }
+ if (component.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineHeight(component);
+ } catch (Exception e) {
+ getLogger().log(Level.FINER,
+ "An exception occurred while validating sizes.", e);
+ return true;
+ }
+ }
+
+ public static boolean checkWidths(Component component) {
+ try {
+ if (!hasRelativeWidth(component)) {
+ return true;
+ }
+ if (component instanceof Window) {
+ return true;
+ }
+ if (component.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineWidth(component);
+ } catch (Exception e) {
+ getLogger().log(Level.FINER,
+ "An exception occurred while validating sizes.", e);
+ return true;
+ }
+ }
+
+ public static class InvalidLayout implements Serializable {
+
+ private final Component component;
+
+ private final boolean invalidHeight;
+ private final boolean invalidWidth;
+
+ private final Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>();
+
+ public InvalidLayout(Component component, boolean height, boolean width) {
+ this.component = component;
+ invalidHeight = height;
+ invalidWidth = width;
+ }
+
+ public void addError(InvalidLayout error) {
+ subErrors.add(error);
+ }
+
+ public void reportErrors(PrintWriter clientJSON,
+ AbstractCommunicationManager communicationManager,
+ PrintStream serverErrorStream) {
+ clientJSON.write("{");
+
+ Component parent = component.getParent();
+ String paintableId = component.getConnectorId();
+
+ clientJSON.print("id:\"" + paintableId + "\"");
+
+ if (invalidHeight) {
+ Stack<ComponentInfo> attributes = null;
+ String msg = "";
+ // set proper error messages
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean vertical = false;
+
+ if (ol instanceof VerticalLayout) {
+ vertical = true;
+ }
+
+ if (vertical) {
+ msg = "Component with relative height inside a VerticalLayout with no height defined.";
+ attributes = getHeightAttributes(component);
+ } else {
+ msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined";
+ attributes = getHeightAttributes(component);
+ }
+ } else if (parent instanceof GridLayout) {
+ msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined.";
+ attributes = getHeightAttributes(component);
+ } else {
+ // default error for non sized parent issue
+ msg = "A component with relative height needs a parent with defined height.";
+ attributes = getHeightAttributes(component);
+ }
+ printServerError(msg, attributes, false, serverErrorStream);
+ clientJSON.print(",\"heightMsg\":\"" + msg + "\"");
+ }
+ if (invalidWidth) {
+ Stack<ComponentInfo> attributes = null;
+ String msg = "";
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean horizontal = true;
+
+ if (ol instanceof VerticalLayout) {
+ horizontal = false;
+ }
+
+ if (horizontal) {
+ msg = "Component with relative width inside a HorizontalLayout with no width defined";
+ attributes = getWidthAttributes(component);
+ } else {
+ msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined";
+ attributes = getWidthAttributes(component);
+ }
+ } else if (parent instanceof GridLayout) {
+ msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined.";
+ attributes = getWidthAttributes(component);
+ } else {
+ // default error for non sized parent issue
+ msg = "A component with relative width needs a parent with defined width.";
+ attributes = getWidthAttributes(component);
+ }
+ clientJSON.print(",\"widthMsg\":\"" + msg + "\"");
+ printServerError(msg, attributes, true, serverErrorStream);
+ }
+ if (subErrors.size() > 0) {
+ serverErrorStream.println("Sub errors >>");
+ clientJSON.write(", \"subErrors\" : [");
+ boolean first = true;
+ for (InvalidLayout subError : subErrors) {
+ if (!first) {
+ clientJSON.print(",");
+ } else {
+ first = false;
+ }
+ subError.reportErrors(clientJSON, communicationManager,
+ serverErrorStream);
+ }
+ clientJSON.write("]");
+ serverErrorStream.println("<< Sub erros");
+ }
+ clientJSON.write("}");
+ }
+ }
+
+ private static class ComponentInfo implements Serializable {
+ Component component;
+ String info;
+
+ public ComponentInfo(Component component, String info) {
+ this.component = component;
+ this.info = info;
+ }
+
+ }
+
+ private static Stack<ComponentInfo> getHeightAttributes(Component component) {
+ Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
+ attributes
+ .add(new ComponentInfo(component, getHeightString(component)));
+ Component parent = component.getParent();
+ attributes.add(new ComponentInfo(parent, getHeightString(parent)));
+
+ while ((parent = parent.getParent()) != null) {
+ attributes.add(new ComponentInfo(parent, getHeightString(parent)));
+ }
+
+ return attributes;
+ }
+
+ private static Stack<ComponentInfo> getWidthAttributes(Component component) {
+ Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
+ attributes.add(new ComponentInfo(component, getWidthString(component)));
+ Component parent = component.getParent();
+ attributes.add(new ComponentInfo(parent, getWidthString(parent)));
+
+ while ((parent = parent.getParent()) != null) {
+ attributes.add(new ComponentInfo(parent, getWidthString(parent)));
+ }
+
+ return attributes;
+ }
+
+ private static String getWidthString(Component component) {
+ String width = "width: ";
+ if (hasRelativeWidth(component)) {
+ width += "RELATIVE, " + component.getWidth() + " %";
+ } else if (component instanceof Window && component.getParent() == null) {
+ width += "MAIN WINDOW";
+ } else if (component.getWidth() >= 0) {
+ width += "ABSOLUTE, " + component.getWidth() + " "
+ + component.getWidthUnits().getSymbol();
+ } else {
+ width += "UNDEFINED";
+ }
+
+ return width;
+ }
+
+ private static String getHeightString(Component component) {
+ String height = "height: ";
+ if (hasRelativeHeight(component)) {
+ height += "RELATIVE, " + component.getHeight() + " %";
+ } else if (component instanceof Window && component.getParent() == null) {
+ height += "MAIN WINDOW";
+ } else if (component.getHeight() > 0) {
+ height += "ABSOLUTE, " + component.getHeight() + " "
+ + component.getHeightUnits().getSymbol();
+ } else {
+ height += "UNDEFINED";
+ }
+
+ return height;
+ }
+
+ private static void showComponent(Component component, String attribute,
+ StringBuffer err, StringBuilder indent, boolean widthError) {
+
+ FileLocation createLoc = creationLocations.get(component);
+
+ FileLocation sizeLoc;
+ if (widthError) {
+ sizeLoc = widthLocations.get(component);
+ } else {
+ sizeLoc = heightLocations.get(component);
+ }
+
+ err.append(indent);
+ indent.append(" ");
+ err.append("- ");
+
+ err.append(component.getClass().getSimpleName());
+ err.append("/").append(Integer.toHexString(component.hashCode()));
+
+ if (component.getCaption() != null) {
+ err.append(" \"");
+ err.append(component.getCaption());
+ err.append("\"");
+ }
+
+ if (component.getDebugId() != null) {
+ err.append(" debugId: ");
+ err.append(component.getDebugId());
+ }
+
+ if (createLoc != null) {
+ err.append(", created at (" + createLoc.file + ":"
+ + createLoc.lineNumber + ")");
+
+ }
+
+ if (attribute != null) {
+ err.append(" (");
+ err.append(attribute);
+ if (sizeLoc != null) {
+ err.append(", set at (" + sizeLoc.file + ":"
+ + sizeLoc.lineNumber + ")");
+ }
+
+ err.append(")");
+ }
+ err.append("\n");
+
+ }
+
+ private static boolean hasNonRelativeHeightComponent(
+ AbstractOrderedLayout ol) {
+ Iterator<Component> it = ol.getComponentIterator();
+ while (it.hasNext()) {
+ if (!hasRelativeHeight(it.next())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static boolean parentCanDefineHeight(Component component) {
+ Component parent = component.getParent();
+ if (parent == null) {
+ // main window, valid situation
+ return true;
+ }
+ if (parent.getHeight() < 0) {
+ // Undefined height
+ if (parent instanceof Window) {
+ // Sub window with undefined size has a min-height
+ return true;
+ }
+
+ if (parent instanceof AbstractOrderedLayout) {
+ boolean horizontal = true;
+ if (parent instanceof VerticalLayout) {
+ horizontal = false;
+ }
+ if (horizontal
+ && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) {
+ return true;
+ } else {
+ return false;
+ }
+
+ } else if (parent instanceof GridLayout) {
+ GridLayout gl = (GridLayout) parent;
+ Area componentArea = gl.getComponentArea(component);
+ boolean rowHasHeight = false;
+ for (int row = componentArea.getRow1(); !rowHasHeight
+ && row <= componentArea.getRow2(); row++) {
+ for (int column = 0; !rowHasHeight
+ && column < gl.getColumns(); column++) {
+ Component c = gl.getComponent(column, row);
+ if (c != null) {
+ rowHasHeight = !hasRelativeHeight(c);
+ }
+ }
+ }
+ if (!rowHasHeight) {
+ return false;
+ } else {
+ // Other components define row height
+ return true;
+ }
+ }
+
+ if (parent instanceof Panel || parent instanceof AbstractSplitPanel
+ || parent instanceof TabSheet
+ || parent instanceof CustomComponent) {
+ // height undefined, we know how how component works and no
+ // exceptions
+ // TODO horiz SplitPanel ??
+ return false;
+ } else {
+ // We cannot generally know if undefined component can serve
+ // space for children (like CustomLayout or component built by
+ // third party) so we assume they can
+ return true;
+ }
+
+ } else if (hasRelativeHeight(parent)) {
+ // Relative height
+ if (parent.getParent() != null) {
+ return parentCanDefineHeight(parent);
+ } else {
+ return true;
+ }
+ } else {
+ // Absolute height
+ return true;
+ }
+ }
+
+ private static boolean hasRelativeHeight(Component component) {
+ return (component.getHeightUnits() == Unit.PERCENTAGE && component
+ .getHeight() > 0);
+ }
+
+ private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) {
+ Iterator<Component> it = ol.getComponentIterator();
+ while (it.hasNext()) {
+ if (!hasRelativeWidth(it.next())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasRelativeWidth(Component paintable) {
+ return paintable.getWidth() > 0
+ && paintable.getWidthUnits() == Unit.PERCENTAGE;
+ }
+
+ public static boolean parentCanDefineWidth(Component component) {
+ Component parent = component.getParent();
+ if (parent == null) {
+ // main window, valid situation
+ return true;
+ }
+ if (parent instanceof Window) {
+ // Sub window with undefined size has a min-width
+ return true;
+ }
+
+ if (parent.getWidth() < 0) {
+ // Undefined width
+
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean horizontal = true;
+ if (ol instanceof VerticalLayout) {
+ horizontal = false;
+ }
+
+ if (!horizontal && hasNonRelativeWidthComponent(ol)) {
+ // valid situation, other components defined width
+ return true;
+ } else {
+ return false;
+ }
+ } else if (parent instanceof GridLayout) {
+ GridLayout gl = (GridLayout) parent;
+ Area componentArea = gl.getComponentArea(component);
+ boolean columnHasWidth = false;
+ for (int col = componentArea.getColumn1(); !columnHasWidth
+ && col <= componentArea.getColumn2(); col++) {
+ for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) {
+ Component c = gl.getComponent(col, row);
+ if (c != null) {
+ columnHasWidth = !hasRelativeWidth(c);
+ }
+ }
+ }
+ if (!columnHasWidth) {
+ return false;
+ } else {
+ // Other components define column width
+ return true;
+ }
+ } else if (parent instanceof Form) {
+ /*
+ * If some other part of the form is not relative it determines
+ * the component width
+ */
+ return hasNonRelativeWidthComponent((Form) parent);
+ } else if (parent instanceof AbstractSplitPanel
+ || parent instanceof TabSheet
+ || parent instanceof CustomComponent) {
+ // FIXME Could we use com.vaadin package name here and
+ // fail for all component containers?
+ // FIXME Actually this should be moved to containers so it can
+ // be implemented for custom containers
+ // TODO vertical splitpanel with another non relative component?
+ return false;
+ } else if (parent instanceof Window) {
+ // Sub window can define width based on caption
+ if (parent.getCaption() != null
+ && !parent.getCaption().equals("")) {
+ return true;
+ } else {
+ return false;
+ }
+ } else if (parent instanceof Panel) {
+ // TODO Panel should be able to define width based on caption
+ return false;
+ } else {
+ return true;
+ }
+ } else if (hasRelativeWidth(parent)) {
+ // Relative width
+ if (parent.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineWidth(parent);
+ } else {
+ return true;
+ }
+
+ }
+
+ private static boolean hasNonRelativeWidthComponent(Form form) {
+ Layout layout = form.getLayout();
+ Layout footer = form.getFooter();
+
+ if (layout != null && !hasRelativeWidth(layout)) {
+ return true;
+ }
+ if (footer != null && !hasRelativeWidth(footer)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>();
+ private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>();
+ private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>();
+
+ public static class FileLocation implements Serializable {
+ public String method;
+ public String file;
+ public String className;
+ public String classNameSimple;
+ public int lineNumber;
+
+ public FileLocation(StackTraceElement traceElement) {
+ file = traceElement.getFileName();
+ className = traceElement.getClassName();
+ classNameSimple = className
+ .substring(className.lastIndexOf('.') + 1);
+ lineNumber = traceElement.getLineNumber();
+ method = traceElement.getMethodName();
+ }
+ }
+
+ public static void setCreationLocation(Object object) {
+ setLocation(creationLocations, object);
+ }
+
+ public static void setWidthLocation(Object object) {
+ setLocation(widthLocations, object);
+ }
+
+ public static void setHeightLocation(Object object) {
+ setLocation(heightLocations, object);
+ }
+
+ private static void setLocation(Map<Object, FileLocation> map, Object object) {
+ StackTraceElement[] traceLines = Thread.currentThread().getStackTrace();
+ for (StackTraceElement traceElement : traceLines) {
+ Class<?> cls;
+ try {
+ String className = traceElement.getClassName();
+ if (className.startsWith("java.")
+ || className.startsWith("sun.")) {
+ continue;
+ }
+
+ cls = Class.forName(className);
+ if (cls == ComponentSizeValidator.class || cls == Thread.class) {
+ continue;
+ }
+
+ if (Component.class.isAssignableFrom(cls)
+ && !CustomComponent.class.isAssignableFrom(cls)) {
+ continue;
+ }
+ FileLocation cl = new FileLocation(traceElement);
+ map.put(object, cl);
+ return;
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ getLogger().log(Level.FINER,
+ "An exception occurred while validating sizes.", e);
+ }
+
+ }
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(ComponentSizeValidator.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/Constants.java b/server/src/com/vaadin/terminal/gwt/server/Constants.java
new file mode 100644
index 0000000000..7efb0205ac
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/Constants.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+/**
+ * TODO Document me!
+ *
+ * @author peholmst
+ *
+ */
+public interface Constants {
+
+ static final String NOT_PRODUCTION_MODE_INFO = "\n"
+ + "=================================================================\n"
+ + "Vaadin is running in DEBUG MODE.\nAdd productionMode=true to web.xml "
+ + "to disable debug features.\nTo show debug window, add ?debug to "
+ + "your application URL.\n"
+ + "=================================================================";
+
+ static final String WARNING_XSRF_PROTECTION_DISABLED = "\n"
+ + "===========================================================\n"
+ + "WARNING: Cross-site request forgery protection is disabled!\n"
+ + "===========================================================";
+
+ static final String WARNING_RESOURCE_CACHING_TIME_NOT_NUMERIC = "\n"
+ + "===========================================================\n"
+ + "WARNING: resourceCacheTime has been set to a non integer value "
+ + "in web.xml. The default of 1h 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"
+ + "version in use. This might cause strange problems - a\n"
+ + "recompile/deploy is strongly recommended.\n"
+ + " Vaadin version: %s\n"
+ + " Widgetset version: %s\n"
+ + "=================================================================";
+
+ static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication";
+ static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication";
+ static final String URL_PARAMETER_REPAINT_ALL = "repaintAll";
+ static final String URL_PARAMETER_THEME = "theme";
+
+ static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode";
+ static final String SERVLET_PARAMETER_DISABLE_XSRF_PROTECTION = "disable-xsrf-protection";
+ static final String SERVLET_PARAMETER_RESOURCE_CACHE_TIME = "resourceCacheTime";
+
+ // Configurable parameter names
+ static final String PARAMETER_VAADIN_RESOURCES = "Resources";
+
+ static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
+
+ static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+ final String THEME_DIRECTORY_PATH = "VAADIN/themes/";
+
+ static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24;
+
+ static final String WIDGETSET_DIRECTORY_PATH = "VAADIN/widgetsets/";
+
+ // Name of the default widget set, used if not specified in web.xml
+ static final String DEFAULT_WIDGETSET = "com.vaadin.terminal.gwt.DefaultWidgetSet";
+
+ // Widget set parameter name
+ static final String PARAMETER_WIDGETSET = "widgetset";
+
+ static final String ERROR_NO_ROOT_FOUND = "Application did not return a root for the request and did not request extra information either. Something is wrong.";
+
+ static final String DEFAULT_THEME_NAME = "reindeer";
+
+ static final String INVALID_SECURITY_KEY_MSG = "Invalid security key.";
+
+ // portal configuration parameters
+ static final String PORTAL_PARAMETER_VAADIN_WIDGETSET = "vaadin.widgetset";
+ static final String PORTAL_PARAMETER_VAADIN_RESOURCE_PATH = "vaadin.resources.path";
+ static final String PORTAL_PARAMETER_VAADIN_THEME = "vaadin.theme";
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java
new file mode 100644
index 0000000000..efb5666efa
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java
@@ -0,0 +1,313 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.TransferableImpl;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.event.dd.acceptcriteria.AcceptCriterion;
+import com.vaadin.shared.communication.SharedState;
+import com.vaadin.shared.ui.dd.DragEventType;
+import com.vaadin.terminal.Extension;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Root;
+
+public class DragAndDropService implements VariableOwner, ClientConnector {
+
+ private int lastVisitId;
+
+ private boolean lastVisitAccepted = false;
+
+ private DragAndDropEvent dragEvent;
+
+ private final AbstractCommunicationManager manager;
+
+ private AcceptCriterion acceptCriterion;
+
+ public DragAndDropService(AbstractCommunicationManager manager) {
+ this.manager = manager;
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ Object owner = variables.get("dhowner");
+
+ // Validate drop handler owner
+ if (!(owner instanceof DropTarget)) {
+ getLogger()
+ .severe("DropHandler owner " + owner
+ + " must implement DropTarget");
+ return;
+ }
+ // owner cannot be null here
+
+ DropTarget dropTarget = (DropTarget) owner;
+ lastVisitId = (Integer) variables.get("visitId");
+
+ // request may be dropRequest or request during drag operation (commonly
+ // dragover or dragenter)
+ boolean dropRequest = isDropRequest(variables);
+ if (dropRequest) {
+ handleDropRequest(dropTarget, variables);
+ } else {
+ handleDragRequest(dropTarget, variables);
+ }
+
+ }
+
+ /**
+ * Handles a drop request from the VDragAndDropManager.
+ *
+ * @param dropTarget
+ * @param variables
+ */
+ private void handleDropRequest(DropTarget dropTarget,
+ Map<String, Object> variables) {
+ DropHandler dropHandler = (dropTarget).getDropHandler();
+ if (dropHandler == null) {
+ // No dropHandler returned so no drop can be performed.
+ getLogger().fine(
+ "DropTarget.getDropHandler() returned null for owner: "
+ + dropTarget);
+ return;
+ }
+
+ /*
+ * Construct the Transferable and the DragDropDetails for the drop
+ * operation based on the info passed from the client widgets (drag
+ * source for Transferable, drop target for DragDropDetails).
+ */
+ Transferable transferable = constructTransferable(dropTarget, variables);
+ TargetDetails dropData = constructDragDropDetails(dropTarget, variables);
+ DragAndDropEvent dropEvent = new DragAndDropEvent(transferable,
+ dropData);
+ if (dropHandler.getAcceptCriterion().accept(dropEvent)) {
+ dropHandler.drop(dropEvent);
+ }
+ }
+
+ /**
+ * Handles a drag/move request from the VDragAndDropManager.
+ *
+ * @param dropTarget
+ * @param variables
+ */
+ private void handleDragRequest(DropTarget dropTarget,
+ Map<String, Object> variables) {
+ lastVisitId = (Integer) variables.get("visitId");
+
+ acceptCriterion = dropTarget.getDropHandler().getAcceptCriterion();
+
+ /*
+ * Construct the Transferable and the DragDropDetails for the drag
+ * operation based on the info passed from the client widgets (drag
+ * source for Transferable, current target for DragDropDetails).
+ */
+ Transferable transferable = constructTransferable(dropTarget, variables);
+ TargetDetails dragDropDetails = constructDragDropDetails(dropTarget,
+ variables);
+
+ dragEvent = new DragAndDropEvent(transferable, dragDropDetails);
+
+ lastVisitAccepted = acceptCriterion.accept(dragEvent);
+ }
+
+ /**
+ * Construct DragDropDetails based on variables from client drop target.
+ * Uses DragDropDetailsTranslator if available, otherwise a default
+ * DragDropDetails implementation is used.
+ *
+ * @param dropTarget
+ * @param variables
+ * @return
+ */
+ @SuppressWarnings("unchecked")
+ private TargetDetails constructDragDropDetails(DropTarget dropTarget,
+ Map<String, Object> variables) {
+ Map<String, Object> rawDragDropDetails = (Map<String, Object>) variables
+ .get("evt");
+
+ TargetDetails dropData = dropTarget
+ .translateDropTargetDetails(rawDragDropDetails);
+
+ if (dropData == null) {
+ // Create a default DragDropDetails with all the raw variables
+ dropData = new TargetDetailsImpl(rawDragDropDetails, dropTarget);
+ }
+
+ return dropData;
+ }
+
+ private boolean isDropRequest(Map<String, Object> variables) {
+ return getRequestType(variables) == DragEventType.DROP;
+ }
+
+ private DragEventType getRequestType(Map<String, Object> variables) {
+ int type = (Integer) variables.get("type");
+ return DragEventType.values()[type];
+ }
+
+ @SuppressWarnings("unchecked")
+ private Transferable constructTransferable(DropTarget dropHandlerOwner,
+ Map<String, Object> variables) {
+ final Component sourceComponent = (Component) variables
+ .get("component");
+
+ variables = (Map<String, Object>) variables.get("tra");
+
+ Transferable transferable = null;
+ if (sourceComponent != null && sourceComponent instanceof DragSource) {
+ transferable = ((DragSource) sourceComponent)
+ .getTransferable(variables);
+ }
+ if (transferable == null) {
+ transferable = new TransferableImpl(sourceComponent, variables);
+ }
+
+ return transferable;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isConnectorEnabled();
+ }
+
+ @Override
+ public boolean isImmediate() {
+ return true;
+ }
+
+ void printJSONResponse(PrintWriter outWriter) throws PaintException {
+ if (isDirty()) {
+
+ outWriter.print(", \"dd\":");
+
+ JsonPaintTarget jsonPaintTarget = new JsonPaintTarget(manager,
+ outWriter, false);
+ jsonPaintTarget.startTag("dd");
+ jsonPaintTarget.addAttribute("visitId", lastVisitId);
+ if (acceptCriterion != null) {
+ jsonPaintTarget.addAttribute("accepted", lastVisitAccepted);
+ acceptCriterion.paintResponse(jsonPaintTarget);
+ }
+ jsonPaintTarget.endTag("dd");
+ jsonPaintTarget.close();
+ lastVisitId = -1;
+ lastVisitAccepted = false;
+ acceptCriterion = null;
+ dragEvent = null;
+ }
+ }
+
+ private boolean isDirty() {
+ if (lastVisitId > 0) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public SharedState getState() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getConnectorId() {
+ return VDragAndDropManager.DD_SERVICE;
+ }
+
+ @Override
+ public boolean isConnectorEnabled() {
+ // Drag'n'drop can't be disabled
+ return true;
+ }
+
+ @Override
+ public List<ClientMethodInvocation> retrievePendingRpcCalls() {
+ return null;
+ }
+
+ @Override
+ public RpcManager getRpcManager(Class<?> rpcInterface) {
+ // TODO Use rpc for drag'n'drop
+ return null;
+ }
+
+ @Override
+ public Class<? extends SharedState> getStateType() {
+ return SharedState.class;
+ }
+
+ @Override
+ public void requestRepaint() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public ClientConnector getParent() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void requestRepaintAll() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void attach() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void detach() {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Collection<Extension> getExtensions() {
+ // TODO Auto-generated method stub
+ return Collections.emptySet();
+ }
+
+ @Override
+ public void removeExtension(Extension extension) {
+ // TODO Auto-generated method stub
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(DragAndDropService.class.getName());
+ }
+
+ @Override
+ public Root getRoot() {
+ return null;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java
new file mode 100644
index 0000000000..cc12c9cc43
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java
@@ -0,0 +1,417 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import com.google.appengine.api.datastore.Blob;
+import com.google.appengine.api.datastore.DatastoreService;
+import com.google.appengine.api.datastore.DatastoreServiceFactory;
+import com.google.appengine.api.datastore.Entity;
+import com.google.appengine.api.datastore.EntityNotFoundException;
+import com.google.appengine.api.datastore.FetchOptions.Builder;
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.KeyFactory;
+import com.google.appengine.api.datastore.PreparedQuery;
+import com.google.appengine.api.datastore.Query;
+import com.google.appengine.api.datastore.Query.FilterOperator;
+import com.google.appengine.api.memcache.Expiration;
+import com.google.appengine.api.memcache.MemcacheService;
+import com.google.appengine.api.memcache.MemcacheServiceFactory;
+import com.google.apphosting.api.DeadlineExceededException;
+import com.vaadin.service.ApplicationContext;
+
+/**
+ * ApplicationServlet to be used when deploying to Google App Engine, in
+ * web.xml:
+ *
+ * <pre>
+ * &lt;servlet&gt;
+ * &lt;servlet-name&gt;HelloWorld&lt;/servlet-name&gt;
+ * &lt;servlet-class&gt;com.vaadin.terminal.gwt.server.GAEApplicationServlet&lt;/servlet-class&gt;
+ * &lt;init-param&gt;
+ * &lt;param-name&gt;application&lt;/param-name&gt;
+ * &lt;param-value&gt;com.vaadin.demo.HelloWorld&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * &lt;/servlet&gt;
+ * </pre>
+ *
+ * Session support must be enabled in appengine-web.xml:
+ *
+ * <pre>
+ * &lt;sessions-enabled&gt;true&lt;/sessions-enabled&gt;
+ * </pre>
+ *
+ * Appengine datastore cleanup can be invoked by calling one of the applications
+ * with an additional path "/CLEAN". This can be set up as a cron-job in
+ * cron.xml (see appengine documentation for more information):
+ *
+ * <pre>
+ * &lt;cronentries&gt;
+ * &lt;cron&gt;
+ * &lt;url&gt;/HelloWorld/CLEAN&lt;/url&gt;
+ * &lt;description&gt;Clean up sessions&lt;/description&gt;
+ * &lt;schedule&gt;every 2 hours&lt;/schedule&gt;
+ * &lt;/cron&gt;
+ * &lt;/cronentries&gt;
+ * </pre>
+ *
+ * It is recommended (but not mandatory) to extract themes and widgetsets and
+ * have App Engine server these statically. Extract VAADIN folder (and it's
+ * contents) 'next to' the WEB-INF folder, and add the following to
+ * appengine-web.xml:
+ *
+ * <pre>
+ * &lt;static-files&gt;
+ * &lt;include path=&quot;/VAADIN/**&quot; /&gt;
+ * &lt;/static-files&gt;
+ * </pre>
+ *
+ * Additional limitations:
+ * <ul>
+ * <li/>Do not change application state when serving an ApplicationResource.
+ * <li/>Avoid changing application state in transaction handlers, unless you're
+ * confident you fully understand the synchronization issues in App Engine.
+ * <li/>The application remains locked while uploading - no progressbar is
+ * possible.
+ * </ul>
+ */
+public class GAEApplicationServlet extends ApplicationServlet {
+
+ // memcache mutex is MUTEX_BASE + sessio id
+ private static final String MUTEX_BASE = "_vmutex";
+
+ // used identify ApplicationContext in memcache and datastore
+ private static final String AC_BASE = "_vac";
+
+ // UIDL requests will attempt to gain access for this long before telling
+ // the client to retry
+ private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000;
+
+ // Tell client to retry after this delay.
+ // Note: currently interpreting Retry-After as ms, not sec
+ private static final int RETRY_AFTER_MILLISECONDS = 100;
+
+ // Properties used in the datastore
+ private static final String PROPERTY_EXPIRES = "expires";
+ private static final String PROPERTY_DATA = "data";
+
+ // path used for cleanup
+ private static final String CLEANUP_PATH = "/CLEAN";
+ // max entities to clean at once
+ private static final int CLEANUP_LIMIT = 200;
+ // appengine session kind
+ private static final String APPENGINE_SESSION_KIND = "_ah_SESSION";
+ // appengine session expires-parameter
+ private static final String PROPERTY_APPENGINE_EXPIRES = "_expires";
+
+ protected void sendDeadlineExceededNotification(
+ WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response) throws IOException {
+ criticalNotification(
+ request,
+ response,
+ "Deadline Exceeded",
+ "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...",
+ "", null);
+ }
+
+ protected void sendNotSerializableNotification(
+ WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response) throws IOException {
+ criticalNotification(
+ request,
+ response,
+ "NotSerializableException",
+ "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...",
+ "", getApplicationUrl(request).toString()
+ + "?restartApplication");
+ }
+
+ protected void sendCriticalErrorNotification(
+ WrappedHttpServletRequest request,
+ WrappedHttpServletResponse response) throws IOException {
+ criticalNotification(
+ request,
+ response,
+ "Critical error",
+ "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...",
+ "", getApplicationUrl(request).toString()
+ + "?restartApplication");
+ }
+
+ @Override
+ protected void service(HttpServletRequest unwrappedRequest,
+ HttpServletResponse unwrappedResponse) throws ServletException,
+ IOException {
+ WrappedHttpServletRequest request = new WrappedHttpServletRequest(
+ unwrappedRequest, getDeploymentConfiguration());
+ WrappedHttpServletResponse response = new WrappedHttpServletResponse(
+ unwrappedResponse, getDeploymentConfiguration());
+
+ if (isCleanupRequest(request)) {
+ cleanDatastore();
+ return;
+ }
+
+ RequestType requestType = getRequestType(request);
+
+ if (requestType == RequestType.STATIC_FILE) {
+ // no locking needed, let superclass handle
+ super.service(request, response);
+ cleanSession(request);
+ return;
+ }
+
+ if (requestType == RequestType.APPLICATION_RESOURCE) {
+ // no locking needed, let superclass handle
+ getApplicationContext(request,
+ MemcacheServiceFactory.getMemcacheService());
+ super.service(request, response);
+ cleanSession(request);
+ return;
+ }
+
+ final HttpSession session = request
+ .getSession(requestCanCreateApplication(request, requestType));
+ if (session == null) {
+ handleServiceSessionExpired(request, response);
+ cleanSession(request);
+ return;
+ }
+
+ boolean locked = false;
+ MemcacheService memcache = null;
+ String mutex = MUTEX_BASE + session.getId();
+ memcache = MemcacheServiceFactory.getMemcacheService();
+ try {
+ // 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 (!locked) {
+ // Not locked; only UIDL can get trough here unlocked: tell
+ // client to retry
+ response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ // Note: currently interpreting Retry-After as ms, not sec
+ response.setHeader("Retry-After", "" + RETRY_AFTER_MILLISECONDS);
+ return;
+ }
+
+ // de-serialize or create application context, store in session
+ ApplicationContext ctx = getApplicationContext(request, memcache);
+
+ super.service(request, response);
+
+ // serialize
+ started = new Date().getTime();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(baos);
+ oos.writeObject(ctx);
+ oos.flush();
+ byte[] bytes = baos.toByteArray();
+
+ started = new Date().getTime();
+
+ String id = AC_BASE + session.getId();
+ Date expire = new Date(started
+ + (session.getMaxInactiveInterval() * 1000));
+ Expiration expires = Expiration.onDate(expire);
+
+ memcache.put(id, bytes, expires);
+
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ Entity entity = new Entity(AC_BASE, id);
+ entity.setProperty(PROPERTY_EXPIRES, expire.getTime());
+ entity.setProperty(PROPERTY_DATA, new Blob(bytes));
+ ds.put(entity);
+
+ } catch (DeadlineExceededException e) {
+ getLogger().warning("DeadlineExceeded for " + session.getId());
+ sendDeadlineExceededNotification(request, response);
+ } catch (NotSerializableException e) {
+ getLogger().log(Level.SEVERE, "Not serializable!", e);
+
+ // TODO this notification is usually not shown - should we redirect
+ // in some other way - can we?
+ sendNotSerializableNotification(request, response);
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING,
+ "An exception occurred while servicing request.", e);
+
+ sendCriticalErrorNotification(request, response);
+ } finally {
+ // "Next, please!"
+ if (locked) {
+ memcache.delete(mutex);
+ }
+ cleanSession(request);
+ }
+ }
+
+ protected ApplicationContext getApplicationContext(
+ HttpServletRequest request, MemcacheService memcache) {
+ HttpSession session = request.getSession();
+ String id = AC_BASE + session.getId();
+ byte[] serializedAC = (byte[]) memcache.get(id);
+ if (serializedAC == null) {
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ Key key = KeyFactory.createKey(AC_BASE, id);
+ Entity entity = null;
+ try {
+ entity = ds.get(key);
+ } catch (EntityNotFoundException e) {
+ // Ok, we were a bit optimistic; we'll create a new one later
+ }
+ if (entity != null) {
+ Blob blob = (Blob) entity.getProperty(PROPERTY_DATA);
+ serializedAC = blob.getBytes();
+ // bring it to memcache
+ memcache.put(AC_BASE + session.getId(), serializedAC,
+ Expiration.byDeltaSeconds(session
+ .getMaxInactiveInterval()),
+ MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT);
+ }
+ }
+ if (serializedAC != null) {
+ ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC);
+ ObjectInputStream ois;
+ try {
+ ois = new ObjectInputStream(bais);
+ ApplicationContext applicationContext = (ApplicationContext) ois
+ .readObject();
+ session.setAttribute(WebApplicationContext.class.getName(),
+ applicationContext);
+ } catch (IOException e) {
+ getLogger().log(
+ Level.WARNING,
+ "Could not de-serialize ApplicationContext for "
+ + session.getId()
+ + " A new one will be created. ", e);
+ } catch (ClassNotFoundException e) {
+ getLogger().log(
+ Level.WARNING,
+ "Could not de-serialize ApplicationContext for "
+ + session.getId()
+ + " A new one will be created. ", e);
+ }
+ }
+ // will create new context if the above did not
+ return getApplicationContext(session);
+
+ }
+
+ private boolean isCleanupRequest(HttpServletRequest request) {
+ String path = getRequestPathInfo(request);
+ if (path != null && path.equals(CLEANUP_PATH)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Removes the ApplicationContext from the session in order to minimize the
+ * data serialized to datastore and memcache.
+ *
+ * @param request
+ */
+ private void cleanSession(HttpServletRequest request) {
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ session.removeAttribute(WebApplicationContext.class.getName());
+ }
+ }
+
+ /**
+ * This will look at the timestamp and delete expired persisted Vaadin and
+ * appengine sessions from the datastore.
+ *
+ * TODO Possible improvements include: 1. Use transactions (requires entity
+ * groups - overkill?) 2. Delete one-at-a-time, catch possible exception,
+ * continue w/ next.
+ */
+ private void cleanDatastore() {
+ long expire = new Date().getTime();
+ try {
+ DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
+ // Vaadin stuff first
+ {
+ Query q = new Query(AC_BASE);
+ q.setKeysOnly();
+
+ q.addFilter(PROPERTY_EXPIRES,
+ FilterOperator.LESS_THAN_OR_EQUAL, expire);
+ PreparedQuery pq = ds.prepare(q);
+ List<Entity> entities = pq.asList(Builder
+ .withLimit(CLEANUP_LIMIT));
+ if (entities != null) {
+ getLogger().info(
+ "Vaadin cleanup deleting " + entities.size()
+ + " expired Vaadin sessions.");
+ List<Key> keys = new ArrayList<Key>();
+ for (Entity e : entities) {
+ keys.add(e.getKey());
+ }
+ ds.delete(keys);
+ }
+ }
+ // Also cleanup GAE sessions
+ {
+ Query q = new Query(APPENGINE_SESSION_KIND);
+ q.setKeysOnly();
+ q.addFilter(PROPERTY_APPENGINE_EXPIRES,
+ FilterOperator.LESS_THAN_OR_EQUAL, expire);
+ PreparedQuery pq = ds.prepare(q);
+ List<Entity> entities = pq.asList(Builder
+ .withLimit(CLEANUP_LIMIT));
+ if (entities != null) {
+ getLogger().info(
+ "Vaadin cleanup deleting " + entities.size()
+ + " expired appengine sessions.");
+ List<Key> keys = new ArrayList<Key>();
+ for (Entity e : entities) {
+ keys.add(e.getKey());
+ }
+ ds.delete(keys);
+ }
+ }
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING, "Exception while cleaning.", e);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(GAEApplicationServlet.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java
new file mode 100644
index 0000000000..d811cadf86
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java
@@ -0,0 +1,54 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+
+import javax.servlet.Filter;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.vaadin.Application;
+import com.vaadin.service.ApplicationContext.TransactionListener;
+import com.vaadin.terminal.Terminal;
+
+/**
+ * {@link Application} that implements this interface gets notified of request
+ * start and end by terminal.
+ * <p>
+ * Interface can be used for several helper tasks including:
+ * <ul>
+ * <li>Opening and closing database connections
+ * <li>Implementing {@link ThreadLocal}
+ * <li>Setting/Getting {@link Cookie}
+ * </ul>
+ * <p>
+ * Alternatives for implementing similar features are are Servlet {@link Filter}
+ * s and {@link TransactionListener}s in Vaadin.
+ *
+ * @since 6.2
+ * @see PortletRequestListener
+ */
+public interface HttpServletRequestListener extends Serializable {
+
+ /**
+ * This method is called before {@link Terminal} applies the request to
+ * Application.
+ *
+ * @param request
+ * @param response
+ */
+ public void onRequestStart(HttpServletRequest request,
+ HttpServletResponse response);
+
+ /**
+ * This method is called at the end of each request.
+ *
+ * @param request
+ * @param response
+ */
+ public void onRequestEnd(HttpServletRequest request,
+ HttpServletResponse response);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java
new file mode 100644
index 0000000000..8199bc6ada
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/JsonCodec.java
@@ -0,0 +1,792 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.external.json.JSONObject;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.communication.UidlValue;
+import com.vaadin.terminal.gwt.client.communication.JsonEncoder;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ConnectorTracker;
+
+/**
+ * Decoder for converting RPC parameters and other values from JSON in transfer
+ * between the client and the server and vice versa.
+ *
+ * @since 7.0
+ */
+public class JsonCodec implements Serializable {
+
+ private static Map<Class<?>, String> typeToTransportType = new HashMap<Class<?>, String>();
+
+ /**
+ * Note! This does not contain primitives.
+ * <p>
+ */
+ private static Map<String, Class<?>> transportTypeToType = new HashMap<String, Class<?>>();
+
+ static {
+ registerType(String.class, JsonEncoder.VTYPE_STRING);
+ registerType(Connector.class, JsonEncoder.VTYPE_CONNECTOR);
+ registerType(Boolean.class, JsonEncoder.VTYPE_BOOLEAN);
+ registerType(boolean.class, JsonEncoder.VTYPE_BOOLEAN);
+ registerType(Integer.class, JsonEncoder.VTYPE_INTEGER);
+ registerType(int.class, JsonEncoder.VTYPE_INTEGER);
+ registerType(Float.class, JsonEncoder.VTYPE_FLOAT);
+ registerType(float.class, JsonEncoder.VTYPE_FLOAT);
+ registerType(Double.class, JsonEncoder.VTYPE_DOUBLE);
+ registerType(double.class, JsonEncoder.VTYPE_DOUBLE);
+ registerType(Long.class, JsonEncoder.VTYPE_LONG);
+ registerType(long.class, JsonEncoder.VTYPE_LONG);
+ registerType(String[].class, JsonEncoder.VTYPE_STRINGARRAY);
+ registerType(Object[].class, JsonEncoder.VTYPE_ARRAY);
+ registerType(Map.class, JsonEncoder.VTYPE_MAP);
+ registerType(HashMap.class, JsonEncoder.VTYPE_MAP);
+ registerType(List.class, JsonEncoder.VTYPE_LIST);
+ registerType(Set.class, JsonEncoder.VTYPE_SET);
+ }
+
+ private static void registerType(Class<?> type, String transportType) {
+ typeToTransportType.put(type, transportType);
+ if (!type.isPrimitive()) {
+ transportTypeToType.put(transportType, type);
+ }
+ }
+
+ public static boolean isInternalTransportType(String transportType) {
+ return transportTypeToType.containsKey(transportType);
+ }
+
+ public static boolean isInternalType(Type type) {
+ if (type instanceof Class && ((Class<?>) type).isPrimitive()) {
+ if (type == byte.class || type == char.class) {
+ // Almost all primitive types are handled internally
+ return false;
+ }
+ // All primitive types are handled internally
+ return true;
+ } else if (type == UidlValue.class) {
+ // UidlValue is a special internal type wrapping type info and a
+ // value
+ return true;
+ }
+ return typeToTransportType.containsKey(getClassForType(type));
+ }
+
+ private static Class<?> getClassForType(Type type) {
+ if (type instanceof ParameterizedType) {
+ return (Class<?>) (((ParameterizedType) type).getRawType());
+ } else if (type instanceof Class<?>) {
+ return (Class<?>) type;
+ } else {
+ return null;
+ }
+ }
+
+ private static Class<?> getType(String transportType) {
+ return transportTypeToType.get(transportType);
+ }
+
+ public static Object decodeInternalOrCustomType(Type targetType,
+ Object value, ConnectorTracker connectorTracker)
+ throws JSONException {
+ if (isInternalType(targetType)) {
+ return decodeInternalType(targetType, false, value,
+ connectorTracker);
+ } else {
+ return decodeCustomType(targetType, value, connectorTracker);
+ }
+ }
+
+ public static Object decodeCustomType(Type targetType, Object value,
+ ConnectorTracker connectorTracker) throws JSONException {
+ if (isInternalType(targetType)) {
+ throw new JSONException("decodeCustomType cannot be used for "
+ + targetType + ", which is an internal type");
+ }
+
+ // Try to decode object using fields
+ if (value == JSONObject.NULL) {
+ return null;
+ } else if (targetType == byte.class || targetType == Byte.class) {
+ return Byte.valueOf(String.valueOf(value));
+ } else if (targetType == char.class || targetType == Character.class) {
+ return Character.valueOf(String.valueOf(value).charAt(0));
+ } else if (targetType instanceof Class<?>
+ && ((Class<?>) targetType).isArray()) {
+ // Legacy Object[] and String[] handled elsewhere, this takes care
+ // of generic arrays
+ Class<?> componentType = ((Class<?>) targetType).getComponentType();
+ return decodeArray(componentType, (JSONArray) value,
+ connectorTracker);
+ } else if (targetType instanceof GenericArrayType) {
+ Type componentType = ((GenericArrayType) targetType)
+ .getGenericComponentType();
+ return decodeArray(componentType, (JSONArray) value,
+ connectorTracker);
+ } else if (targetType == JSONObject.class
+ || targetType == JSONArray.class) {
+ return value;
+ } else {
+ return decodeObject(targetType, (JSONObject) value,
+ connectorTracker);
+ }
+ }
+
+ private static Object decodeArray(Type componentType, JSONArray value,
+ ConnectorTracker connectorTracker) throws JSONException {
+ Class<?> componentClass = getClassForType(componentType);
+ Object array = Array.newInstance(componentClass, value.length());
+ for (int i = 0; i < value.length(); i++) {
+ Object decodedValue = decodeInternalOrCustomType(componentType,
+ value.get(i), connectorTracker);
+ Array.set(array, i, decodedValue);
+ }
+ return array;
+ }
+
+ /**
+ * Decodes a value that is of an internal type.
+ * <p>
+ * Ensures the encoded value is of the same type as target type.
+ * </p>
+ * <p>
+ * Allows restricting collections so that they must be declared using
+ * generics. If this is used then all objects in the collection are encoded
+ * using the declared type. Otherwise only internal types are allowed in
+ * collections.
+ * </p>
+ *
+ * @param targetType
+ * The type that should be returned by this method
+ * @param valueAndType
+ * The encoded value and type array
+ * @param application
+ * A reference to the application
+ * @param enforceGenericsInCollections
+ * true if generics should be enforce, false to only allow
+ * internal types in collections
+ * @return
+ * @throws JSONException
+ */
+ public static Object decodeInternalType(Type targetType,
+ boolean restrictToInternalTypes, Object encodedJsonValue,
+ ConnectorTracker connectorTracker) throws JSONException {
+ if (!isInternalType(targetType)) {
+ throw new JSONException("Type " + targetType
+ + " is not a supported internal type.");
+ }
+ String transportType = getInternalTransportType(targetType);
+
+ if (encodedJsonValue == JSONObject.NULL) {
+ return null;
+ }
+
+ // UidlValue
+ if (targetType == UidlValue.class) {
+ return decodeUidlValue((JSONArray) encodedJsonValue,
+ connectorTracker);
+ }
+
+ // Collections
+ if (JsonEncoder.VTYPE_LIST.equals(transportType)) {
+ return decodeList(targetType, restrictToInternalTypes,
+ (JSONArray) encodedJsonValue, connectorTracker);
+ } else if (JsonEncoder.VTYPE_SET.equals(transportType)) {
+ return decodeSet(targetType, restrictToInternalTypes,
+ (JSONArray) encodedJsonValue, connectorTracker);
+ } else if (JsonEncoder.VTYPE_MAP.equals(transportType)) {
+ return decodeMap(targetType, restrictToInternalTypes,
+ encodedJsonValue, connectorTracker);
+ }
+
+ // Arrays
+ if (JsonEncoder.VTYPE_ARRAY.equals(transportType)) {
+
+ return decodeObjectArray(targetType, (JSONArray) encodedJsonValue,
+ connectorTracker);
+
+ } else if (JsonEncoder.VTYPE_STRINGARRAY.equals(transportType)) {
+ return decodeStringArray((JSONArray) encodedJsonValue);
+ }
+
+ // Special Vaadin types
+
+ String stringValue = String.valueOf(encodedJsonValue);
+
+ if (JsonEncoder.VTYPE_CONNECTOR.equals(transportType)) {
+ return connectorTracker.getConnector(stringValue);
+ }
+
+ // Legacy types
+
+ if (JsonEncoder.VTYPE_STRING.equals(transportType)) {
+ return stringValue;
+ } else if (JsonEncoder.VTYPE_INTEGER.equals(transportType)) {
+ return Integer.valueOf(stringValue);
+ } else if (JsonEncoder.VTYPE_LONG.equals(transportType)) {
+ return Long.valueOf(stringValue);
+ } else if (JsonEncoder.VTYPE_FLOAT.equals(transportType)) {
+ return Float.valueOf(stringValue);
+ } else if (JsonEncoder.VTYPE_DOUBLE.equals(transportType)) {
+ return Double.valueOf(stringValue);
+ } else if (JsonEncoder.VTYPE_BOOLEAN.equals(transportType)) {
+ return Boolean.valueOf(stringValue);
+ }
+
+ throw new JSONException("Unknown type " + transportType);
+ }
+
+ private static UidlValue decodeUidlValue(JSONArray encodedJsonValue,
+ ConnectorTracker connectorTracker) throws JSONException {
+ String type = encodedJsonValue.getString(0);
+
+ Object decodedValue = decodeInternalType(getType(type), true,
+ encodedJsonValue.get(1), connectorTracker);
+ return new UidlValue(decodedValue);
+ }
+
+ private static boolean transportTypesCompatible(
+ String encodedTransportType, String transportType) {
+ if (encodedTransportType == null) {
+ return false;
+ }
+ if (encodedTransportType.equals(transportType)) {
+ return true;
+ }
+ if (encodedTransportType.equals(JsonEncoder.VTYPE_NULL)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Map<Object, Object> decodeMap(Type targetType,
+ boolean restrictToInternalTypes, Object jsonMap,
+ ConnectorTracker connectorTracker) throws JSONException {
+ if (jsonMap instanceof JSONArray) {
+ // Client-side has no declared type information to determine
+ // encoding method for empty maps, so these are handled separately.
+ // See #8906.
+ JSONArray jsonArray = (JSONArray) jsonMap;
+ if (jsonArray.length() == 0) {
+ return new HashMap<Object, Object>();
+ }
+ }
+
+ if (!restrictToInternalTypes && targetType instanceof ParameterizedType) {
+ Type keyType = ((ParameterizedType) targetType)
+ .getActualTypeArguments()[0];
+ Type valueType = ((ParameterizedType) targetType)
+ .getActualTypeArguments()[1];
+ if (keyType == String.class) {
+ return decodeStringMap(valueType, (JSONObject) jsonMap,
+ connectorTracker);
+ } else if (keyType == Connector.class) {
+ return decodeConnectorMap(valueType, (JSONObject) jsonMap,
+ connectorTracker);
+ } else {
+ return decodeObjectMap(keyType, valueType, (JSONArray) jsonMap,
+ connectorTracker);
+ }
+ } else {
+ return decodeStringMap(UidlValue.class, (JSONObject) jsonMap,
+ connectorTracker);
+ }
+ }
+
+ private static Map<Object, Object> decodeObjectMap(Type keyType,
+ Type valueType, JSONArray jsonMap, ConnectorTracker connectorTracker)
+ throws JSONException {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ JSONArray keys = jsonMap.getJSONArray(0);
+ JSONArray values = jsonMap.getJSONArray(1);
+
+ assert (keys.length() == values.length());
+
+ for (int i = 0; i < keys.length(); i++) {
+ Object key = decodeInternalOrCustomType(keyType, keys.get(i),
+ connectorTracker);
+ Object value = decodeInternalOrCustomType(valueType, values.get(i),
+ connectorTracker);
+
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ private static Map<Object, Object> decodeConnectorMap(Type valueType,
+ JSONObject jsonMap, ConnectorTracker connectorTracker)
+ throws JSONException {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) {
+ String key = (String) iter.next();
+ Object value = decodeInternalOrCustomType(valueType,
+ jsonMap.get(key), connectorTracker);
+ if (valueType == UidlValue.class) {
+ value = ((UidlValue) value).getValue();
+ }
+ map.put(connectorTracker.getConnector(key), value);
+ }
+
+ return map;
+ }
+
+ private static Map<Object, Object> decodeStringMap(Type valueType,
+ JSONObject jsonMap, ConnectorTracker connectorTracker)
+ throws JSONException {
+ Map<Object, Object> map = new HashMap<Object, Object>();
+
+ for (Iterator<?> iter = jsonMap.keys(); iter.hasNext();) {
+ String key = (String) iter.next();
+ Object value = decodeInternalOrCustomType(valueType,
+ jsonMap.get(key), connectorTracker);
+ if (valueType == UidlValue.class) {
+ value = ((UidlValue) value).getValue();
+ }
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ /**
+ * @param targetType
+ * @param restrictToInternalTypes
+ * @param typeIndex
+ * The index of a generic type to use to define the child type
+ * that should be decoded
+ * @param encodedValueAndType
+ * @param application
+ * @return
+ * @throws JSONException
+ */
+ private static Object decodeParametrizedType(Type targetType,
+ boolean restrictToInternalTypes, int typeIndex, Object value,
+ ConnectorTracker connectorTracker) throws JSONException {
+ if (!restrictToInternalTypes && targetType instanceof ParameterizedType) {
+ Type childType = ((ParameterizedType) targetType)
+ .getActualTypeArguments()[typeIndex];
+ // Only decode the given type
+ return decodeInternalOrCustomType(childType, value,
+ connectorTracker);
+ } else {
+ // Only UidlValue when not enforcing a given type to avoid security
+ // issues
+ UidlValue decodeInternalType = (UidlValue) decodeInternalType(
+ UidlValue.class, true, value, connectorTracker);
+ return decodeInternalType.getValue();
+ }
+ }
+
+ private static Object decodeEnum(Class<? extends Enum> cls, JSONObject value) {
+ String enumIdentifier = String.valueOf(value);
+ return Enum.valueOf(cls, enumIdentifier);
+ }
+
+ private static String[] decodeStringArray(JSONArray jsonArray)
+ throws JSONException {
+ int length = jsonArray.length();
+ List<String> tokens = new ArrayList<String>(length);
+ for (int i = 0; i < length; ++i) {
+ tokens.add(jsonArray.getString(i));
+ }
+ return tokens.toArray(new String[tokens.size()]);
+ }
+
+ private static Object[] decodeObjectArray(Type targetType,
+ JSONArray jsonArray, ConnectorTracker connectorTracker)
+ throws JSONException {
+ List list = decodeList(List.class, true, jsonArray, connectorTracker);
+ return list.toArray(new Object[list.size()]);
+ }
+
+ private static List<Object> decodeList(Type targetType,
+ boolean restrictToInternalTypes, JSONArray jsonArray,
+ ConnectorTracker connectorTracker) throws JSONException {
+ List<Object> list = new ArrayList<Object>();
+ for (int i = 0; i < jsonArray.length(); ++i) {
+ // each entry always has two elements: type and value
+ Object encodedValue = jsonArray.get(i);
+ Object decodedChild = decodeParametrizedType(targetType,
+ restrictToInternalTypes, 0, encodedValue, connectorTracker);
+ list.add(decodedChild);
+ }
+ return list;
+ }
+
+ private static Set<Object> decodeSet(Type targetType,
+ boolean restrictToInternalTypes, JSONArray jsonArray,
+ ConnectorTracker connectorTracker) throws JSONException {
+ HashSet<Object> set = new HashSet<Object>();
+ set.addAll(decodeList(targetType, restrictToInternalTypes, jsonArray,
+ connectorTracker));
+ return set;
+ }
+
+ /**
+ * Returns the name that should be used as field name in the JSON. We strip
+ * "set" from the setter, keeping the result - this is easy to do on both
+ * server and client, avoiding some issues with cASE. E.g setZIndex()
+ * becomes "zIndex". Also ensures that both getter and setter are present,
+ * returning null otherwise.
+ *
+ * @param pd
+ * @return the name to be used or null if both getter and setter are not
+ * found.
+ */
+ static String getTransportFieldName(PropertyDescriptor pd) {
+ if (pd.getReadMethod() == null || pd.getWriteMethod() == null) {
+ return null;
+ }
+ String fieldName = pd.getWriteMethod().getName().substring(3);
+ fieldName = Character.toLowerCase(fieldName.charAt(0))
+ + fieldName.substring(1);
+ return fieldName;
+ }
+
+ private static Object decodeObject(Type targetType,
+ JSONObject serializedObject, ConnectorTracker connectorTracker)
+ throws JSONException {
+
+ Class<?> targetClass = getClassForType(targetType);
+ if (Enum.class.isAssignableFrom(targetClass)) {
+ return decodeEnum(targetClass.asSubclass(Enum.class),
+ serializedObject);
+ }
+
+ try {
+ Object decodedObject = targetClass.newInstance();
+ for (PropertyDescriptor pd : Introspector.getBeanInfo(targetClass)
+ .getPropertyDescriptors()) {
+
+ String fieldName = getTransportFieldName(pd);
+ if (fieldName == null) {
+ continue;
+ }
+ Object encodedFieldValue = serializedObject.get(fieldName);
+ Type fieldType = pd.getReadMethod().getGenericReturnType();
+ Object decodedFieldValue = decodeInternalOrCustomType(
+ fieldType, encodedFieldValue, connectorTracker);
+
+ pd.getWriteMethod().invoke(decodedObject, decodedFieldValue);
+ }
+
+ return decodedObject;
+ } catch (IllegalArgumentException e) {
+ throw new JSONException(e);
+ } catch (IllegalAccessException e) {
+ throw new JSONException(e);
+ } catch (InvocationTargetException e) {
+ throw new JSONException(e);
+ } catch (InstantiationException e) {
+ throw new JSONException(e);
+ } catch (IntrospectionException e) {
+ throw new JSONException(e);
+ }
+ }
+
+ public static Object encode(Object value, Object referenceValue,
+ Type valueType, ConnectorTracker connectorTracker)
+ throws JSONException {
+
+ if (valueType == null) {
+ throw new IllegalArgumentException("type must be defined");
+ }
+
+ if (valueType instanceof WildcardType) {
+ throw new IllegalStateException(
+ "Can not serialize type with wildcard: " + valueType);
+ }
+
+ if (null == value) {
+ return encodeNull();
+ }
+
+ if (value instanceof String[]) {
+ String[] array = (String[]) value;
+ JSONArray jsonArray = new JSONArray();
+ for (int i = 0; i < array.length; ++i) {
+ jsonArray.put(array[i]);
+ }
+ return jsonArray;
+ } else if (value instanceof String) {
+ return value;
+ } else if (value instanceof Boolean) {
+ return value;
+ } else if (value instanceof Number) {
+ return value;
+ } else if (value instanceof Character) {
+ // Character is not a Number
+ return value;
+ } else if (value instanceof Collection) {
+ Collection<?> collection = (Collection<?>) value;
+ JSONArray jsonArray = encodeCollection(valueType, collection,
+ connectorTracker);
+ return jsonArray;
+ } else if (valueType instanceof Class<?>
+ && ((Class<?>) valueType).isArray()) {
+ JSONArray jsonArray = encodeArrayContents(
+ ((Class<?>) valueType).getComponentType(), value,
+ connectorTracker);
+ return jsonArray;
+ } else if (valueType instanceof GenericArrayType) {
+ Type componentType = ((GenericArrayType) valueType)
+ .getGenericComponentType();
+ JSONArray jsonArray = encodeArrayContents(componentType, value,
+ connectorTracker);
+ return jsonArray;
+ } else if (value instanceof Map) {
+ Object jsonMap = encodeMap(valueType, (Map<?, ?>) value,
+ connectorTracker);
+ return jsonMap;
+ } else if (value instanceof Connector) {
+ Connector connector = (Connector) value;
+ if (value instanceof Component
+ && !(AbstractCommunicationManager
+ .isVisible((Component) value))) {
+ return encodeNull();
+ }
+ return connector.getConnectorId();
+ } else if (value instanceof Enum) {
+ return encodeEnum((Enum<?>) value, connectorTracker);
+ } else if (value instanceof JSONArray || value instanceof JSONObject) {
+ return value;
+ } else {
+ // Any object that we do not know how to encode we encode by looping
+ // through fields
+ return encodeObject(value, referenceValue, connectorTracker);
+ }
+ }
+
+ private static Object encodeNull() {
+ return JSONObject.NULL;
+ }
+
+ private static Object encodeObject(Object value, Object referenceValue,
+ ConnectorTracker connectorTracker) throws JSONException {
+ JSONObject jsonMap = new JSONObject();
+
+ try {
+ for (PropertyDescriptor pd : Introspector.getBeanInfo(
+ value.getClass()).getPropertyDescriptors()) {
+ String fieldName = getTransportFieldName(pd);
+ if (fieldName == null) {
+ continue;
+ }
+ Method getterMethod = pd.getReadMethod();
+ // We can't use PropertyDescriptor.getPropertyType() as it does
+ // not support generics
+ Type fieldType = getterMethod.getGenericReturnType();
+ Object fieldValue = getterMethod.invoke(value, (Object[]) null);
+ boolean equals = false;
+ Object referenceFieldValue = null;
+ if (referenceValue != null) {
+ referenceFieldValue = getterMethod.invoke(referenceValue,
+ (Object[]) null);
+ equals = equals(fieldValue, referenceFieldValue);
+ }
+ if (!equals) {
+ if (jsonMap.has(fieldName)) {
+ throw new RuntimeException(
+ "Can't encode "
+ + value.getClass().getName()
+ + " as it has multiple fields with the name "
+ + fieldName.toLowerCase()
+ + ". This can happen if only casing distinguishes one property name from another.");
+ }
+ jsonMap.put(
+ fieldName,
+ encode(fieldValue, referenceFieldValue, fieldType,
+ connectorTracker));
+ // } else {
+ // System.out.println("Skipping field " + fieldName
+ // + " of type " + fieldType.getName()
+ // + " for object " + value.getClass().getName()
+ // + " as " + fieldValue + "==" + referenceFieldValue);
+ }
+ }
+ } catch (Exception e) {
+ // TODO: Should exceptions be handled in a different way?
+ throw new JSONException(e);
+ }
+ return jsonMap;
+ }
+
+ /**
+ * Compares the value with the reference. If they match, returns true.
+ *
+ * @param fieldValue
+ * @param referenceValue
+ * @return
+ */
+ private static boolean equals(Object fieldValue, Object referenceValue) {
+ if (fieldValue == null) {
+ return referenceValue == null;
+ }
+
+ if (fieldValue.equals(referenceValue)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static String encodeEnum(Enum<?> e,
+ ConnectorTracker connectorTracker) throws JSONException {
+ return e.name();
+ }
+
+ private static JSONArray encodeArrayContents(Type componentType,
+ Object array, ConnectorTracker connectorTracker)
+ throws JSONException {
+ JSONArray jsonArray = new JSONArray();
+ for (int i = 0; i < Array.getLength(array); i++) {
+ jsonArray.put(encode(Array.get(array, i), null, componentType,
+ connectorTracker));
+ }
+ return jsonArray;
+ }
+
+ private static JSONArray encodeCollection(Type targetType,
+ Collection collection, ConnectorTracker connectorTracker)
+ throws JSONException {
+ JSONArray jsonArray = new JSONArray();
+ for (Object o : collection) {
+ jsonArray.put(encodeChild(targetType, 0, o, connectorTracker));
+ }
+ return jsonArray;
+ }
+
+ private static Object encodeChild(Type targetType, int typeIndex, Object o,
+ ConnectorTracker connectorTracker) throws JSONException {
+ if (targetType instanceof ParameterizedType) {
+ Type childType = ((ParameterizedType) targetType)
+ .getActualTypeArguments()[typeIndex];
+ // Encode using the given type
+ return encode(o, null, childType, connectorTracker);
+ } else {
+ throw new JSONException("Collection is missing generics");
+ }
+ }
+
+ private static Object encodeMap(Type mapType, Map<?, ?> map,
+ ConnectorTracker connectorTracker) throws JSONException {
+ Type keyType, valueType;
+
+ if (mapType instanceof ParameterizedType) {
+ keyType = ((ParameterizedType) mapType).getActualTypeArguments()[0];
+ valueType = ((ParameterizedType) mapType).getActualTypeArguments()[1];
+ } else {
+ throw new JSONException("Map is missing generics");
+ }
+
+ if (map.isEmpty()) {
+ // Client -> server encodes empty map as an empty array because of
+ // #8906. Do the same for server -> client to maintain symmetry.
+ return new JSONArray();
+ }
+
+ if (keyType == String.class) {
+ return encodeStringMap(valueType, map, connectorTracker);
+ } else if (keyType == Connector.class) {
+ return encodeConnectorMap(valueType, map, connectorTracker);
+ } else {
+ return encodeObjectMap(keyType, valueType, map, connectorTracker);
+ }
+ }
+
+ private static JSONArray encodeObjectMap(Type keyType, Type valueType,
+ Map<?, ?> map, ConnectorTracker connectorTracker)
+ throws JSONException {
+ JSONArray keys = new JSONArray();
+ JSONArray values = new JSONArray();
+
+ for (Entry<?, ?> entry : map.entrySet()) {
+ Object encodedKey = encode(entry.getKey(), null, keyType,
+ connectorTracker);
+ Object encodedValue = encode(entry.getValue(), null, valueType,
+ connectorTracker);
+
+ keys.put(encodedKey);
+ values.put(encodedValue);
+ }
+
+ return new JSONArray(Arrays.asList(keys, values));
+ }
+
+ private static JSONObject encodeConnectorMap(Type valueType, Map<?, ?> map,
+ ConnectorTracker connectorTracker) throws JSONException {
+ JSONObject jsonMap = new JSONObject();
+
+ for (Entry<?, ?> entry : map.entrySet()) {
+ Connector key = (Connector) entry.getKey();
+ Object encodedValue = encode(entry.getValue(), null, valueType,
+ connectorTracker);
+ jsonMap.put(key.getConnectorId(), encodedValue);
+ }
+
+ return jsonMap;
+ }
+
+ private static JSONObject encodeStringMap(Type valueType, Map<?, ?> map,
+ ConnectorTracker connectorTracker) throws JSONException {
+ JSONObject jsonMap = new JSONObject();
+
+ for (Entry<?, ?> entry : map.entrySet()) {
+ String key = (String) entry.getKey();
+ Object encodedValue = encode(entry.getValue(), null, valueType,
+ connectorTracker);
+ jsonMap.put(key, encodedValue);
+ }
+
+ return jsonMap;
+ }
+
+ /**
+ * Gets the transport type for the given class. Returns null if no transport
+ * type can be found.
+ *
+ * @param valueType
+ * The type that should be transported
+ * @return
+ * @throws JSONException
+ */
+ private static String getInternalTransportType(Type valueType) {
+ return typeToTransportType.get(getClassForType(valueType));
+ }
+
+ private static String getCustomTransportType(Class<?> targetType) {
+ return targetType.getName();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java
new file mode 100644
index 0000000000..5a830ddb63
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java
@@ -0,0 +1,1022 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.Vector;
+import java.util.logging.Logger;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.StreamVariable;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.ui.Alignment;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CustomLayout;
+
+/**
+ * User Interface Description Language Target.
+ *
+ * TODO document better: role of this class, UIDL format, attributes, variables,
+ * etc.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class JsonPaintTarget implements PaintTarget {
+
+ /* Document type declarations */
+
+ private final static String UIDL_ARG_NAME = "name";
+
+ private final Stack<String> mOpenTags;
+
+ private final Stack<JsonTag> openJsonTags;
+
+ // these match each other element-wise
+ private final Stack<ClientConnector> openPaintables;
+ private final Stack<String> openPaintableTags;
+
+ private final PrintWriter uidlBuffer;
+
+ private boolean closed = false;
+
+ private final AbstractCommunicationManager manager;
+
+ private int changes = 0;
+
+ private final Set<Object> usedResources = new HashSet<Object>();
+
+ private boolean customLayoutArgumentsOpen = false;
+
+ private JsonTag tag;
+
+ private boolean cacheEnabled = false;
+
+ private final Set<Class<? extends ClientConnector>> usedClientConnectors = new HashSet<Class<? extends ClientConnector>>();
+
+ /**
+ * Creates a new JsonPaintTarget.
+ *
+ * @param manager
+ * @param outWriter
+ * A character-output stream.
+ * @param cachingRequired
+ * true if this is not a full repaint, i.e. caches are to be
+ * used.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public JsonPaintTarget(AbstractCommunicationManager manager,
+ PrintWriter outWriter, boolean cachingRequired)
+ throws PaintException {
+
+ this.manager = manager;
+
+ // Sets the target for UIDL writing
+ uidlBuffer = outWriter;
+
+ // Initialize tag-writing
+ mOpenTags = new Stack<String>();
+ openJsonTags = new Stack<JsonTag>();
+
+ openPaintables = new Stack<ClientConnector>();
+ openPaintableTags = new Stack<String>();
+
+ cacheEnabled = cachingRequired;
+ }
+
+ @Override
+ public void startTag(String tagName) throws PaintException {
+ startTag(tagName, false);
+ }
+
+ /**
+ * Prints the element start tag.
+ *
+ * <pre>
+ * Todo:
+ * Checking of input values
+ *
+ * </pre>
+ *
+ * @param tagName
+ * the name of the start tag.
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ */
+ public void startTag(String tagName, boolean isChildNode)
+ throws PaintException {
+ // In case of null data output nothing:
+ if (tagName == null) {
+ throw new NullPointerException();
+ }
+
+ // Ensures that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ if (tag != null) {
+ openJsonTags.push(tag);
+ }
+ // Checks tagName and attributes here
+ mOpenTags.push(tagName);
+
+ tag = new JsonTag(tagName);
+
+ customLayoutArgumentsOpen = false;
+
+ }
+
+ /**
+ * Prints the element end tag.
+ *
+ * If the parent tag is closed before every child tag is closed an
+ * PaintException is raised.
+ *
+ * @param tag
+ * the name of the end tag.
+ * @throws Paintexception
+ * if the paint operation failed.
+ */
+
+ @Override
+ public void endTag(String tagName) throws PaintException {
+ // In case of null data output nothing:
+ if (tagName == null) {
+ throw new NullPointerException();
+ }
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ if (openJsonTags.size() > 0) {
+ final JsonTag parent = openJsonTags.pop();
+
+ String lastTag = "";
+
+ lastTag = mOpenTags.pop();
+ if (!tagName.equalsIgnoreCase(lastTag)) {
+ throw new PaintException("Invalid UIDL: wrong ending tag: '"
+ + tagName + "' expected: '" + lastTag + "'.");
+ }
+
+ parent.addData(tag.getJSON());
+
+ tag = parent;
+ } else {
+ changes++;
+ uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON());
+ tag = null;
+ }
+ }
+
+ /**
+ * Substitutes the XML sensitive characters with predefined XML entities.
+ *
+ * @param xml
+ * the String to be substituted.
+ * @return A new string instance where all occurrences of XML sensitive
+ * characters are substituted with entities.
+ */
+ static public String escapeXML(String xml) {
+ if (xml == null || xml.length() <= 0) {
+ return "";
+ }
+ return escapeXML(new StringBuilder(xml)).toString();
+ }
+
+ /**
+ * Substitutes the XML sensitive characters with predefined XML entities.
+ *
+ * @param xml
+ * the String to be substituted.
+ * @return A new StringBuilder instance where all occurrences of XML
+ * sensitive characters are substituted with entities.
+ *
+ */
+ static StringBuilder escapeXML(StringBuilder xml) {
+ if (xml == null || xml.length() <= 0) {
+ return new StringBuilder("");
+ }
+
+ final StringBuilder result = new StringBuilder(xml.length() * 2);
+
+ for (int i = 0; i < xml.length(); i++) {
+ final char c = xml.charAt(i);
+ final String s = toXmlChar(c);
+ if (s != null) {
+ result.append(s);
+ } else {
+ result.append(c);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Escapes the given string so it can safely be used as a JSON string.
+ *
+ * @param s
+ * The string to escape
+ * @return Escaped version of the string
+ */
+ static public String escapeJSON(String s) {
+ // FIXME: Move this method to another class as other classes use it
+ // also.
+ if (s == null) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ final char ch = s.charAt(i);
+ switch (ch) {
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '/':
+ sb.append("\\/");
+ break;
+ default:
+ if (ch >= '\u0000' && ch <= '\u001F') {
+ final String ss = Integer.toHexString(ch);
+ sb.append("\\u");
+ for (int k = 0; k < 4 - ss.length(); k++) {
+ sb.append('0');
+ }
+ sb.append(ss.toUpperCase());
+ } else {
+ sb.append(ch);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Substitutes a XML sensitive character with predefined XML entity.
+ *
+ * @param c
+ * the Character to be replaced with an entity.
+ * @return String of the entity or null if character is not to be replaced
+ * with an entity.
+ */
+ private static String toXmlChar(char c) {
+ switch (c) {
+ case '&':
+ return "&amp;"; // & => &amp;
+ case '>':
+ return "&gt;"; // > => &gt;
+ case '<':
+ return "&lt;"; // < => &lt;
+ case '"':
+ return "&quot;"; // " => &quot;
+ case '\'':
+ return "&apos;"; // ' => &apos;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Prints XML-escaped text.
+ *
+ * @param str
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ */
+
+ @Override
+ public void addText(String str) throws PaintException {
+ tag.addData("\"" + escapeJSON(str) + "\"");
+ }
+
+ @Override
+ public void addAttribute(String name, boolean value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false"));
+ }
+
+ @Override
+ public void addAttribute(String name, Resource value) throws PaintException {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ ResourceReference reference = ResourceReference.create(value);
+ addAttribute(name, reference.getURL());
+ }
+
+ @Override
+ public void addAttribute(String name, int value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ @Override
+ public void addAttribute(String name, long value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ @Override
+ public void addAttribute(String name, float value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ @Override
+ public void addAttribute(String name, double value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ @Override
+ public void addAttribute(String name, String value) throws PaintException {
+ // In case of null data output nothing:
+ if ((value == null) || (name == null)) {
+ throw new NullPointerException(
+ "Parameters must be non-null strings");
+ }
+
+ tag.addAttribute("\"" + name + "\":\"" + escapeJSON(value) + "\"");
+
+ if (customLayoutArgumentsOpen && "template".equals(name)) {
+ getUsedResources().add("layouts/" + value + ".html");
+ }
+
+ if (name.equals("locale")) {
+ manager.requireLocale(value);
+ }
+
+ }
+
+ @Override
+ public void addAttribute(String name, Component value)
+ throws PaintException {
+ final String id = value.getConnectorId();
+ addAttribute(name, id);
+ }
+
+ @Override
+ public void addAttribute(String name, Map<?, ?> value)
+ throws PaintException {
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("\"");
+ sb.append(name);
+ sb.append("\":");
+ sb.append("{");
+ for (Iterator<?> it = value.keySet().iterator(); it.hasNext();) {
+ Object key = it.next();
+ Object mapValue = value.get(key);
+ sb.append("\"");
+ if (key instanceof ClientConnector) {
+ sb.append(((ClientConnector) key).getConnectorId());
+ } else {
+ sb.append(escapeJSON(key.toString()));
+ }
+ sb.append("\":");
+ if (mapValue instanceof Float || mapValue instanceof Integer
+ || mapValue instanceof Double
+ || mapValue instanceof Boolean
+ || mapValue instanceof Alignment) {
+ sb.append(mapValue);
+ } else {
+ sb.append("\"");
+ sb.append(escapeJSON(mapValue.toString()));
+ sb.append("\"");
+ }
+ if (it.hasNext()) {
+ sb.append(",");
+ }
+ }
+ sb.append("}");
+
+ tag.addAttribute(sb.toString());
+ }
+
+ @Override
+ public void addAttribute(String name, Object[] values) {
+ // In case of null data output nothing:
+ if ((values == null) || (name == null)) {
+ throw new NullPointerException(
+ "Parameters must be non-null strings");
+ }
+ final StringBuilder buf = new StringBuilder();
+ buf.append("\"" + name + "\":[");
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ buf.append(",");
+ }
+ buf.append("\"");
+ buf.append(escapeJSON(values[i].toString()));
+ buf.append("\"");
+ }
+ buf.append("]");
+ tag.addAttribute(buf.toString());
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, String value)
+ throws PaintException {
+ tag.addVariable(new StringVariable(owner, name, escapeJSON(value)));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, Component value)
+ throws PaintException {
+ tag.addVariable(new StringVariable(owner, name, value.getConnectorId()));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, int value)
+ throws PaintException {
+ tag.addVariable(new IntVariable(owner, name, value));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, long value)
+ throws PaintException {
+ tag.addVariable(new LongVariable(owner, name, value));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, float value)
+ throws PaintException {
+ tag.addVariable(new FloatVariable(owner, name, value));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, double value)
+ throws PaintException {
+ tag.addVariable(new DoubleVariable(owner, name, value));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, boolean value)
+ throws PaintException {
+ tag.addVariable(new BooleanVariable(owner, name, value));
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name, String[] value)
+ throws PaintException {
+ tag.addVariable(new ArrayVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a upload stream type variable.
+ *
+ * TODO not converted for JSON
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+
+ @Override
+ public void addUploadStreamVariable(VariableOwner owner, String name)
+ throws PaintException {
+ startTag("uploadstream");
+ addAttribute(UIDL_ARG_NAME, name);
+ endTag("uploadstream");
+ }
+
+ /**
+ * Prints the single text section.
+ *
+ * Prints full text section. The section data is escaped
+ *
+ * @param sectionTagName
+ * the name of the tag.
+ * @param sectionData
+ * the section data to be printed.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+
+ @Override
+ public void addSection(String sectionTagName, String sectionData)
+ throws PaintException {
+ tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData)
+ + "\"}");
+ }
+
+ /**
+ * Adds XML directly to UIDL.
+ *
+ * @param xml
+ * the Xml to be added.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+
+ @Override
+ public void addUIDL(String xml) throws PaintException {
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ // Make sure that the open start tag is closed before
+ // anything is written.
+
+ // Escape and write what was given
+ if (xml != null) {
+ tag.addData("\"" + escapeJSON(xml) + "\"");
+ }
+
+ }
+
+ /**
+ * Adds XML section with namespace.
+ *
+ * @param sectionTagName
+ * the name of the tag.
+ * @param sectionData
+ * the section data.
+ * @param namespace
+ * the namespace to be added.
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ * @see com.vaadin.terminal.PaintTarget#addXMLSection(String, String,
+ * String)
+ */
+
+ @Override
+ public void addXMLSection(String sectionTagName, String sectionData,
+ String namespace) throws PaintException {
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ startTag(sectionTagName);
+ if (namespace != null) {
+ addAttribute("xmlns", namespace);
+ }
+
+ if (sectionData != null) {
+ tag.addData("\"" + escapeJSON(sectionData) + "\"");
+ }
+ endTag(sectionTagName);
+ }
+
+ /**
+ * Gets the UIDL already printed to stream. Paint target must be closed
+ * before the <code>getUIDL</code> can be called.
+ *
+ * @return the UIDL.
+ */
+ public String getUIDL() {
+ if (closed) {
+ return uidlBuffer.toString();
+ }
+ throw new IllegalStateException(
+ "Tried to read UIDL from open PaintTarget");
+ }
+
+ /**
+ * Closes the paint target. Paint target must be closed before the
+ * <code>getUIDL</code> can be called. Subsequent attempts to write to paint
+ * target. If the target was already closed, call to this function is
+ * ignored. will generate an exception.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void close() throws PaintException {
+ if (tag != null) {
+ uidlBuffer.write(tag.getJSON());
+ }
+ flush();
+ closed = true;
+ }
+
+ /**
+ * Method flush.
+ */
+ private void flush() {
+ uidlBuffer.flush();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.PaintTarget#startPaintable(com.vaadin.terminal
+ * .Paintable, java.lang.String)
+ */
+
+ @Override
+ public PaintStatus startPaintable(Component connector, String tagName)
+ throws PaintException {
+ boolean topLevelPaintable = openPaintables.isEmpty();
+
+ getLogger().fine(
+ "startPaintable for " + connector.getClass().getName() + "@"
+ + Integer.toHexString(connector.hashCode()));
+ startTag(tagName, true);
+
+ openPaintables.push(connector);
+ openPaintableTags.push(tagName);
+
+ addAttribute("id", connector.getConnectorId());
+
+ // Only paint top level paintables. All sub paintables are marked as
+ // queued and painted separately later.
+ if (!topLevelPaintable) {
+ return PaintStatus.CACHED;
+ }
+
+ if (connector instanceof CustomLayout) {
+ customLayoutArgumentsOpen = true;
+ }
+ return PaintStatus.PAINTING;
+ }
+
+ @Override
+ public void endPaintable(Component paintable) throws PaintException {
+ getLogger().fine(
+ "endPaintable for " + paintable.getClass().getName() + "@"
+ + Integer.toHexString(paintable.hashCode()));
+
+ ClientConnector openPaintable = openPaintables.peek();
+ if (paintable != openPaintable) {
+ throw new PaintException("Invalid UIDL: closing wrong paintable: '"
+ + paintable.getConnectorId() + "' expected: '"
+ + openPaintable.getConnectorId() + "'.");
+ }
+ // remove paintable from the stack
+ openPaintables.pop();
+ String openTag = openPaintableTags.pop();
+ endTag(openTag);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.PaintTarget#addCharacterData(java.lang.String )
+ */
+
+ @Override
+ public void addCharacterData(String text) throws PaintException {
+ if (text != null) {
+ tag.addData(text);
+ }
+ }
+
+ /**
+ * This is basically a container for UI components variables, that will be
+ * added at the end of JSON object.
+ *
+ * @author mattitahvonen
+ *
+ */
+ class JsonTag implements Serializable {
+ boolean firstField = false;
+
+ Vector<Object> variables = new Vector<Object>();
+
+ Vector<Object> children = new Vector<Object>();
+
+ Vector<Object> attr = new Vector<Object>();
+
+ StringBuilder data = new StringBuilder();
+
+ public boolean childrenArrayOpen = false;
+
+ private boolean childNode = false;
+
+ private boolean tagClosed = false;
+
+ public JsonTag(String tagName) {
+ data.append("[\"" + tagName + "\"");
+ }
+
+ private void closeTag() {
+ if (!tagClosed) {
+ data.append(attributesAsJsonObject());
+ data.append(getData());
+ // Writes the end (closing) tag
+ data.append("]");
+ tagClosed = true;
+ }
+ }
+
+ public String getJSON() {
+ if (!tagClosed) {
+ closeTag();
+ }
+ return data.toString();
+ }
+
+ public void openChildrenArray() {
+ if (!childrenArrayOpen) {
+ // append("c : [");
+ childrenArrayOpen = true;
+ // firstField = true;
+ }
+ }
+
+ public void closeChildrenArray() {
+ // append("]");
+ // firstField = false;
+ }
+
+ public void setChildNode(boolean b) {
+ childNode = b;
+ }
+
+ public boolean isChildNode() {
+ return childNode;
+ }
+
+ public String startField() {
+ if (firstField) {
+ firstField = false;
+ return "";
+ } else {
+ return ",";
+ }
+ }
+
+ /**
+ *
+ * @param s
+ * json string, object or array
+ */
+ public void addData(String s) {
+ children.add(s);
+ }
+
+ public String getData() {
+ final StringBuilder buf = new StringBuilder();
+ final Iterator<Object> it = children.iterator();
+ while (it.hasNext()) {
+ buf.append(startField());
+ buf.append(it.next());
+ }
+ return buf.toString();
+ }
+
+ public void addAttribute(String jsonNode) {
+ attr.add(jsonNode);
+ }
+
+ private String attributesAsJsonObject() {
+ final StringBuilder buf = new StringBuilder();
+ buf.append(startField());
+ buf.append("{");
+ for (final Iterator<Object> iter = attr.iterator(); iter.hasNext();) {
+ final String element = (String) iter.next();
+ buf.append(element);
+ if (iter.hasNext()) {
+ buf.append(",");
+ }
+ }
+ buf.append(tag.variablesAsJsonObject());
+ buf.append("}");
+ return buf.toString();
+ }
+
+ public void addVariable(Variable v) {
+ variables.add(v);
+ }
+
+ private String variablesAsJsonObject() {
+ if (variables.size() == 0) {
+ return "";
+ }
+ final StringBuilder buf = new StringBuilder();
+ buf.append(startField());
+ buf.append("\"v\":{");
+ final Iterator<Object> iter = variables.iterator();
+ while (iter.hasNext()) {
+ final Variable element = (Variable) iter.next();
+ buf.append(element.getJsonPresentation());
+ if (iter.hasNext()) {
+ buf.append(",");
+ }
+ }
+ buf.append("}");
+ return buf.toString();
+ }
+ }
+
+ abstract class Variable implements Serializable {
+
+ String name;
+
+ public abstract String getJsonPresentation();
+ }
+
+ class BooleanVariable extends Variable implements Serializable {
+ boolean value;
+
+ public BooleanVariable(VariableOwner owner, String name, boolean v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + (value == true ? "true" : "false");
+ }
+
+ }
+
+ class StringVariable extends Variable implements Serializable {
+ String value;
+
+ public StringVariable(VariableOwner owner, String name, String v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":\"" + value + "\"";
+ }
+
+ }
+
+ class IntVariable extends Variable implements Serializable {
+ int value;
+
+ public IntVariable(VariableOwner owner, String name, int v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class LongVariable extends Variable implements Serializable {
+ long value;
+
+ public LongVariable(VariableOwner owner, String name, long v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class FloatVariable extends Variable implements Serializable {
+ float value;
+
+ public FloatVariable(VariableOwner owner, String name, float v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class DoubleVariable extends Variable implements Serializable {
+ double value;
+
+ public DoubleVariable(VariableOwner owner, String name, double v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class ArrayVariable extends Variable implements Serializable {
+ String[] value;
+
+ public ArrayVariable(VariableOwner owner, String name, String[] v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\"");
+ sb.append(name);
+ sb.append("\":[");
+ for (int i = 0; i < value.length;) {
+ sb.append("\"");
+ sb.append(escapeJSON(value[i]));
+ sb.append("\"");
+ i++;
+ if (i < value.length) {
+ sb.append(",");
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+
+ public Set<Object> getUsedResources() {
+ return usedResources;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public String getTag(ClientConnector clientConnector) {
+ Class<? extends ClientConnector> clientConnectorClass = clientConnector
+ .getClass();
+ while (clientConnectorClass.isAnonymousClass()) {
+ clientConnectorClass = (Class<? extends ClientConnector>) clientConnectorClass
+ .getSuperclass();
+ }
+ Class<?> clazz = clientConnectorClass;
+ while (!usedClientConnectors.contains(clazz)
+ && clazz.getSuperclass() != null
+ && ClientConnector.class.isAssignableFrom(clazz)) {
+ usedClientConnectors.add((Class<? extends ClientConnector>) clazz);
+ clazz = clazz.getSuperclass();
+ }
+ return manager.getTagForType(clientConnectorClass);
+ }
+
+ Collection<Class<? extends ClientConnector>> getUsedClientConnectors() {
+ return usedClientConnectors;
+ }
+
+ @Override
+ public void addVariable(VariableOwner owner, String name,
+ StreamVariable value) throws PaintException {
+ String url = manager.getStreamVariableTargetUrl(
+ (ClientConnector) owner, name, value);
+ if (url != null) {
+ addVariable(owner, name, url);
+ } // else { //NOP this was just a cleanup by component }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.PaintTarget#isFullRepaint()
+ */
+
+ @Override
+ public boolean isFullRepaint() {
+ return !cacheEnabled;
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(JsonPaintTarget.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java
new file mode 100644
index 0000000000..9dba05d2c1
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java
@@ -0,0 +1,38 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+public class LegacyChangeVariablesInvocation extends MethodInvocation {
+ private Map<String, Object> variableChanges = new HashMap<String, Object>();
+
+ public LegacyChangeVariablesInvocation(String connectorId,
+ String variableName, Object value) {
+ super(connectorId, ApplicationConnection.UPDATE_VARIABLE_INTERFACE,
+ ApplicationConnection.UPDATE_VARIABLE_METHOD);
+ setVariableChange(variableName, value);
+ }
+
+ public static boolean isLegacyVariableChange(String interfaceName,
+ String methodName) {
+ return ApplicationConnection.UPDATE_VARIABLE_METHOD
+ .equals(interfaceName)
+ && ApplicationConnection.UPDATE_VARIABLE_METHOD
+ .equals(methodName);
+ }
+
+ public void setVariableChange(String name, Object value) {
+ variableChanges.put(name, value);
+ }
+
+ public Map<String, Object> getVariableChanges() {
+ return variableChanges;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java
new file mode 100644
index 0000000000..70c3add858
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java
@@ -0,0 +1,9 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class NoInputStreamException extends Exception {
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java
new file mode 100644
index 0000000000..e4db8453b0
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java
@@ -0,0 +1,9 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class NoOutputStreamException extends Exception {
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java
new file mode 100644
index 0000000000..70505ab5f9
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java
@@ -0,0 +1,398 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.File;
+import java.io.Serializable;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.EventRequest;
+import javax.portlet.EventResponse;
+import javax.portlet.MimeResponse;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletModeException;
+import javax.portlet.PortletResponse;
+import javax.portlet.PortletSession;
+import javax.portlet.PortletURL;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+import javax.portlet.ResourceRequest;
+import javax.portlet.ResourceResponse;
+import javax.portlet.StateAwareResponse;
+import javax.servlet.http.HttpSessionBindingListener;
+import javax.xml.namespace.QName;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.ExternalResource;
+import com.vaadin.ui.Root;
+
+/**
+ * TODO Write documentation, fix JavaDoc tags.
+ *
+ * This is automatically registered as a {@link HttpSessionBindingListener} when
+ * {@link PortletSession#setAttribute()} is called with the context as value.
+ *
+ * @author peholmst
+ */
+@SuppressWarnings("serial")
+public class PortletApplicationContext2 extends AbstractWebApplicationContext {
+
+ protected Map<Application, Set<PortletListener>> portletListeners = new HashMap<Application, Set<PortletListener>>();
+
+ protected transient PortletSession session;
+ protected transient PortletConfig portletConfig;
+
+ protected HashMap<String, Application> portletWindowIdToApplicationMap = new HashMap<String, Application>();
+
+ private transient PortletResponse response;
+
+ private final Map<String, QName> eventActionDestinationMap = new HashMap<String, QName>();
+ private final Map<String, Serializable> eventActionValueMap = new HashMap<String, Serializable>();
+
+ private final Map<String, String> sharedParameterActionNameMap = new HashMap<String, String>();
+ private final Map<String, String> sharedParameterActionValueMap = new HashMap<String, String>();
+
+ @Override
+ public File getBaseDirectory() {
+ String resultPath = session.getPortletContext().getRealPath("/");
+ if (resultPath != null) {
+ return new File(resultPath);
+ } else {
+ try {
+ final URL url = session.getPortletContext().getResource("/");
+ return new File(url.getFile());
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger()
+ .log(Level.INFO,
+ "Cannot access base directory, possible security issue "
+ + "with Application Server or Servlet Container",
+ e);
+ }
+ }
+ return null;
+ }
+
+ protected PortletCommunicationManager getApplicationManager(
+ Application application) {
+ PortletCommunicationManager mgr = (PortletCommunicationManager) applicationToAjaxAppMgrMap
+ .get(application);
+
+ if (mgr == null) {
+ // Creates a new manager
+ mgr = createPortletCommunicationManager(application);
+ applicationToAjaxAppMgrMap.put(application, mgr);
+ }
+ return mgr;
+ }
+
+ protected PortletCommunicationManager createPortletCommunicationManager(
+ Application application) {
+ return new PortletCommunicationManager(application);
+ }
+
+ public static PortletApplicationContext2 getApplicationContext(
+ PortletSession session) {
+ Object cxattr = session.getAttribute(PortletApplicationContext2.class
+ .getName());
+ PortletApplicationContext2 cx = null;
+ // can be false also e.g. if old context comes from another
+ // classloader when using
+ // <private-session-attributes>false</private-session-attributes>
+ // and redeploying the portlet - see #7461
+ if (cxattr instanceof PortletApplicationContext2) {
+ cx = (PortletApplicationContext2) cxattr;
+ }
+ if (cx == null) {
+ cx = new PortletApplicationContext2();
+ session.setAttribute(PortletApplicationContext2.class.getName(), cx);
+ }
+ if (cx.session == null) {
+ cx.session = session;
+ }
+ return cx;
+ }
+
+ @Override
+ protected void removeApplication(Application application) {
+ super.removeApplication(application);
+ // values() is backed by map, removes the key-value pair from the map
+ portletWindowIdToApplicationMap.values().remove(application);
+ }
+
+ protected void addApplication(Application application,
+ String portletWindowId) {
+ applications.add(application);
+ portletWindowIdToApplicationMap.put(portletWindowId, application);
+ }
+
+ public Application getApplicationForWindowId(String portletWindowId) {
+ return portletWindowIdToApplicationMap.get(portletWindowId);
+ }
+
+ public PortletSession getPortletSession() {
+ return session;
+ }
+
+ public PortletConfig getPortletConfig() {
+ return portletConfig;
+ }
+
+ public void setPortletConfig(PortletConfig config) {
+ portletConfig = config;
+ }
+
+ public void addPortletListener(Application app, PortletListener listener) {
+ Set<PortletListener> l = portletListeners.get(app);
+ if (l == null) {
+ l = new LinkedHashSet<PortletListener>();
+ portletListeners.put(app, l);
+ }
+ l.add(listener);
+ }
+
+ public void removePortletListener(Application app, PortletListener listener) {
+ Set<PortletListener> l = portletListeners.get(app);
+ if (l != null) {
+ l.remove(listener);
+ }
+ }
+
+ public void firePortletRenderRequest(Application app, Root root,
+ RenderRequest request, RenderResponse response) {
+ Set<PortletListener> listeners = portletListeners.get(app);
+ if (listeners != null) {
+ for (PortletListener l : listeners) {
+ l.handleRenderRequest(request, new RestrictedRenderResponse(
+ response), root);
+ }
+ }
+ }
+
+ public void firePortletActionRequest(Application app, Root root,
+ ActionRequest request, ActionResponse response) {
+ String key = request.getParameter(ActionRequest.ACTION_NAME);
+ if (eventActionDestinationMap.containsKey(key)) {
+ // this action request is only to send queued portlet events
+ response.setEvent(eventActionDestinationMap.get(key),
+ eventActionValueMap.get(key));
+ // cleanup
+ eventActionDestinationMap.remove(key);
+ eventActionValueMap.remove(key);
+ } else if (sharedParameterActionNameMap.containsKey(key)) {
+ // this action request is only to set shared render parameters
+ response.setRenderParameter(sharedParameterActionNameMap.get(key),
+ sharedParameterActionValueMap.get(key));
+ // cleanup
+ sharedParameterActionNameMap.remove(key);
+ sharedParameterActionValueMap.remove(key);
+ } else {
+ // normal action request, notify listeners
+ Set<PortletListener> listeners = portletListeners.get(app);
+ if (listeners != null) {
+ for (PortletListener l : listeners) {
+ l.handleActionRequest(request, response, root);
+ }
+ }
+ }
+ }
+
+ public void firePortletEventRequest(Application app, Root root,
+ EventRequest request, EventResponse response) {
+ Set<PortletListener> listeners = portletListeners.get(app);
+ if (listeners != null) {
+ for (PortletListener l : listeners) {
+ l.handleEventRequest(request, response, root);
+ }
+ }
+ }
+
+ public void firePortletResourceRequest(Application app, Root root,
+ ResourceRequest request, ResourceResponse response) {
+ Set<PortletListener> listeners = portletListeners.get(app);
+ if (listeners != null) {
+ for (PortletListener l : listeners) {
+ l.handleResourceRequest(request, response, root);
+ }
+ }
+ }
+
+ public interface PortletListener extends Serializable {
+
+ public void handleRenderRequest(RenderRequest request,
+ RenderResponse response, Root root);
+
+ public void handleActionRequest(ActionRequest request,
+ ActionResponse response, Root root);
+
+ public void handleEventRequest(EventRequest request,
+ EventResponse response, Root root);
+
+ public void handleResourceRequest(ResourceRequest request,
+ ResourceResponse response, Root root);
+ }
+
+ /**
+ * This is for use by {@link AbstractApplicationPortlet} only.
+ *
+ * TODO cleaner implementation, now "semi-static"!
+ *
+ * @param mimeResponse
+ */
+ void setResponse(PortletResponse response) {
+ this.response = response;
+ }
+
+ /**
+ * Creates a new action URL.
+ *
+ * @param action
+ * @return action URL or null if called outside a MimeRequest (outside a
+ * UIDL request or similar)
+ */
+ public PortletURL generateActionURL(String action) {
+ PortletURL url = null;
+ if (response instanceof MimeResponse) {
+ url = ((MimeResponse) response).createActionURL();
+ url.setParameter("javax.portlet.action", action);
+ } else {
+ return null;
+ }
+ return url;
+ }
+
+ /**
+ * Sends a portlet event to the indicated destination.
+ *
+ * Internally, an action may be created and opened, as an event cannot be
+ * sent directly from all types of requests.
+ *
+ * The event destinations and values need to be kept in the context until
+ * sent. Any memory leaks if the action fails are limited to the session.
+ *
+ * Event names for events sent and received by a portlet need to be declared
+ * in portlet.xml .
+ *
+ * @param root
+ * a window in which a temporary action URL can be opened if
+ * necessary
+ * @param name
+ * event name
+ * @param value
+ * event value object that is Serializable and, if appropriate,
+ * has a valid JAXB annotation
+ */
+ public void sendPortletEvent(Root root, QName name, Serializable value)
+ throws IllegalStateException {
+ if (response instanceof MimeResponse) {
+ String actionKey = "" + System.currentTimeMillis();
+ while (eventActionDestinationMap.containsKey(actionKey)) {
+ actionKey = actionKey + ".";
+ }
+ PortletURL actionUrl = generateActionURL(actionKey);
+ if (actionUrl != null) {
+ eventActionDestinationMap.put(actionKey, name);
+ eventActionValueMap.put(actionKey, value);
+ root.getPage().open(new ExternalResource(actionUrl.toString()));
+ } else {
+ // this should never happen as we already know the response is a
+ // MimeResponse
+ throw new IllegalStateException(
+ "Portlet events can only be sent from a portlet request");
+ }
+ } else if (response instanceof StateAwareResponse) {
+ ((StateAwareResponse) response).setEvent(name, value);
+ } else {
+ throw new IllegalStateException(
+ "Portlet events can only be sent from a portlet request");
+ }
+ }
+
+ /**
+ * Sets a shared portlet parameter.
+ *
+ * Internally, an action may be created and opened, as shared parameters
+ * cannot be set directly from all types of requests.
+ *
+ * The parameters and values need to be kept in the context until sent. Any
+ * memory leaks if the action fails are limited to the session.
+ *
+ * Shared parameters set or read by a portlet need to be declared in
+ * portlet.xml .
+ *
+ * @param root
+ * a window in which a temporary action URL can be opened if
+ * necessary
+ * @param name
+ * parameter identifier
+ * @param value
+ * parameter value
+ */
+ public void setSharedRenderParameter(Root root, String name, String value)
+ throws IllegalStateException {
+ if (response instanceof MimeResponse) {
+ String actionKey = "" + System.currentTimeMillis();
+ while (sharedParameterActionNameMap.containsKey(actionKey)) {
+ actionKey = actionKey + ".";
+ }
+ PortletURL actionUrl = generateActionURL(actionKey);
+ if (actionUrl != null) {
+ sharedParameterActionNameMap.put(actionKey, name);
+ sharedParameterActionValueMap.put(actionKey, value);
+ root.getPage().open(new ExternalResource(actionUrl.toString()));
+ } else {
+ // this should never happen as we already know the response is a
+ // MimeResponse
+ throw new IllegalStateException(
+ "Shared parameters can only be set from a portlet request");
+ }
+ } else if (response instanceof StateAwareResponse) {
+ ((StateAwareResponse) response).setRenderParameter(name, value);
+ } else {
+ throw new IllegalStateException(
+ "Shared parameters can only be set from a portlet request");
+ }
+ }
+
+ /**
+ * Sets the portlet mode. This may trigger a new render request.
+ *
+ * Portlet modes used by a portlet need to be declared in portlet.xml .
+ *
+ * @param root
+ * a window in which the render URL can be opened if necessary
+ * @param portletMode
+ * the portlet mode to switch to
+ * @throws PortletModeException
+ * if the portlet mode is not allowed for some reason
+ * (configuration, permissions etc.)
+ */
+ public void setPortletMode(Root root, PortletMode portletMode)
+ throws IllegalStateException, PortletModeException {
+ if (response instanceof MimeResponse) {
+ PortletURL url = ((MimeResponse) response).createRenderURL();
+ url.setPortletMode(portletMode);
+ throw new RuntimeException("Root.open has not yet been implemented");
+ // root.open(new ExternalResource(url.toString()));
+ } else if (response instanceof StateAwareResponse) {
+ ((StateAwareResponse) response).setPortletMode(portletMode);
+ } else {
+ throw new IllegalStateException(
+ "Portlet mode can only be changed from a portlet request");
+ }
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(PortletApplicationContext2.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java
new file mode 100644
index 0000000000..39c27d05fe
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java
@@ -0,0 +1,170 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.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 com.vaadin.Application;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.external.json.JSONObject;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConfiguration;
+import com.vaadin.ui.Root;
+
+/**
+ * TODO document me!
+ *
+ * @author peholmst
+ *
+ */
+@SuppressWarnings("serial")
+public class PortletCommunicationManager extends AbstractCommunicationManager {
+
+ public PortletCommunicationManager(Application application) {
+ super(application);
+ }
+
+ @Override
+ protected BootstrapHandler createBootstrapHandler() {
+ return new BootstrapHandler() {
+ @Override
+ public boolean handleRequest(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException {
+ PortletRequest portletRequest = WrappedPortletRequest.cast(
+ request).getPortletRequest();
+ if (portletRequest instanceof RenderRequest) {
+ return super.handleRequest(application, request, response);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected String getApplicationId(BootstrapContext context) {
+ PortletRequest portletRequest = WrappedPortletRequest.cast(
+ context.getRequest()).getPortletRequest();
+ /*
+ * We need to generate a unique ID because some portals already
+ * create a DIV with the portlet's Window ID as the DOM ID.
+ */
+ return "v-" + portletRequest.getWindowID();
+ }
+
+ @Override
+ protected String getAppUri(BootstrapContext context) {
+ return getRenderResponse(context).createActionURL().toString();
+ }
+
+ private RenderResponse getRenderResponse(BootstrapContext context) {
+ PortletResponse response = ((WrappedPortletResponse) context
+ .getResponse()).getPortletResponse();
+
+ RenderResponse renderResponse = (RenderResponse) response;
+ return renderResponse;
+ }
+
+ @Override
+ protected JSONObject getDefaultParameters(BootstrapContext context)
+ throws JSONException {
+ /*
+ * We need this in order to get uploads to work. TODO this is
+ * not needed for uploads anymore, check if this is needed for
+ * some other things
+ */
+ JSONObject defaults = super.getDefaultParameters(context);
+
+ ResourceURL portletResourceUrl = getRenderResponse(context)
+ .createResourceURL();
+ portletResourceUrl
+ .setResourceID(AbstractApplicationPortlet.RESOURCE_URL_ID);
+ defaults.put(ApplicationConfiguration.PORTLET_RESOUCE_URL_BASE,
+ portletResourceUrl.toString());
+
+ defaults.put("pathInfo", "");
+
+ return defaults;
+ }
+
+ @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 = WrappedPortletRequest
+ .cast(context.getRequest())
+ .getPortalProperty(
+ AbstractApplicationPortlet.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) {
+ DeploymentConfiguration deploymentConfiguration = context
+ .getRequest().getDeploymentConfiguration();
+ return deploymentConfiguration.getApplicationOrSystemProperty(
+ AbstractApplicationPortlet.PORTLET_PARAMETER_STYLE,
+ null);
+ }
+
+ @Override
+ protected String getInitialUIDL(WrappedRequest request, Root root)
+ throws PaintException, JSONException {
+ return PortletCommunicationManager.this.getInitialUIDL(request,
+ root);
+ }
+
+ @Override
+ protected JSONObject getApplicationParameters(
+ BootstrapContext context) throws JSONException,
+ PaintException {
+ JSONObject parameters = super.getApplicationParameters(context);
+ WrappedPortletResponse wrappedPortletResponse = (WrappedPortletResponse) context
+ .getResponse();
+ MimeResponse portletResponse = (MimeResponse) wrappedPortletResponse
+ .getPortletResponse();
+ ResourceURL resourceURL = portletResponse.createResourceURL();
+ resourceURL.setResourceID("browserDetails");
+ parameters.put("browserDetailsUrl", resourceURL.toString());
+ return parameters;
+ }
+
+ };
+
+ }
+
+ @Override
+ protected InputStream getThemeResourceAsStream(Root root, String themeName,
+ String resource) {
+ PortletApplicationContext2 context = (PortletApplicationContext2) root
+ .getApplication().getContext();
+ PortletContext portletContext = context.getPortletSession()
+ .getPortletContext();
+ return portletContext.getResourceAsStream("/"
+ + AbstractApplicationPortlet.THEME_DIRECTORY_PATH + themeName
+ + "/" + resource);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java
new file mode 100644
index 0000000000..8a30f5c1d4
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java
@@ -0,0 +1,56 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletResponse;
+import javax.servlet.Filter;
+
+import com.vaadin.Application;
+import com.vaadin.service.ApplicationContext.TransactionListener;
+import com.vaadin.terminal.Terminal;
+
+/**
+ * An {@link Application} that implements this interface gets notified of
+ * request start and end by the terminal. It is quite similar to the
+ * {@link HttpServletRequestListener}, but the parameters are Portlet specific.
+ * If an Application is deployed as both a Servlet and a Portlet, one most
+ * likely needs to implement both.
+ * <p>
+ * Only JSR 286 style Portlets are supported.
+ * <p>
+ * The interface can be used for several helper tasks including:
+ * <ul>
+ * <li>Opening and closing database connections
+ * <li>Implementing {@link ThreadLocal}
+ * <li>Inter-portlet communication
+ * </ul>
+ * <p>
+ * Alternatives for implementing similar features are are Servlet {@link Filter}
+ * s and {@link TransactionListener}s in Vaadin.
+ *
+ * @since 6.2
+ * @see HttpServletRequestListener
+ */
+public interface PortletRequestListener extends Serializable {
+
+ /**
+ * This method is called before {@link Terminal} applies the request to
+ * Application.
+ *
+ * @param requestData
+ * the {@link PortletRequest} about to change Application state
+ */
+ public void onRequestStart(PortletRequest request, PortletResponse response);
+
+ /**
+ * This method is called at the end of each request.
+ *
+ * @param requestData
+ * the {@link PortletRequest}
+ */
+ public void onRequestEnd(PortletRequest request, PortletResponse response);
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java
new file mode 100644
index 0000000000..6c0edec466
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/RequestTimer.java
@@ -0,0 +1,43 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.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(AbstractWebApplicationContext 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.setLastRequestTime(time);
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java
new file mode 100644
index 0000000000..2104ad4b87
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ResourceReference.java
@@ -0,0 +1,67 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.Application;
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.ExternalResource;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.ThemeResource;
+
+public class ResourceReference extends URLReference {
+
+ private Resource resource;
+
+ public ResourceReference(Resource resource) {
+ this.resource = resource;
+ }
+
+ public Resource getResource() {
+ return resource;
+ }
+
+ @Override
+ public String getURL() {
+ if (resource instanceof ExternalResource) {
+ return ((ExternalResource) resource).getURL();
+ } else if (resource instanceof ApplicationResource) {
+ final ApplicationResource r = (ApplicationResource) resource;
+ final Application a = r.getApplication();
+ if (a == null) {
+ throw new RuntimeException(
+ "An ApplicationResource ("
+ + r.getClass().getName()
+ + " must be attached to an application when it is sent to the client.");
+ }
+ final String uri = a.getRelativeLocation(r);
+ return uri;
+ } else if (resource instanceof ThemeResource) {
+ final String uri = "theme://"
+ + ((ThemeResource) resource).getResourceId();
+ return uri;
+ } else {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " does not support resources of type: "
+ + resource.getClass().getName());
+ }
+
+ }
+
+ public static ResourceReference create(Resource resource) {
+ if (resource == null) {
+ return null;
+ } else {
+ return new ResourceReference(resource);
+ }
+ }
+
+ public static Resource getResource(URLReference reference) {
+ if (reference == null) {
+ return null;
+ }
+ assert reference instanceof ResourceReference;
+ return ((ResourceReference) reference).getResource();
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java
new file mode 100644
index 0000000000..9fdffbf9a5
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java
@@ -0,0 +1,172 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Locale;
+
+import javax.portlet.CacheControl;
+import javax.portlet.PortletMode;
+import javax.portlet.PortletURL;
+import javax.portlet.RenderResponse;
+import javax.portlet.ResourceURL;
+import javax.servlet.http.Cookie;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Element;
+
+/**
+ * Read-only wrapper for a {@link RenderResponse}.
+ *
+ * Only for use by {@link PortletApplicationContext} and
+ * {@link PortletApplicationContext2}.
+ */
+class RestrictedRenderResponse implements RenderResponse, Serializable {
+
+ private RenderResponse response;
+
+ RestrictedRenderResponse(RenderResponse response) {
+ this.response = response;
+ }
+
+ @Override
+ public void addProperty(String key, String value) {
+ response.addProperty(key, value);
+ }
+
+ @Override
+ public PortletURL createActionURL() {
+ return response.createActionURL();
+ }
+
+ @Override
+ public PortletURL createRenderURL() {
+ return response.createRenderURL();
+ }
+
+ @Override
+ public String encodeURL(String path) {
+ return response.encodeURL(path);
+ }
+
+ @Override
+ public void flushBuffer() throws IOException {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public int getBufferSize() {
+ return response.getBufferSize();
+ }
+
+ @Override
+ public String getCharacterEncoding() {
+ return response.getCharacterEncoding();
+ }
+
+ @Override
+ public String getContentType() {
+ return response.getContentType();
+ }
+
+ @Override
+ public Locale getLocale() {
+ return response.getLocale();
+ }
+
+ @Override
+ public String getNamespace() {
+ return response.getNamespace();
+ }
+
+ @Override
+ public OutputStream getPortletOutputStream() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ @Override
+ public boolean isCommitted() {
+ return response.isCommitted();
+ }
+
+ @Override
+ public void reset() {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public void resetBuffer() {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public void setBufferSize(int size) {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public void setContentType(String type) {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public void setProperty(String key, String value) {
+ response.setProperty(key, value);
+ }
+
+ @Override
+ public void setTitle(String title) {
+ response.setTitle(title);
+ }
+
+ @Override
+ public void setNextPossiblePortletModes(Collection<PortletMode> portletModes) {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public ResourceURL createResourceURL() {
+ return response.createResourceURL();
+ }
+
+ @Override
+ public CacheControl getCacheControl() {
+ return response.getCacheControl();
+ }
+
+ @Override
+ public void addProperty(Cookie cookie) {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public void addProperty(String key, Element element) {
+ // NOP
+ // TODO throw?
+ }
+
+ @Override
+ public Element createElement(String tagName) throws DOMException {
+ // NOP
+ return null;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcManager.java b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java
new file mode 100644
index 0000000000..026c847e2b
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/RpcManager.java
@@ -0,0 +1,48 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+
+/**
+ * Server side RPC manager that can invoke methods based on RPC calls received
+ * from the client.
+ *
+ * @since 7.0
+ */
+public interface RpcManager extends Serializable {
+ public void applyInvocation(ServerRpcMethodInvocation invocation)
+ throws RpcInvocationException;
+
+ /**
+ * Wrapper exception for exceptions which occur during invocation of an RPC
+ * call
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0
+ *
+ */
+ public static class RpcInvocationException extends Exception {
+
+ public RpcInvocationException() {
+ super();
+ }
+
+ public RpcInvocationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RpcInvocationException(String message) {
+ super(message);
+ }
+
+ public RpcInvocationException(Throwable cause) {
+ super(cause);
+ }
+
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java
new file mode 100644
index 0000000000..b280f5c6b5
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/RpcTarget.java
@@ -0,0 +1,28 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.VariableOwner;
+
+/**
+ * Marker interface for server side classes that can receive RPC calls.
+ *
+ * This plays a role similar to that of {@link VariableOwner}.
+ *
+ * @since 7.0
+ */
+public interface RpcTarget extends Serializable {
+ /**
+ * Returns the RPC manager instance to use when receiving calls for an RPC
+ * interface.
+ *
+ * @param rpcInterface
+ * interface for which the call was made
+ * @return RpcManager or null if none found for the interface
+ */
+ public RpcManager getRpcManager(Class<?> rpcInterface);
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java
new file mode 100644
index 0000000000..1c7af82a36
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java
@@ -0,0 +1,142 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.shared.Connector;
+
+/**
+ * Server side RPC manager that handles RPC calls coming from the client.
+ *
+ * Each {@link RpcTarget} (typically a {@link ClientConnector}) should have its
+ * own instance of {@link ServerRpcManager} if it wants to receive RPC calls
+ * from the client.
+ *
+ * @since 7.0
+ */
+public class ServerRpcManager<T> implements RpcManager {
+
+ private final T implementation;
+ private final Class<T> rpcInterface;
+
+ private static final Map<Class<?>, Class<?>> boxedTypes = new HashMap<Class<?>, Class<?>>();
+ static {
+ try {
+ Class<?>[] boxClasses = new Class<?>[] { Boolean.class, Byte.class,
+ Short.class, Character.class, Integer.class, Long.class,
+ Float.class, Double.class };
+ for (Class<?> boxClass : boxClasses) {
+ Field typeField = boxClass.getField("TYPE");
+ Class<?> primitiveType = (Class<?>) typeField.get(boxClass);
+ boxedTypes.put(primitiveType, boxClass);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Create a RPC manager for an RPC target.
+ *
+ * @param target
+ * RPC call target (normally a {@link Connector})
+ * @param implementation
+ * RPC interface implementation for the target
+ * @param rpcInterface
+ * RPC interface type
+ */
+ public ServerRpcManager(T implementation, Class<T> rpcInterface) {
+ this.implementation = implementation;
+ this.rpcInterface = rpcInterface;
+ }
+
+ /**
+ * Invoke a method in a server side RPC target class. This method is to be
+ * used by the RPC framework and unit testing tools only.
+ *
+ * @param target
+ * non-null target of the RPC call
+ * @param invocation
+ * method invocation to perform
+ * @throws RpcInvocationException
+ */
+ public static void applyInvocation(RpcTarget target,
+ ServerRpcMethodInvocation invocation) throws RpcInvocationException {
+ RpcManager manager = target.getRpcManager(invocation
+ .getInterfaceClass());
+ if (manager != null) {
+ manager.applyInvocation(invocation);
+ } else {
+ getLogger()
+ .log(Level.WARNING,
+ "RPC call received for RpcTarget "
+ + target.getClass().getName()
+ + " ("
+ + invocation.getConnectorId()
+ + ") but the target has not registered any RPC interfaces");
+ }
+ }
+
+ /**
+ * Returns the RPC interface implementation for the RPC target.
+ *
+ * @return RPC interface implementation
+ */
+ protected T getImplementation() {
+ return implementation;
+ }
+
+ /**
+ * Returns the RPC interface type managed by this RPC manager instance.
+ *
+ * @return RPC interface type
+ */
+ protected Class<T> getRpcInterface() {
+ return rpcInterface;
+ }
+
+ /**
+ * Invoke a method in a server side RPC target class. This method is to be
+ * used by the RPC framework and unit testing tools only.
+ *
+ * @param invocation
+ * method invocation to perform
+ */
+ @Override
+ public void applyInvocation(ServerRpcMethodInvocation invocation)
+ throws RpcInvocationException {
+ Method method = invocation.getMethod();
+ Class<?>[] parameterTypes = method.getParameterTypes();
+ Object[] args = new Object[parameterTypes.length];
+ Object[] arguments = invocation.getParameters();
+ for (int i = 0; i < args.length; i++) {
+ // no conversion needed for basic cases
+ // Class<?> type = parameterTypes[i];
+ // if (type.isPrimitive()) {
+ // type = boxedTypes.get(type);
+ // }
+ args[i] = arguments[i];
+ }
+ try {
+ method.invoke(implementation, args);
+ } catch (Exception e) {
+ throw new RpcInvocationException("Unable to invoke method "
+ + invocation.getMethodName() + " in "
+ + invocation.getInterfaceName(), e);
+ }
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(ServerRpcManager.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java
new file mode 100644
index 0000000000..ff81a27596
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java
@@ -0,0 +1,113 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.vaadin.shared.communication.MethodInvocation;
+import com.vaadin.shared.communication.ServerRpc;
+
+public class ServerRpcMethodInvocation extends MethodInvocation {
+
+ private static final Map<String, Method> invocationMethodCache = new ConcurrentHashMap<String, Method>(
+ 128, 0.75f, 1);
+
+ private final Method method;
+
+ private Class<? extends ServerRpc> interfaceClass;
+
+ public ServerRpcMethodInvocation(String connectorId, String interfaceName,
+ String methodName, int parameterCount) {
+ super(connectorId, interfaceName, methodName);
+
+ interfaceClass = findClass();
+ method = findInvocationMethod(interfaceClass, methodName,
+ parameterCount);
+ }
+
+ private Class<? extends ServerRpc> findClass() {
+ try {
+ Class<?> rpcInterface = Class.forName(getInterfaceName());
+ if (!ServerRpc.class.isAssignableFrom(rpcInterface)) {
+ throw new IllegalArgumentException("The interface "
+ + getInterfaceName() + "is not a server RPC interface.");
+ }
+ return (Class<? extends ServerRpc>) rpcInterface;
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("The server RPC interface "
+ + getInterfaceName() + " could not be found", e);
+ } finally {
+
+ }
+ }
+
+ public Class<? extends ServerRpc> getInterfaceClass() {
+ return interfaceClass;
+ }
+
+ public Method getMethod() {
+ return method;
+ }
+
+ /**
+ * Tries to find the method from the cache or alternatively by invoking
+ * {@link #doFindInvocationMethod(Class, String, int)} and updating the
+ * cache.
+ *
+ * @param targetType
+ * @param methodName
+ * @param parameterCount
+ * @return
+ */
+ private Method findInvocationMethod(Class<?> targetType, String methodName,
+ int parameterCount) {
+ // TODO currently only using method name and number of parameters as the
+ // signature
+ String signature = targetType.getName() + "." + methodName + "("
+ + parameterCount;
+ Method invocationMethod = invocationMethodCache.get(signature);
+
+ if (invocationMethod == null) {
+ invocationMethod = doFindInvocationMethod(targetType, methodName,
+ parameterCount);
+
+ if (invocationMethod != null) {
+ invocationMethodCache.put(signature, invocationMethod);
+ }
+ }
+
+ if (invocationMethod == null) {
+ throw new IllegalStateException("Can't find method " + methodName
+ + " with " + parameterCount + " parameters in "
+ + targetType.getName());
+ }
+
+ return invocationMethod;
+ }
+
+ /**
+ * Tries to find the method from the class by looping through available
+ * methods.
+ *
+ * @param targetType
+ * @param methodName
+ * @param parameterCount
+ * @return
+ */
+ private Method doFindInvocationMethod(Class<?> targetType,
+ String methodName, int parameterCount) {
+ Method[] methods = targetType.getMethods();
+ for (Method method : methods) {
+ Class<?>[] parameterTypes = method.getParameterTypes();
+ if (method.getName().equals(methodName)
+ && parameterTypes.length == parameterCount) {
+ return method;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
new file mode 100644
index 0000000000..2a1dc31897
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
@@ -0,0 +1,120 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.io.Serializable;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.ui.Root;
+
+/*
+ @VaadinApache2LicenseForJavaFiles@
+ */
+
+class ServletPortletHelper implements Serializable {
+ public static final String UPLOAD_URL_PREFIX = "APP/UPLOAD/";
+
+ public static class ApplicationClassException extends Exception {
+
+ public ApplicationClassException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ApplicationClassException(String message) {
+ super(message);
+ }
+ }
+
+ static Class<? extends Application> getApplicationClass(
+ DeploymentConfiguration deploymentConfiguration)
+ throws ApplicationClassException {
+ String applicationParameter = deploymentConfiguration
+ .getInitParameters().getProperty("application");
+ String rootParameter = deploymentConfiguration.getInitParameters()
+ .getProperty(Application.ROOT_PARAMETER);
+ ClassLoader classLoader = deploymentConfiguration.getClassLoader();
+
+ if (applicationParameter == null) {
+
+ // Validate the parameter value
+ verifyRootClass(rootParameter, classLoader);
+
+ // Application can be used if a valid rootLayout is defined
+ return Application.class;
+ }
+
+ try {
+ return (Class<? extends Application>) classLoader
+ .loadClass(applicationParameter);
+ } catch (final ClassNotFoundException e) {
+ throw new ApplicationClassException(
+ "Failed to load application class: " + applicationParameter,
+ e);
+ }
+ }
+
+ private static void verifyRootClass(String className,
+ ClassLoader classLoader) throws ApplicationClassException {
+ if (className == null) {
+ throw new ApplicationClassException(Application.ROOT_PARAMETER
+ + " init parameter not defined");
+ }
+
+ // Check that the root layout class can be found
+ try {
+ Class<?> rootClass = classLoader.loadClass(className);
+ if (!Root.class.isAssignableFrom(rootClass)) {
+ throw new ApplicationClassException(className
+ + " does not implement Root");
+ }
+ // Try finding a default constructor, else throw exception
+ rootClass.getConstructor();
+ } catch (ClassNotFoundException e) {
+ throw new ApplicationClassException(className
+ + " could not be loaded", e);
+ } catch (SecurityException e) {
+ throw new ApplicationClassException("Could not access " + className
+ + " class", e);
+ } catch (NoSuchMethodException e) {
+ throw new ApplicationClassException(className
+ + " doesn't have a public no-args constructor");
+ }
+ }
+
+ private static boolean hasPathPrefix(WrappedRequest request, String prefix) {
+ String pathInfo = request.getRequestPathInfo();
+
+ if (pathInfo == null) {
+ return false;
+ }
+
+ if (!prefix.startsWith("/")) {
+ prefix = '/' + prefix;
+ }
+
+ if (pathInfo.startsWith(prefix)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public static boolean isFileUploadRequest(WrappedRequest request) {
+ return hasPathPrefix(request, UPLOAD_URL_PREFIX);
+ }
+
+ public static boolean isConnectorResourceRequest(WrappedRequest request) {
+ return hasPathPrefix(request,
+ ApplicationConnection.CONNECTOR_RESOURCE_PREFIX + "/");
+ }
+
+ public static boolean isUIDLRequest(WrappedRequest request) {
+ return hasPathPrefix(request, ApplicationConnection.UIDL_REQUEST_PATH);
+ }
+
+ public static boolean isApplicationResourceRequest(WrappedRequest request) {
+ return hasPathPrefix(request, ApplicationConnection.APP_REQUEST_PATH);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java
new file mode 100644
index 0000000000..37b76de443
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java
@@ -0,0 +1,9 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class SessionExpiredException extends Exception {
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java
new file mode 100644
index 0000000000..0d4963bd7d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java
@@ -0,0 +1,16 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.terminal.StreamVariable.StreamingEndEvent;
+
+@SuppressWarnings("serial")
+final class StreamingEndEventImpl extends AbstractStreamingEvent implements
+ StreamingEndEvent {
+
+ public StreamingEndEventImpl(String filename, String type, long totalBytes) {
+ super(filename, type, totalBytes, totalBytes);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java
new file mode 100644
index 0000000000..6ab3df2789
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.terminal.StreamVariable.StreamingErrorEvent;
+
+@SuppressWarnings("serial")
+final class StreamingErrorEventImpl extends AbstractStreamingEvent implements
+ StreamingErrorEvent {
+
+ private final Exception exception;
+
+ public StreamingErrorEventImpl(final String filename, final String type,
+ long contentLength, long bytesReceived, final Exception exception) {
+ super(filename, type, contentLength, bytesReceived);
+ this.exception = exception;
+ }
+
+ @Override
+ public final Exception getException() {
+ return exception;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java
new file mode 100644
index 0000000000..cfa7a1b98d
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java
@@ -0,0 +1,17 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.terminal.StreamVariable.StreamingProgressEvent;
+
+@SuppressWarnings("serial")
+final class StreamingProgressEventImpl extends AbstractStreamingEvent implements
+ StreamingProgressEvent {
+
+ public StreamingProgressEventImpl(final String filename, final String type,
+ long contentLength, long bytesReceived) {
+ super(filename, type, contentLength, bytesReceived);
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java
new file mode 100644
index 0000000000..274d05e111
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java
@@ -0,0 +1,28 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import com.vaadin.terminal.StreamVariable.StreamingStartEvent;
+
+@SuppressWarnings("serial")
+final class StreamingStartEventImpl extends AbstractStreamingEvent implements
+ StreamingStartEvent {
+
+ private boolean disposed;
+
+ public StreamingStartEventImpl(final String filename, final String type,
+ long contentLength) {
+ super(filename, type, contentLength, 0);
+ }
+
+ @Override
+ public void disposeStreamVariable() {
+ disposed = true;
+ }
+
+ boolean isDisposed() {
+ return disposed;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java
new file mode 100644
index 0000000000..d15ff8a7ef
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java
@@ -0,0 +1,57 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class SystemMessageException extends RuntimeException {
+
+ /**
+ * Cause of the method exception
+ */
+ private Throwable cause;
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message.
+ */
+ public SystemMessageException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message and cause.
+ *
+ * @param msg
+ * the detail message.
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> from another
+ * exception.
+ *
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(Throwable cause) {
+ this.cause = cause;
+ }
+
+ /**
+ * @see java.lang.Throwable#getCause()
+ */
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java
new file mode 100644
index 0000000000..5248af595e
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java
@@ -0,0 +1,89 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+
+/**
+ * A {@link RequestHandler} that presents an informative page if the browser in
+ * use is unsupported. Recognizes Chrome Frame and allow it to be used.
+ *
+ * <p>
+ * This handler is usually added to the application by
+ * {@link AbstractCommunicationManager}.
+ * </p>
+ */
+@SuppressWarnings("serial")
+public class UnsupportedBrowserHandler implements RequestHandler {
+
+ /** Cookie used to ignore browser checks */
+ public static final String FORCE_LOAD_COOKIE = "vaadinforceload=1";
+
+ @Override
+ public boolean handleRequest(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException {
+
+ if (request.getBrowserDetails() != null) {
+ // Check if the browser is supported
+ // If Chrome Frame is available we'll assume it's ok
+ WebBrowser b = request.getBrowserDetails().getWebBrowser();
+ if (b.isTooOldToFunctionProperly() && !b.isChromeFrameCapable()) {
+ // bypass if cookie set
+ String c = request.getHeader("Cookie");
+ if (c == null || !c.contains(FORCE_LOAD_COOKIE)) {
+ writeBrowserTooOldPage(request, response);
+ return true; // request handled
+ }
+ }
+ }
+
+ return false; // pass to next handler
+ }
+
+ /**
+ * Writes a page encouraging the user to upgrade to a more current browser.
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ */
+ protected void writeBrowserTooOldPage(WrappedRequest request,
+ WrappedResponse response) throws IOException {
+ Writer page = response.getWriter();
+ WebBrowser b = request.getBrowserDetails().getWebBrowser();
+
+ page.write("<html><body><h1>I'm sorry, but your browser is not supported</h1>"
+ + "<p>The version ("
+ + b.getBrowserMajorVersion()
+ + "."
+ + b.getBrowserMinorVersion()
+ + ") of the browser you are using "
+ + " is outdated and not supported.</p>"
+ + "<p>You should <b>consider upgrading</b> to a more up-to-date browser.</p> "
+ + "<p>The most popular browsers are <b>"
+ + " <a href=\"https://www.google.com/chrome\">Chrome</a>,"
+ + " <a href=\"http://www.mozilla.com/firefox\">Firefox</a>,"
+ + (b.isWindows() ? " <a href=\"http://windows.microsoft.com/en-US/internet-explorer/downloads/ie\">Internet Explorer</a>,"
+ : "")
+ + " <a href=\"http://www.opera.com/browser\">Opera</a>"
+ + " and <a href=\"http://www.apple.com/safari\">Safari</a>.</b><br/>"
+ + "Upgrading to the latest version of one of these <b>will make the web safer, faster and better looking.</b></p>"
+ + (b.isIE() ? "<script type=\"text/javascript\" src=\"http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js\"></script>"
+ + "<p>If you can not upgrade your browser, please consider trying <a onclick=\"CFInstall.check({mode:'overlay'});return false;\" href=\"http://www.google.com/chromeframe\">Chrome Frame</a>.</p>"
+ : "") //
+ + "<p><sub><a onclick=\"document.cookie='"
+ + FORCE_LOAD_COOKIE
+ + "';window.location.reload();return false;\" href=\"#\">Continue without updating</a> (not recommended)</sub></p>"
+ + "</body>\n" + "</html>");
+
+ page.close();
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/UploadException.java b/server/src/com/vaadin/terminal/gwt/server/UploadException.java
new file mode 100644
index 0000000000..58253da0fb
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/UploadException.java
@@ -0,0 +1,15 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class UploadException extends Exception {
+ public UploadException(Exception e) {
+ super("Upload failed", e);
+ }
+
+ public UploadException(String msg) {
+ super(msg);
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
new file mode 100644
index 0000000000..36c08b2ed9
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
@@ -0,0 +1,180 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashMap;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import com.vaadin.Application;
+
+/**
+ * Web application context for Vaadin applications.
+ *
+ * This is automatically added as a {@link HttpSessionBindingListener} when
+ * added to a {@link HttpSession}.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.1
+ */
+@SuppressWarnings("serial")
+public class WebApplicationContext extends AbstractWebApplicationContext {
+
+ protected transient HttpSession session;
+ private transient boolean reinitializingSession = false;
+
+ /**
+ * Stores a reference to the currentRequest. Null it not inside a request.
+ */
+ private transient Object currentRequest = null;
+
+ /**
+ * Creates a new Web Application Context.
+ *
+ */
+ protected WebApplicationContext() {
+
+ }
+
+ @Override
+ protected void startTransaction(Application application, Object request) {
+ currentRequest = request;
+ super.startTransaction(application, request);
+ }
+
+ @Override
+ protected void endTransaction(Application application, Object request) {
+ super.endTransaction(application, request);
+ currentRequest = null;
+ }
+
+ @Override
+ public void valueUnbound(HttpSessionBindingEvent event) {
+ if (!reinitializingSession) {
+ // Avoid closing the application if we are only reinitializing the
+ // session. Closing the application would cause the state to be lost
+ // and a new application to be created, which is not what we want.
+ super.valueUnbound(event);
+ }
+ }
+
+ /**
+ * Discards the current session and creates a new session with the same
+ * contents. The purpose of this is to introduce a new session key in order
+ * to avoid session fixation attacks.
+ */
+ @SuppressWarnings("unchecked")
+ public void reinitializeSession() {
+
+ HttpSession oldSession = getHttpSession();
+
+ // Stores all attributes (security key, reference to this context
+ // instance) so they can be added to the new session
+ HashMap<String, Object> attrs = new HashMap<String, Object>();
+ for (Enumeration<String> e = oldSession.getAttributeNames(); e
+ .hasMoreElements();) {
+ String name = e.nextElement();
+ attrs.put(name, oldSession.getAttribute(name));
+ }
+
+ // Invalidate the current session, set flag to avoid call to
+ // valueUnbound
+ reinitializingSession = true;
+ oldSession.invalidate();
+ reinitializingSession = false;
+
+ // Create a new session
+ HttpSession newSession = ((HttpServletRequest) currentRequest)
+ .getSession();
+
+ // Restores all attributes (security key, reference to this context
+ // instance)
+ for (String name : attrs.keySet()) {
+ newSession.setAttribute(name, attrs.get(name));
+ }
+
+ // Update the "current session" variable
+ session = newSession;
+ }
+
+ /**
+ * Gets the application context base directory.
+ *
+ * @see com.vaadin.service.ApplicationContext#getBaseDirectory()
+ */
+ @Override
+ public File getBaseDirectory() {
+ final String realPath = ApplicationServlet.getResourcePath(
+ session.getServletContext(), "/");
+ if (realPath == null) {
+ return null;
+ }
+ return new File(realPath);
+ }
+
+ /**
+ * Gets the http-session application is running in.
+ *
+ * @return HttpSession this application context resides in.
+ */
+ public HttpSession getHttpSession() {
+ return session;
+ }
+
+ /**
+ * Gets the application context for an HttpSession.
+ *
+ * @param session
+ * the HTTP session.
+ * @return the application context for HttpSession.
+ */
+ static public WebApplicationContext getApplicationContext(
+ HttpSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName());
+ if (cx == null) {
+ cx = new WebApplicationContext();
+ session.setAttribute(WebApplicationContext.class.getName(), cx);
+ }
+ if (cx.session == null) {
+ cx.session = session;
+ }
+ return cx;
+ }
+
+ protected void addApplication(Application application) {
+ applications.add(application);
+ }
+
+ /**
+ * Gets communication manager for an application.
+ *
+ * If this application has not been running before, a new manager is
+ * created.
+ *
+ * @param application
+ * @return CommunicationManager
+ */
+ public CommunicationManager getApplicationManager(Application application,
+ AbstractApplicationServlet servlet) {
+ CommunicationManager mgr = (CommunicationManager) applicationToAjaxAppMgrMap
+ .get(application);
+
+ if (mgr == null) {
+ // Creates new manager
+ mgr = servlet.createCommunicationManager(application);
+ applicationToAjaxAppMgrMap.put(application, mgr);
+ }
+ return mgr;
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java
new file mode 100644
index 0000000000..4b92b12b66
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WebBrowser.java
@@ -0,0 +1,462 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.shared.VBrowserDetails;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.WrappedRequest;
+
+/**
+ * Class that provides information about the web browser the user is using.
+ * Provides information such as browser name and version, screen resolution and
+ * IP address.
+ *
+ * @author Vaadin Ltd.
+ * @version @VERSION@
+ */
+public class WebBrowser implements Terminal {
+
+ private int screenHeight = 0;
+ private int screenWidth = 0;
+ private String browserApplication = null;
+ private Locale locale;
+ private String address;
+ private boolean secureConnection;
+ private int timezoneOffset = 0;
+ private int rawTimezoneOffset = 0;
+ private int dstSavings;
+ private boolean dstInEffect;
+ private boolean touchDevice;
+
+ private VBrowserDetails browserDetails;
+ private long clientServerTimeDelta;
+
+ /**
+ * There is no default-theme for this terminal type.
+ *
+ * @return Always returns null.
+ */
+
+ @Override
+ public String getDefaultTheme() {
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Terminal#getScreenHeight()
+ */
+
+ @Override
+ public int getScreenHeight() {
+ return screenHeight;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Terminal#getScreenWidth()
+ */
+
+ @Override
+ public int getScreenWidth() {
+ return screenWidth;
+ }
+
+ /**
+ * Get the browser user-agent string.
+ *
+ * @return The raw browser userAgent string
+ */
+ public String getBrowserApplication() {
+ return browserApplication;
+ }
+
+ /**
+ * Gets the IP-address of the web browser. If the application is running
+ * inside a portlet, this method will return null.
+ *
+ * @return IP-address in 1.12.123.123 -format
+ */
+ public String getAddress() {
+ return address;
+ }
+
+ /** Get the default locate of the browser. */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /** Is the connection made using HTTPS? */
+ public boolean isSecureConnection() {
+ return secureConnection;
+ }
+
+ /**
+ * Tests whether the user is using Firefox.
+ *
+ * @return true if the user is using Firefox, false if the user is not using
+ * Firefox or if no information on the browser is present
+ */
+ public boolean isFirefox() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isFirefox();
+ }
+
+ /**
+ * Tests whether the user is using Internet Explorer.
+ *
+ * @return true if the user is using Internet Explorer, false if the user is
+ * not using Internet Explorer or if no information on the browser
+ * is present
+ */
+ public boolean isIE() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isIE();
+ }
+
+ /**
+ * Tests whether the user is using Safari.
+ *
+ * @return true if the user is using Safari, false if the user is not using
+ * Safari or if no information on the browser is present
+ */
+ public boolean isSafari() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isSafari();
+ }
+
+ /**
+ * Tests whether the user is using Opera.
+ *
+ * @return true if the user is using Opera, false if the user is not using
+ * Opera or if no information on the browser is present
+ */
+ public boolean isOpera() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isOpera();
+ }
+
+ /**
+ * Tests whether the user is using Chrome.
+ *
+ * @return true if the user is using Chrome, false if the user is not using
+ * Chrome or if no information on the browser is present
+ */
+ public boolean isChrome() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isChrome();
+ }
+
+ /**
+ * Tests whether the user is using Chrome Frame.
+ *
+ * @return true if the user is using Chrome Frame, false if the user is not
+ * using Chrome or if no information on the browser is present
+ */
+ public boolean isChromeFrame() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isChromeFrame();
+ }
+
+ /**
+ * Tests whether the user's browser is Chrome Frame capable.
+ *
+ * @return true if the user can use Chrome Frame, false if the user can not
+ * or if no information on the browser is present
+ */
+ public boolean isChromeFrameCapable() {
+ if (browserDetails == null) {
+ return false;
+ }
+
+ return browserDetails.isChromeFrameCapable();
+ }
+
+ /**
+ * Gets the major version of the browser the user is using.
+ *
+ * <p>
+ * Note that Internet Explorer in IE7 compatibility mode might return 8 in
+ * some cases even though it should return 7.
+ * </p>
+ *
+ * @return The major version of the browser or -1 if not known.
+ */
+ public int getBrowserMajorVersion() {
+ if (browserDetails == null) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserMajorVersion();
+ }
+
+ /**
+ * Gets the minor version of the browser the user is using.
+ *
+ * @see #getBrowserMajorVersion()
+ *
+ * @return The minor version of the browser or -1 if not known.
+ */
+ public int getBrowserMinorVersion() {
+ if (browserDetails == null) {
+ return -1;
+ }
+
+ return browserDetails.getBrowserMinorVersion();
+ }
+
+ /**
+ * Tests whether the user is using Linux.
+ *
+ * @return true if the user is using Linux, false if the user is not using
+ * Linux or if no information on the browser is present
+ */
+ public boolean isLinux() {
+ return browserDetails.isLinux();
+ }
+
+ /**
+ * Tests whether the user is using Mac OS X.
+ *
+ * @return true if the user is using Mac OS X, false if the user is not
+ * using Mac OS X or if no information on the browser is present
+ */
+ public boolean isMacOSX() {
+ return browserDetails.isMacOSX();
+ }
+
+ /**
+ * Tests whether the user is using Windows.
+ *
+ * @return true if the user is using Windows, false if the user is not using
+ * Windows or if no information on the browser is present
+ */
+ public boolean isWindows() {
+ return browserDetails.isWindows();
+ }
+
+ /**
+ * Returns the browser-reported TimeZone offset in milliseconds from GMT.
+ * This includes possible daylight saving adjustments, to figure out which
+ * TimeZone the user actually might be in, see
+ * {@link #getRawTimezoneOffset()}.
+ *
+ * @see WebBrowser#getRawTimezoneOffset()
+ * @return timezone offset in milliseconds, 0 if not available
+ */
+ public Integer getTimezoneOffset() {
+ return timezoneOffset;
+ }
+
+ /**
+ * Returns the browser-reported TimeZone offset in milliseconds from GMT
+ * ignoring possible daylight saving adjustments that may be in effect in
+ * the browser.
+ * <p>
+ * You can use this to figure out which TimeZones the user could actually be
+ * in by calling {@link TimeZone#getAvailableIDs(int)}.
+ * </p>
+ * <p>
+ * If {@link #getRawTimezoneOffset()} and {@link #getTimezoneOffset()}
+ * returns the same value, the browser is either in a zone that does not
+ * currently have daylight saving time, or in a zone that never has daylight
+ * saving time.
+ * </p>
+ *
+ * @return timezone offset in milliseconds excluding DST, 0 if not available
+ */
+ public Integer getRawTimezoneOffset() {
+ return rawTimezoneOffset;
+ }
+
+ /**
+ * Gets the difference in minutes between the browser's GMT TimeZone and
+ * DST.
+ *
+ * @return the amount of minutes that the TimeZone shifts when DST is in
+ * effect
+ */
+ public int getDSTSavings() {
+ return dstSavings;
+ }
+
+ /**
+ * Determines whether daylight savings time (DST) is currently in effect in
+ * the region of the browser or not.
+ *
+ * @return true if the browser resides at a location that currently is in
+ * DST
+ */
+ public boolean isDSTInEffect() {
+ return dstInEffect;
+ }
+
+ /**
+ * Returns the current date and time of the browser. This will not be
+ * 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.
+ *
+ * @return the current date and time of the browser.
+ * @see #isDSTInEffect()
+ * @see #getDSTSavings()
+ * @see #getTimezoneOffset()
+ */
+ public Date getCurrentDate() {
+ return new Date(new Date().getTime() + clientServerTimeDelta);
+ }
+
+ /**
+ * @return true if the browser is detected to support touch events
+ */
+ public boolean isTouchDevice() {
+ return touchDevice;
+ }
+
+ /**
+ * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet
+ * only. Updates all properties in the class according to the given
+ * information.
+ *
+ * @param sw
+ * Screen width
+ * @param sh
+ * Screen height
+ * @param tzo
+ * TimeZone offset in minutes from GMT
+ * @param rtzo
+ * raw TimeZone offset in minutes from GMT (w/o DST adjustment)
+ * @param dstSavings
+ * the difference between the raw TimeZone and DST in minutes
+ * @param dstInEffect
+ * is DST currently active in the region or not?
+ * @param curDate
+ * the current date in milliseconds since the epoch
+ * @param touchDevice
+ */
+ void updateClientSideDetails(String sw, String sh, String tzo, String rtzo,
+ String dstSavings, String dstInEffect, String curDate,
+ boolean touchDevice) {
+ if (sw != null) {
+ try {
+ screenHeight = Integer.parseInt(sh);
+ screenWidth = Integer.parseInt(sw);
+ } catch (final NumberFormatException e) {
+ screenHeight = screenWidth = 0;
+ }
+ }
+ if (tzo != null) {
+ try {
+ // browser->java conversion: min->ms, reverse sign
+ timezoneOffset = -Integer.parseInt(tzo) * 60 * 1000;
+ } catch (final NumberFormatException e) {
+ timezoneOffset = 0; // default gmt+0
+ }
+ }
+ if (rtzo != null) {
+ try {
+ // browser->java conversion: min->ms, reverse sign
+ rawTimezoneOffset = -Integer.parseInt(rtzo) * 60 * 1000;
+ } catch (final NumberFormatException e) {
+ rawTimezoneOffset = 0; // default gmt+0
+ }
+ }
+ if (dstSavings != null) {
+ try {
+ // browser->java conversion: min->ms
+ this.dstSavings = Integer.parseInt(dstSavings) * 60 * 1000;
+ } catch (final NumberFormatException e) {
+ this.dstSavings = 0; // default no savings
+ }
+ }
+ if (dstInEffect != null) {
+ this.dstInEffect = Boolean.parseBoolean(dstInEffect);
+ }
+ if (curDate != null) {
+ try {
+ long curTime = Long.parseLong(curDate);
+ clientServerTimeDelta = curTime - new Date().getTime();
+ } catch (final NumberFormatException e) {
+ clientServerTimeDelta = 0;
+ }
+ }
+ this.touchDevice = touchDevice;
+
+ }
+
+ /**
+ * For internal use by AbstractApplicationServlet/AbstractApplicationPortlet
+ * only. Updates all properties in the class according to the given
+ * information.
+ *
+ * @param request
+ * the wrapped request to read the information from
+ */
+ void updateRequestDetails(WrappedRequest request) {
+ locale = request.getLocale();
+ address = request.getRemoteAddr();
+ secureConnection = request.isSecure();
+ String agent = request.getHeader("user-agent");
+
+ if (agent != null) {
+ browserApplication = agent;
+ browserDetails = new VBrowserDetails(agent);
+ }
+
+ if (request.getParameter("sw") != null) {
+ updateClientSideDetails(request.getParameter("sw"),
+ request.getParameter("sh"), request.getParameter("tzo"),
+ request.getParameter("rtzo"), request.getParameter("dstd"),
+ request.getParameter("dston"),
+ request.getParameter("curdate"),
+ request.getParameter("td") != null);
+ }
+ }
+
+ /**
+ * Checks if the browser is so old that it simply won't work with a Vaadin
+ * application. Can be used to redirect to an alternative page, show
+ * alternative content or similar.
+ *
+ * When this method returns true chances are very high that the browser
+ * won't work and it does not make sense to direct the user to the Vaadin
+ * application.
+ *
+ * @return true if the browser won't work, false if not the browser is
+ * supported or might work
+ */
+ public boolean isTooOldToFunctionProperly() {
+ if (browserDetails == null) {
+ // Don't know, so assume it will work
+ return false;
+ }
+
+ return browserDetails.isTooOldToFunctionProperly();
+ }
+
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java
new file mode 100644
index 0000000000..cf58f398af
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java
@@ -0,0 +1,118 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.CombinedRequest;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.WrappedRequest;
+
+/**
+ * Wrapper for {@link HttpServletRequest}.
+ *
+ * @author Vaadin Ltd.
+ * @since 7.0
+ *
+ * @see WrappedRequest
+ * @see WrappedHttpServletResponse
+ */
+public class WrappedHttpServletRequest extends HttpServletRequestWrapper
+ implements WrappedRequest {
+
+ private final DeploymentConfiguration deploymentConfiguration;
+
+ /**
+ * Wraps a http servlet request and associates with a deployment
+ * configuration
+ *
+ * @param request
+ * the http servlet request to wrap
+ * @param deploymentConfiguration
+ * the associated deployment configuration
+ */
+ public WrappedHttpServletRequest(HttpServletRequest request,
+ DeploymentConfiguration deploymentConfiguration) {
+ super(request);
+ this.deploymentConfiguration = deploymentConfiguration;
+ }
+
+ @Override
+ public String getRequestPathInfo() {
+ return getPathInfo();
+ }
+
+ @Override
+ public int getSessionMaxInactiveInterval() {
+ return getSession().getMaxInactiveInterval();
+ }
+
+ @Override
+ public Object getSessionAttribute(String name) {
+ return getSession().getAttribute(name);
+ }
+
+ @Override
+ public void setSessionAttribute(String name, Object attribute) {
+ getSession().setAttribute(name, attribute);
+ }
+
+ /**
+ * Gets the original, unwrapped HTTP servlet request.
+ *
+ * @return the servlet request
+ */
+ public HttpServletRequest getHttpServletRequest() {
+ return this;
+ }
+
+ @Override
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+
+ @Override
+ public BrowserDetails getBrowserDetails() {
+ return new BrowserDetails() {
+ @Override
+ public String getUriFragment() {
+ return null;
+ }
+
+ @Override
+ public String getWindowName() {
+ return null;
+ }
+
+ @Override
+ public WebBrowser getWebBrowser() {
+ WebApplicationContext context = (WebApplicationContext) Application
+ .getCurrent().getContext();
+ return context.getBrowser();
+ }
+ };
+ }
+
+ /**
+ * Helper method to get a <code>WrappedHttpServletRequest</code> from a
+ * <code>WrappedRequest</code>. Aside from casting, this method also takes
+ * care of situations where there's another level of wrapping.
+ *
+ * @param request
+ * a wrapped request
+ * @return a wrapped http servlet request
+ * @throws ClassCastException
+ * if the wrapped request doesn't wrap a http servlet request
+ */
+ public static WrappedHttpServletRequest cast(WrappedRequest request) {
+ if (request instanceof CombinedRequest) {
+ CombinedRequest combinedRequest = (CombinedRequest) request;
+ request = combinedRequest.getSecondRequest();
+ }
+ return (WrappedHttpServletRequest) request;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java
new file mode 100644
index 0000000000..32b2f352a8
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java
@@ -0,0 +1,75 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.WrappedResponse;
+
+/**
+ * Wrapper for {@link HttpServletResponse}.
+ *
+ * @author Vaadin Ltd.
+ * @since 7.0
+ *
+ * @see WrappedResponse
+ * @see WrappedHttpServletRequest
+ */
+public class WrappedHttpServletResponse extends HttpServletResponseWrapper
+ implements WrappedResponse {
+
+ private DeploymentConfiguration deploymentConfiguration;
+
+ /**
+ * Wraps a http servlet response and an associated deployment configuration
+ *
+ * @param response
+ * the http servlet response to wrap
+ * @param deploymentConfiguration
+ * the associated deployment configuration
+ */
+ public WrappedHttpServletResponse(HttpServletResponse response,
+ DeploymentConfiguration deploymentConfiguration) {
+ super(response);
+ this.deploymentConfiguration = deploymentConfiguration;
+ }
+
+ /**
+ * Gets the original unwrapped <code>HttpServletResponse</code>
+ *
+ * @return the unwrapped response
+ */
+ public HttpServletResponse getHttpServletResponse() {
+ return this;
+ }
+
+ @Override
+ public void setCacheTime(long milliseconds) {
+ doSetCacheTime(this, milliseconds);
+ }
+
+ // Implementation shared with WrappedPortletResponse
+ static void doSetCacheTime(WrappedResponse response, long milliseconds) {
+ if (milliseconds <= 0) {
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Pragma", "no-cache");
+ response.setDateHeader("Expires", 0);
+ } else {
+ response.setHeader("Cache-Control", "max-age=" + milliseconds
+ / 1000);
+ response.setDateHeader("Expires", System.currentTimeMillis()
+ + milliseconds);
+ // Required to apply caching in some Tomcats
+ response.setHeader("Pragma", "cache");
+ }
+ }
+
+ @Override
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java
new file mode 100644
index 0000000000..a3fa172034
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java
@@ -0,0 +1,217 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.portlet.ClientDataRequest;
+import javax.portlet.PortletRequest;
+import javax.portlet.ResourceRequest;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.CombinedRequest;
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+/**
+ * Wrapper for {@link PortletRequest} and its subclasses.
+ *
+ * @author Vaadin Ltd.
+ * @since 7.0
+ *
+ * @see WrappedRequest
+ * @see WrappedPortletResponse
+ */
+public class WrappedPortletRequest implements WrappedRequest {
+
+ private final PortletRequest request;
+ private final DeploymentConfiguration deploymentConfiguration;
+
+ /**
+ * Wraps a portlet request and an associated deployment configuration
+ *
+ * @param request
+ * the portlet request to wrap
+ * @param deploymentConfiguration
+ * the associated deployment configuration
+ */
+ public WrappedPortletRequest(PortletRequest request,
+ DeploymentConfiguration deploymentConfiguration) {
+ this.request = request;
+ this.deploymentConfiguration = deploymentConfiguration;
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return request.getAttribute(name);
+ }
+
+ @Override
+ public int getContentLength() {
+ try {
+ return ((ClientDataRequest) request).getContentLength();
+ } catch (ClassCastException e) {
+ throw new IllegalStateException(
+ "Content lenght only available for ClientDataRequests");
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ try {
+ return ((ClientDataRequest) request).getPortletInputStream();
+ } catch (ClassCastException e) {
+ throw new IllegalStateException(
+ "Input data only available for ClientDataRequests");
+ }
+ }
+
+ @Override
+ public String getParameter(String name) {
+ return request.getParameter(name);
+ }
+
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ return request.getParameterMap();
+ }
+
+ @Override
+ public void setAttribute(String name, Object o) {
+ request.setAttribute(name, o);
+ }
+
+ @Override
+ public String getRequestPathInfo() {
+ if (request instanceof ResourceRequest) {
+ ResourceRequest resourceRequest = (ResourceRequest) request;
+ String resourceID = resourceRequest.getResourceID();
+ if (AbstractApplicationPortlet.RESOURCE_URL_ID.equals(resourceID)) {
+ String resourcePath = resourceRequest
+ .getParameter(ApplicationConnection.V_RESOURCE_PATH);
+ return resourcePath;
+ }
+ return resourceID;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getSessionMaxInactiveInterval() {
+ return request.getPortletSession().getMaxInactiveInterval();
+ }
+
+ @Override
+ public Object getSessionAttribute(String name) {
+ return request.getPortletSession().getAttribute(name);
+ }
+
+ @Override
+ public void setSessionAttribute(String name, Object attribute) {
+ request.getPortletSession().setAttribute(name, attribute);
+ }
+
+ /**
+ * Gets the original, unwrapped portlet request.
+ *
+ * @return the unwrapped portlet request
+ */
+ public PortletRequest getPortletRequest() {
+ return request;
+ }
+
+ @Override
+ public String getContentType() {
+ try {
+ return ((ResourceRequest) request).getContentType();
+ } catch (ClassCastException e) {
+ throw new IllegalStateException(
+ "Content type only available for ResourceRequests");
+ }
+ }
+
+ @Override
+ public BrowserDetails getBrowserDetails() {
+ return new BrowserDetails() {
+ @Override
+ public String getUriFragment() {
+ return null;
+ }
+
+ @Override
+ public String getWindowName() {
+ return null;
+ }
+
+ @Override
+ public WebBrowser getWebBrowser() {
+ PortletApplicationContext2 context = (PortletApplicationContext2) Application
+ .getCurrent().getContext();
+ return context.getBrowser();
+ }
+ };
+ }
+
+ @Override
+ public Locale getLocale() {
+ return request.getLocale();
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return null;
+ }
+
+ @Override
+ public boolean isSecure() {
+ return request.isSecure();
+ }
+
+ @Override
+ public String getHeader(String string) {
+ return null;
+ }
+
+ /**
+ * Reads a portal property from the portal context of the wrapped request.
+ *
+ * @param name
+ * a string with the name of the portal property to get
+ * @return a string with the value of the property, or <code>null</code> if
+ * the property is not defined
+ */
+ public String getPortalProperty(String name) {
+ return request.getPortalContext().getProperty(name);
+ }
+
+ @Override
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+
+ /**
+ * Helper method to get a <code>WrappedPortlettRequest</code> from a
+ * <code>WrappedRequest</code>. Aside from casting, this method also takes
+ * care of situations where there's another level of wrapping.
+ *
+ * @param request
+ * a wrapped request
+ * @return a wrapped portlet request
+ * @throws ClassCastException
+ * if the wrapped request doesn't wrap a portlet request
+ */
+ public static WrappedPortletRequest cast(WrappedRequest request) {
+ if (request instanceof CombinedRequest) {
+ CombinedRequest combinedRequest = (CombinedRequest) request;
+ request = combinedRequest.getSecondRequest();
+ }
+ return (WrappedPortletRequest) request;
+ }
+}
diff --git a/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java
new file mode 100644
index 0000000000..f7ecf26f3c
--- /dev/null
+++ b/server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java
@@ -0,0 +1,111 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import javax.portlet.MimeResponse;
+import javax.portlet.PortletResponse;
+import javax.portlet.ResourceResponse;
+
+import com.vaadin.terminal.DeploymentConfiguration;
+import com.vaadin.terminal.WrappedResponse;
+
+/**
+ * Wrapper for {@link PortletResponse} and its subclasses.
+ *
+ * @author Vaadin Ltd.
+ * @since 7.0
+ *
+ * @see WrappedResponse
+ * @see WrappedPortletRequest
+ */
+public class WrappedPortletResponse implements WrappedResponse {
+ private static final DateFormat HTTP_DATE_FORMAT = new SimpleDateFormat(
+ "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
+ static {
+ HTTP_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
+ }
+
+ private final PortletResponse response;
+ private DeploymentConfiguration deploymentConfiguration;
+
+ /**
+ * Wraps a portlet response and an associated deployment configuration
+ *
+ * @param response
+ * the portlet response to wrap
+ * @param deploymentConfiguration
+ * the associated deployment configuration
+ */
+ public WrappedPortletResponse(PortletResponse response,
+ DeploymentConfiguration deploymentConfiguration) {
+ this.response = response;
+ this.deploymentConfiguration = deploymentConfiguration;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return ((MimeResponse) response).getPortletOutputStream();
+ }
+
+ /**
+ * Gets the original, unwrapped portlet response.
+ *
+ * @return the unwrapped portlet response
+ */
+ public PortletResponse getPortletResponse() {
+ return response;
+ }
+
+ @Override
+ public void setContentType(String type) {
+ ((MimeResponse) response).setContentType(type);
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ return ((MimeResponse) response).getWriter();
+ }
+
+ @Override
+ public void setStatus(int responseStatus) {
+ response.setProperty(ResourceResponse.HTTP_STATUS_CODE,
+ Integer.toString(responseStatus));
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ response.setProperty(name, value);
+ }
+
+ @Override
+ public void setDateHeader(String name, long timestamp) {
+ response.setProperty(name, HTTP_DATE_FORMAT.format(new Date(timestamp)));
+ }
+
+ @Override
+ public void setCacheTime(long milliseconds) {
+ WrappedHttpServletResponse.doSetCacheTime(this, milliseconds);
+ }
+
+ @Override
+ public void sendError(int errorCode, String message) throws IOException {
+ setStatus(errorCode);
+ getWriter().write(message);
+ }
+
+ @Override
+ public DeploymentConfiguration getDeploymentConfiguration() {
+ return deploymentConfiguration;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/terminal/package.html b/server/src/com/vaadin/terminal/package.html
new file mode 100644
index 0000000000..83514a0de5
--- /dev/null
+++ b/server/src/com/vaadin/terminal/package.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides classes and interfaces that wrap the terminal-side functionalities
+for the server-side application. (FIXME: This could be a little more descriptive and wordy.)</p>
+
+<h2>Package Specification</h2>
+
+<!-- Package spec here -->
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/tools/ReflectTools.java b/server/src/com/vaadin/tools/ReflectTools.java
new file mode 100644
index 0000000000..ea2afae301
--- /dev/null
+++ b/server/src/com/vaadin/tools/ReflectTools.java
@@ -0,0 +1,126 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.tools;
+
+import java.beans.IntrospectionException;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An util class with helpers for reflection operations. Used internally by
+ * Vaadin and should not be used by application developers. Subject to change at
+ * any time.
+ *
+ * @since 6.2
+ */
+public class ReflectTools {
+ /**
+ * Locates the method in the given class. Returns null if the method is not
+ * found. Throws an ExceptionInInitializerError if there is a problem
+ * locating the method as this is mainly called from static blocks.
+ *
+ * @param cls
+ * Class that contains the method
+ * @param methodName
+ * The name of the method
+ * @param parameterTypes
+ * The parameter types for the method.
+ * @return A reference to the method
+ * @throws ExceptionInInitializerError
+ * Wraps any exception in an {@link ExceptionInInitializerError}
+ * so this method can be called from a static initializer.
+ */
+ public static Method findMethod(Class<?> cls, String methodName,
+ Class<?>... parameterTypes) throws ExceptionInInitializerError {
+ try {
+ return cls.getDeclaredMethod(methodName, parameterTypes);
+ } catch (Exception e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ /**
+ * Returns the value of the java field.
+ * <p>
+ * Uses getter if present, otherwise tries to access even private fields
+ * directly.
+ *
+ * @param object
+ * The object containing the field
+ * @param field
+ * The field we want to get the value for
+ * @return The value of the field in the object
+ * @throws InvocationTargetException
+ * If the value could not be retrieved
+ * @throws IllegalAccessException
+ * If the value could not be retrieved
+ * @throws IllegalArgumentException
+ * If the value could not be retrieved
+ */
+ public static Object getJavaFieldValue(Object object,
+ java.lang.reflect.Field field) throws IllegalArgumentException,
+ IllegalAccessException, InvocationTargetException {
+ PropertyDescriptor pd;
+ try {
+ pd = new PropertyDescriptor(field.getName(), object.getClass());
+ Method getter = pd.getReadMethod();
+ if (getter != null) {
+ return getter.invoke(object, (Object[]) null);
+ }
+ } catch (IntrospectionException e1) {
+ // Ignore this and try to get directly using the field
+ }
+
+ // Try to get the value or throw an exception
+ if (!field.isAccessible()) {
+ // Try to gain access even if field is private
+ field.setAccessible(true);
+ }
+ return field.get(object);
+ }
+
+ /**
+ * Sets the value of a java field.
+ * <p>
+ * Uses setter if present, otherwise tries to access even private fields
+ * directly.
+ *
+ * @param object
+ * The object containing the field
+ * @param field
+ * The field we want to set the value for
+ * @param value
+ * The value to set
+ * @throws IllegalAccessException
+ * If the value could not be assigned to the field
+ * @throws IllegalArgumentException
+ * If the value could not be assigned to the field
+ * @throws InvocationTargetException
+ * If the value could not be assigned to the field
+ */
+ public static void setJavaFieldValue(Object object,
+ java.lang.reflect.Field field, Object value)
+ throws IllegalAccessException, IllegalArgumentException,
+ InvocationTargetException {
+ PropertyDescriptor pd;
+ try {
+ pd = new PropertyDescriptor(field.getName(), object.getClass());
+ Method setter = pd.getWriteMethod();
+ if (setter != null) {
+ // Exceptions are thrown forward if this fails
+ setter.invoke(object, value);
+ }
+ } catch (IntrospectionException e1) {
+ // Ignore this and try to set directly using the field
+ }
+
+ // Try to set the value directly to the field or throw an exception
+ if (!field.isAccessible()) {
+ // Try to gain access even if field is private
+ field.setAccessible(true);
+ }
+ field.set(object, value);
+ }
+}
diff --git a/server/src/com/vaadin/tools/WidgetsetCompiler.java b/server/src/com/vaadin/tools/WidgetsetCompiler.java
new file mode 100644
index 0000000000..ecc1946e60
--- /dev/null
+++ b/server/src/com/vaadin/tools/WidgetsetCompiler.java
@@ -0,0 +1,94 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.tools;
+
+import java.lang.reflect.Method;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.terminal.gwt.widgetsetutils.WidgetSetBuilder;
+
+/**
+ * A wrapper for the GWT 1.6 compiler that runs the compiler in a new thread.
+ *
+ * This allows circumventing a J2SE 5.0 bug (6316197) that prevents setting the
+ * stack size for the main thread. Thus, larger widgetsets can be compiled.
+ *
+ * This class takes the same command line arguments as the
+ * com.google.gwt.dev.GWTCompiler class. The old and deprecated compiler is used
+ * for compatibility with GWT 1.5.
+ *
+ * A typical invocation would use e.g. the following arguments
+ *
+ * "-out WebContent/VAADIN/widgetsets com.vaadin.terminal.gwt.DefaultWidgetSet"
+ *
+ * In addition, larger memory usage settings for the VM should be used, e.g.
+ *
+ * "-Xms256M -Xmx512M -Xss8M"
+ *
+ * The source directory containing widgetset and related classes must be
+ * included in the classpath, as well as the gwt-dev-[platform].jar and other
+ * relevant JARs.
+ *
+ * @deprecated with Java 6, can use com.google.gwt.dev.Compiler directly (also
+ * in Eclipse plug-in etc.)
+ */
+@Deprecated
+public class WidgetsetCompiler {
+
+ /**
+ * @param args
+ * same arguments as for com.google.gwt.dev.Compiler
+ */
+ public static void main(final String[] args) {
+ try {
+ // run the compiler in a different thread to enable using the
+ // user-set stack size
+
+ // on Windows, the default stack size is too small for the main
+ // thread and cannot be changed in JRE 1.5 (see
+ // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6316197)
+
+ Runnable runCompiler = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // GWTCompiler.main(args);
+ // avoid warnings
+
+ String wsname = args[args.length - 1];
+
+ // TODO expecting this is launched via eclipse WTP
+ // project
+ System.out
+ .println("Updating GWT module description file...");
+ WidgetSetBuilder.updateWidgetSet(wsname);
+ System.out.println("Done.");
+
+ System.out.println("Starting GWT compiler");
+ System.setProperty("gwt.nowarn.legacy.tools", "true");
+ Class<?> compilerClass = Class
+ .forName("com.google.gwt.dev.GWTCompiler");
+ Method method = compilerClass.getDeclaredMethod("main",
+ String[].class);
+ method.invoke(null, new Object[] { args });
+ } catch (Throwable thr) {
+ getLogger().log(Level.SEVERE,
+ "Widgetset compilation failed", thr);
+ }
+ }
+ };
+ Thread runThread = new Thread(runCompiler);
+ runThread.start();
+ runThread.join();
+ System.out.println("Widgetset compilation finished");
+ } catch (Throwable thr) {
+ getLogger().log(Level.SEVERE, "Widgetset compilation failed", thr);
+ }
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(WidgetsetCompiler.class.getName());
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbsoluteLayout.java b/server/src/com/vaadin/ui/AbsoluteLayout.java
new file mode 100644
index 0000000000..1c84ca2865
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbsoluteLayout.java
@@ -0,0 +1,632 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc;
+import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState;
+import com.vaadin.terminal.Sizeable;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+
+/**
+ * AbsoluteLayout is a layout implementation that mimics html absolute
+ * positioning.
+ *
+ */
+@SuppressWarnings("serial")
+public class AbsoluteLayout extends AbstractLayout implements
+ LayoutClickNotifier {
+
+ private AbsoluteLayoutServerRpc rpc = new AbsoluteLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(AbsoluteLayout.this,
+ mouseDetails, clickedConnector));
+ }
+ };
+ // Maps each component to a position
+ private LinkedHashMap<Component, ComponentPosition> componentToCoordinates = new LinkedHashMap<Component, ComponentPosition>();
+
+ /**
+ * Creates an AbsoluteLayout with full size.
+ */
+ public AbsoluteLayout() {
+ registerRpc(rpc);
+ setSizeFull();
+ }
+
+ @Override
+ public AbsoluteLayoutState getState() {
+ return (AbsoluteLayoutState) super.getState();
+ }
+
+ /**
+ * Gets an iterator for going through all components enclosed in the
+ * absolute layout.
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return componentToCoordinates.keySet().iterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return componentToCoordinates.size();
+ }
+
+ /**
+ * Replaces one component with another one. The new component inherits the
+ * old components position.
+ */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+ ComponentPosition position = getPosition(oldComponent);
+ removeComponent(oldComponent);
+ addComponent(newComponent, position);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component
+ * )
+ */
+ @Override
+ public void addComponent(Component c) {
+ addComponent(c, new ComponentPosition());
+ }
+
+ /**
+ * Adds a component to the layout. The component can be positioned by
+ * providing a string formatted in CSS-format.
+ * <p>
+ * For example the string "top:10px;left:10px" will position the component
+ * 10 pixels from the left and 10 pixels from the top. The identifiers:
+ * "top","left","right" and "bottom" can be used to specify the position.
+ * </p>
+ *
+ * @param c
+ * The component to add to the layout
+ * @param cssPosition
+ * The css position string
+ */
+ public void addComponent(Component c, String cssPosition) {
+ ComponentPosition position = new ComponentPosition();
+ position.setCSSString(cssPosition);
+ addComponent(c, position);
+ }
+
+ /**
+ * Adds the component using the given position. Ensures the position is only
+ * set if the component is added correctly.
+ *
+ * @param c
+ * The component to add
+ * @param position
+ * The position info for the component. Must not be null.
+ * @throws IllegalArgumentException
+ * If adding the component failed
+ */
+ private void addComponent(Component c, ComponentPosition position)
+ throws IllegalArgumentException {
+ /*
+ * Create position instance and add it to componentToCoordinates map. We
+ * need to do this before we call addComponent so the attachListeners
+ * can access this position. #6368
+ */
+ internalSetPosition(c, position);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ internalRemoveComponent(c);
+ throw e;
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Removes the component from all internal data structures. Does not
+ * actually remove the component from the layout (this is assumed to have
+ * been done by the caller).
+ *
+ * @param c
+ * The component to remove
+ */
+ private void internalRemoveComponent(Component c) {
+ componentToCoordinates.remove(c);
+ }
+
+ @Override
+ public void updateState() {
+ super.updateState();
+
+ // This could be in internalRemoveComponent and internalSetComponent if
+ // Map<Connector,String> was supported. We cannot get the child
+ // connectorId unless the component is attached to the application so
+ // the String->String map cannot be populated in internal* either.
+ Map<String, String> connectorToPosition = new HashMap<String, String>();
+ for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) {
+ Component c = ci.next();
+ connectorToPosition.put(c.getConnectorId(), getPosition(c)
+ .getCSSString());
+ }
+ getState().setConnectorToCssPosition(connectorToPosition);
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui
+ * .Component)
+ */
+ @Override
+ public void removeComponent(Component c) {
+ internalRemoveComponent(c);
+ super.removeComponent(c);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the position of a component in the layout. Returns null if component
+ * is not attached to the layout.
+ * <p>
+ * Note that you cannot update the position by updating this object. Call
+ * {@link #setPosition(Component, ComponentPosition)} with the updated
+ * {@link ComponentPosition} object.
+ * </p>
+ *
+ * @param component
+ * The component which position is needed
+ * @return An instance of ComponentPosition containing the position of the
+ * component, or null if the component is not enclosed in the
+ * layout.
+ */
+ public ComponentPosition getPosition(Component component) {
+ return componentToCoordinates.get(component);
+ }
+
+ /**
+ * Sets the position of a component in the layout.
+ *
+ * @param component
+ * @param position
+ */
+ public void setPosition(Component component, ComponentPosition position) {
+ if (!componentToCoordinates.containsKey(component)) {
+ throw new IllegalArgumentException(
+ "Component must be a child of this layout");
+ }
+ internalSetPosition(component, position);
+ }
+
+ /**
+ * Updates the position for a component. Caller must ensure component is a
+ * child of this layout.
+ *
+ * @param component
+ * The component. Must be a child for this layout. Not enforced.
+ * @param position
+ * New position. Must not be null.
+ */
+ private void internalSetPosition(Component component,
+ ComponentPosition position) {
+ componentToCoordinates.put(component, position);
+ requestRepaint();
+ }
+
+ /**
+ * The CompontPosition class represents a components position within the
+ * absolute layout. It contains the attributes for left, right, top and
+ * bottom and the units used to specify them.
+ */
+ public class ComponentPosition implements Serializable {
+
+ private int zIndex = -1;
+ private Float topValue = null;
+ private Float rightValue = null;
+ private Float bottomValue = null;
+ private Float leftValue = null;
+
+ private Unit topUnits = Unit.PIXELS;
+ private Unit rightUnits = Unit.PIXELS;
+ private Unit bottomUnits = Unit.PIXELS;
+ private Unit leftUnits = Unit.PIXELS;
+
+ /**
+ * Sets the position attributes using CSS syntax. Attributes not
+ * included in the string are reset to their unset states.
+ *
+ * <code><pre>
+ * setCSSString("top:10px;left:20%;z-index:16;");
+ * </pre></code>
+ *
+ * @param css
+ */
+ public void setCSSString(String css) {
+ topValue = rightValue = bottomValue = leftValue = null;
+ topUnits = rightUnits = bottomUnits = leftUnits = Unit.PIXELS;
+ zIndex = -1;
+ if (css == null) {
+ return;
+ }
+
+ String[] cssProperties = css.split(";");
+ for (int i = 0; i < cssProperties.length; i++) {
+ String[] keyValuePair = cssProperties[i].split(":");
+ String key = keyValuePair[0].trim();
+ if (key.equals("")) {
+ continue;
+ }
+ if (key.equals("z-index")) {
+ zIndex = Integer.parseInt(keyValuePair[1].trim());
+ } else {
+ String value;
+ if (keyValuePair.length > 1) {
+ value = keyValuePair[1].trim();
+ } else {
+ value = "";
+ }
+ String symbol = value.replaceAll("[0-9\\.\\-]+", "");
+ if (!symbol.equals("")) {
+ value = value.substring(0, value.indexOf(symbol))
+ .trim();
+ }
+ float v = Float.parseFloat(value);
+ Unit unit = Unit.getUnitFromSymbol(symbol);
+ if (key.equals("top")) {
+ topValue = v;
+ topUnits = unit;
+ } else if (key.equals("right")) {
+ rightValue = v;
+ rightUnits = unit;
+ } else if (key.equals("bottom")) {
+ bottomValue = v;
+ bottomUnits = unit;
+ } else if (key.equals("left")) {
+ leftValue = v;
+ leftUnits = unit;
+ }
+ }
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Converts the internal values into a valid CSS string.
+ *
+ * @return A valid CSS string
+ */
+ public String getCSSString() {
+ String s = "";
+ if (topValue != null) {
+ s += "top:" + topValue + topUnits.getSymbol() + ";";
+ }
+ if (rightValue != null) {
+ s += "right:" + rightValue + rightUnits.getSymbol() + ";";
+ }
+ if (bottomValue != null) {
+ s += "bottom:" + bottomValue + bottomUnits.getSymbol() + ";";
+ }
+ if (leftValue != null) {
+ s += "left:" + leftValue + leftUnits.getSymbol() + ";";
+ }
+ if (zIndex >= 0) {
+ s += "z-index:" + zIndex + ";";
+ }
+ return s;
+ }
+
+ /**
+ * Sets the 'top' attribute; distance from the top of the component to
+ * the top edge of the layout.
+ *
+ * @param topValue
+ * The value of the 'top' attribute
+ * @param topUnits
+ * The unit of the 'top' attribute. See UNIT_SYMBOLS for a
+ * description of the available units.
+ */
+ public void setTop(Float topValue, Unit topUnits) {
+ this.topValue = topValue;
+ this.topUnits = topUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the 'right' attribute; distance from the right of the component
+ * to the right edge of the layout.
+ *
+ * @param rightValue
+ * The value of the 'right' attribute
+ * @param rightUnits
+ * The unit of the 'right' attribute. See UNIT_SYMBOLS for a
+ * description of the available units.
+ */
+ public void setRight(Float rightValue, Unit rightUnits) {
+ this.rightValue = rightValue;
+ this.rightUnits = rightUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the 'bottom' attribute; distance from the bottom of the
+ * component to the bottom edge of the layout.
+ *
+ * @param bottomValue
+ * The value of the 'bottom' attribute
+ * @param units
+ * The unit of the 'bottom' attribute. See UNIT_SYMBOLS for a
+ * description of the available units.
+ */
+ public void setBottom(Float bottomValue, Unit bottomUnits) {
+ this.bottomValue = bottomValue;
+ this.bottomUnits = bottomUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the 'left' attribute; distance from the left of the component to
+ * the left edge of the layout.
+ *
+ * @param leftValue
+ * The value of the 'left' attribute
+ * @param units
+ * The unit of the 'left' attribute. See UNIT_SYMBOLS for a
+ * description of the available units.
+ */
+ public void setLeft(Float leftValue, Unit leftUnits) {
+ this.leftValue = leftValue;
+ this.leftUnits = leftUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the 'z-index' attribute; the visual stacking order
+ *
+ * @param zIndex
+ * The z-index for the component.
+ */
+ public void setZIndex(int zIndex) {
+ this.zIndex = zIndex;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the value of the 'top' attribute; distance from the top of the
+ * component to the top edge of the layout.
+ *
+ * @param topValue
+ * The value of the 'left' attribute
+ */
+ public void setTopValue(Float topValue) {
+ this.topValue = topValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the 'top' attributes value in current units.
+ *
+ * @see #getTopUnits()
+ * @return The value of the 'top' attribute, null if not set
+ */
+ public Float getTopValue() {
+ return topValue;
+ }
+
+ /**
+ * Gets the 'right' attributes value in current units.
+ *
+ * @return The value of the 'right' attribute, null if not set
+ * @see #getRightUnits()
+ */
+ public Float getRightValue() {
+ return rightValue;
+ }
+
+ /**
+ * Sets the 'right' attribute value (distance from the right of the
+ * component to the right edge of the layout). Currently active units
+ * are maintained.
+ *
+ * @param rightValue
+ * The value of the 'right' attribute
+ * @see #setRightUnits(int)
+ */
+ public void setRightValue(Float rightValue) {
+ this.rightValue = rightValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the 'bottom' attributes value using current units.
+ *
+ * @return The value of the 'bottom' attribute, null if not set
+ * @see #getBottomUnits()
+ */
+ public Float getBottomValue() {
+ return bottomValue;
+ }
+
+ /**
+ * Sets the 'bottom' attribute value (distance from the bottom of the
+ * component to the bottom edge of the layout). Currently active units
+ * are maintained.
+ *
+ * @param bottomValue
+ * The value of the 'bottom' attribute
+ * @see #setBottomUnits(int)
+ */
+ public void setBottomValue(Float bottomValue) {
+ this.bottomValue = bottomValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the 'left' attributes value using current units.
+ *
+ * @return The value of the 'left' attribute, null if not set
+ * @see #getLeftUnits()
+ */
+ public Float getLeftValue() {
+ return leftValue;
+ }
+
+ /**
+ * Sets the 'left' attribute value (distance from the left of the
+ * component to the left edge of the layout). Currently active units are
+ * maintained.
+ *
+ * @param leftValue
+ * The value of the 'left' CSS-attribute
+ * @see #setLeftUnits(int)
+ */
+ public void setLeftValue(Float leftValue) {
+ this.leftValue = leftValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the unit for the 'top' attribute
+ *
+ * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public Unit getTopUnits() {
+ return topUnits;
+ }
+
+ /**
+ * Sets the unit for the 'top' attribute
+ *
+ * @param topUnits
+ * See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public void setTopUnits(Unit topUnits) {
+ this.topUnits = topUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the unit for the 'right' attribute
+ *
+ * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public Unit getRightUnits() {
+ return rightUnits;
+ }
+
+ /**
+ * Sets the unit for the 'right' attribute
+ *
+ * @param rightUnits
+ * See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public void setRightUnits(Unit rightUnits) {
+ this.rightUnits = rightUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the unit for the 'bottom' attribute
+ *
+ * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public Unit getBottomUnits() {
+ return bottomUnits;
+ }
+
+ /**
+ * Sets the unit for the 'bottom' attribute
+ *
+ * @param bottomUnits
+ * See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public void setBottomUnits(Unit bottomUnits) {
+ this.bottomUnits = bottomUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the unit for the 'left' attribute
+ *
+ * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public Unit getLeftUnits() {
+ return leftUnits;
+ }
+
+ /**
+ * Sets the unit for the 'left' attribute
+ *
+ * @param leftUnits
+ * See {@link Sizeable} UNIT_SYMBOLS for a description of the
+ * available units.
+ */
+ public void setLeftUnits(Unit leftUnits) {
+ this.leftUnits = leftUnits;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the 'z-index' attribute.
+ *
+ * @return the zIndex The z-index attribute
+ */
+ public int getZIndex() {
+ return zIndex;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return getCSSString();
+ }
+
+ }
+
+ @Override
+ public void addListener(LayoutClickListener listener) {
+ addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ @Override
+ public void removeListener(LayoutClickListener listener) {
+ removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java
new file mode 100644
index 0000000000..e7cb38256c
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractComponent.java
@@ -0,0 +1,1382 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.vaadin.Application;
+import com.vaadin.event.ActionManager;
+import com.vaadin.event.EventRouter;
+import com.vaadin.event.MethodEventSource;
+import com.vaadin.event.ShortcutListener;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.terminal.AbstractClientConnector;
+import com.vaadin.terminal.ErrorMessage;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+import com.vaadin.terminal.gwt.server.ComponentSizeValidator;
+import com.vaadin.terminal.gwt.server.ResourceReference;
+import com.vaadin.tools.ReflectTools;
+
+/**
+ * An abstract class that defines default implementation for the
+ * {@link Component} interface. Basic UI components that are not derived from an
+ * external component can inherit this class to easily qualify as Vaadin
+ * components. Most components in Vaadin do just that.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractComponent extends AbstractClientConnector
+ implements Component, MethodEventSource {
+
+ /* Private members */
+
+ /**
+ * Application specific data object. The component does not use or modify
+ * this.
+ */
+ private Object applicationData;
+
+ /**
+ * The EventRouter used for the event model.
+ */
+ private EventRouter eventRouter = null;
+
+ /**
+ * The internal error message of the component.
+ */
+ private ErrorMessage componentError = null;
+
+ /**
+ * Locale of this component.
+ */
+ private Locale locale;
+
+ /**
+ * The component should receive focus (if {@link Focusable}) when attached.
+ */
+ private boolean delayedFocus;
+
+ /* Sizeable fields */
+
+ private float width = SIZE_UNDEFINED;
+ private float height = SIZE_UNDEFINED;
+ private Unit widthUnit = Unit.PIXELS;
+ private Unit heightUnit = Unit.PIXELS;
+ private static final Pattern sizePattern = Pattern
+ .compile("^(-?\\d+(\\.\\d+)?)(%|px|em|ex|in|cm|mm|pt|pc)?$");
+
+ private ComponentErrorHandler errorHandler = null;
+
+ /**
+ * Keeps track of the Actions added to this component; the actual
+ * handling/notifying is delegated, usually to the containing window.
+ */
+ private ActionManager actionManager;
+
+ /* Constructor */
+
+ /**
+ * Constructs a new Component.
+ */
+ public AbstractComponent() {
+ // ComponentSizeValidator.setCreationLocation(this);
+ }
+
+ /* Get/Set component properties */
+
+ @Override
+ public void setDebugId(String id) {
+ getState().setDebugId(id);
+ }
+
+ @Override
+ public String getDebugId() {
+ return getState().getDebugId();
+ }
+
+ /**
+ * Gets style for component. Multiple styles are joined with spaces.
+ *
+ * @return the component's styleValue of property style.
+ * @deprecated Use getStyleName() instead; renamed for consistency and to
+ * indicate that "style" should not be used to switch client
+ * side implementation, only to style the component.
+ */
+ @Deprecated
+ public String getStyle() {
+ return getStyleName();
+ }
+
+ /**
+ * Sets and replaces all previous style names of the component. This method
+ * will trigger a {@link RepaintRequestEvent}.
+ *
+ * @param style
+ * the new style of the component.
+ * @deprecated Use setStyleName() instead; renamed for consistency and to
+ * indicate that "style" should not be used to switch client
+ * side implementation, only to style the component.
+ */
+ @Deprecated
+ public void setStyle(String style) {
+ setStyleName(style);
+ }
+
+ /*
+ * Gets the component's style. Don't add a JavaDoc comment here, we use the
+ * default documentation from implemented interface.
+ */
+ @Override
+ public String getStyleName() {
+ String s = "";
+ if (getState().getStyles() != null) {
+ for (final Iterator<String> it = getState().getStyles().iterator(); it
+ .hasNext();) {
+ s += it.next();
+ if (it.hasNext()) {
+ s += " ";
+ }
+ }
+ }
+ return s;
+ }
+
+ /*
+ * Sets the component's style. Don't add a JavaDoc comment here, we use the
+ * default documentation from implemented interface.
+ */
+ @Override
+ public void setStyleName(String style) {
+ if (style == null || "".equals(style)) {
+ getState().setStyles(null);
+ requestRepaint();
+ return;
+ }
+ if (getState().getStyles() == null) {
+ getState().setStyles(new ArrayList<String>());
+ }
+ List<String> styles = getState().getStyles();
+ styles.clear();
+ String[] styleParts = style.split(" +");
+ for (String part : styleParts) {
+ if (part.length() > 0) {
+ styles.add(part);
+ }
+ }
+ requestRepaint();
+ }
+
+ @Override
+ public void addStyleName(String style) {
+ if (style == null || "".equals(style)) {
+ return;
+ }
+ if (style.contains(" ")) {
+ // Split space separated style names and add them one by one.
+ for (String realStyle : style.split(" ")) {
+ addStyleName(realStyle);
+ }
+ return;
+ }
+
+ if (getState().getStyles() == null) {
+ getState().setStyles(new ArrayList<String>());
+ }
+ List<String> styles = getState().getStyles();
+ if (!styles.contains(style)) {
+ styles.add(style);
+ requestRepaint();
+ }
+ }
+
+ @Override
+ public void removeStyleName(String style) {
+ if (getState().getStyles() != null) {
+ String[] styleParts = style.split(" +");
+ for (String part : styleParts) {
+ if (part.length() > 0) {
+ getState().getStyles().remove(part);
+ }
+ }
+ requestRepaint();
+ }
+ }
+
+ /*
+ * Get's the component's caption. Don't add a JavaDoc comment here, we use
+ * the default documentation from implemented interface.
+ */
+ @Override
+ public String getCaption() {
+ return getState().getCaption();
+ }
+
+ /**
+ * Sets the component's caption <code>String</code>. Caption is the visible
+ * name of the component. This method will trigger a
+ * {@link RepaintRequestEvent}.
+ *
+ * @param caption
+ * the new caption <code>String</code> for the component.
+ */
+ @Override
+ public void setCaption(String caption) {
+ getState().setCaption(caption);
+ requestRepaint();
+ }
+
+ /*
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public Locale getLocale() {
+ if (locale != null) {
+ return locale;
+ }
+ HasComponents parent = getParent();
+ if (parent != null) {
+ return parent.getLocale();
+ }
+ final Application app = getApplication();
+ if (app != null) {
+ return app.getLocale();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the locale of this component.
+ *
+ * <pre>
+ * // Component for which the locale is meaningful
+ * InlineDateField date = new InlineDateField(&quot;Datum&quot;);
+ *
+ * // German language specified with ISO 639-1 language
+ * // code and ISO 3166-1 alpha-2 country code.
+ * date.setLocale(new Locale(&quot;de&quot;, &quot;DE&quot;));
+ *
+ * date.setResolution(DateField.RESOLUTION_DAY);
+ * layout.addComponent(date);
+ * </pre>
+ *
+ *
+ * @param locale
+ * the locale to become this component's locale.
+ */
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+
+ // FIXME: Reload value if there is a converter
+ requestRepaint();
+ }
+
+ /*
+ * Gets the component's icon resource. Don't add a JavaDoc comment here, we
+ * use the default documentation from implemented interface.
+ */
+ @Override
+ public Resource getIcon() {
+ return ResourceReference.getResource(getState().getIcon());
+ }
+
+ /**
+ * Sets the component's icon. This method will trigger a
+ * {@link RepaintRequestEvent}.
+ *
+ * @param icon
+ * the icon to be shown with the component's caption.
+ */
+ @Override
+ public void setIcon(Resource icon) {
+ getState().setIcon(ResourceReference.create(icon));
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#isEnabled()
+ */
+ @Override
+ public boolean isEnabled() {
+ return getState().isEnabled();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#setEnabled(boolean)
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (getState().isEnabled() != enabled) {
+ getState().setEnabled(enabled);
+ requestRepaint();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Connector#isConnectorEnabled()
+ */
+ @Override
+ public boolean isConnectorEnabled() {
+ if (!isVisible()) {
+ return false;
+ } else if (!isEnabled()) {
+ return false;
+ } else if (!super.isConnectorEnabled()) {
+ return false;
+ } else if (!getParent().isComponentVisible(this)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /*
+ * Tests if the component is in the immediate mode. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ public boolean isImmediate() {
+ return getState().isImmediate();
+ }
+
+ /**
+ * Sets the component's immediate mode to the specified status. This method
+ * will trigger a {@link RepaintRequestEvent}.
+ *
+ * @param immediate
+ * the boolean value specifying if the component should be in the
+ * immediate mode after the call.
+ * @see Component#isImmediate()
+ */
+ public void setImmediate(boolean immediate) {
+ getState().setImmediate(immediate);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#isVisible()
+ */
+ @Override
+ public boolean isVisible() {
+ return getState().isVisible();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#setVisible(boolean)
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ if (getState().isVisible() == visible) {
+ return;
+ }
+
+ getState().setVisible(visible);
+ requestRepaint();
+ if (getParent() != null) {
+ // Must always repaint the parent (at least the hierarchy) when
+ // visibility of a child component changes.
+ getParent().requestRepaint();
+ }
+ }
+
+ /**
+ * <p>
+ * Gets the component's description, used in tooltips and can be displayed
+ * directly in certain other components such as forms. The description can
+ * be used to briefly describe the state of the component to the user. The
+ * description string may contain certain XML tags:
+ * </p>
+ *
+ * <p>
+ * <table border=1>
+ * <tr>
+ * <td width=120><b>Tag</b></td>
+ * <td width=120><b>Description</b></td>
+ * <td width=120><b>Example</b></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;b></td>
+ * <td>bold</td>
+ * <td><b>bold text</b></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;i></td>
+ * <td>italic</td>
+ * <td><i>italic text</i></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;u></td>
+ * <td>underlined</td>
+ * <td><u>underlined text</u></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;br></td>
+ * <td>linebreak</td>
+ * <td>N/A</td>
+ * </tr>
+ * <tr>
+ * <td>&lt;ul><br>
+ * &lt;li>item1<br>
+ * &lt;li>item1<br>
+ * &lt;/ul></td>
+ * <td>item list</td>
+ * <td>
+ * <ul>
+ * <li>item1
+ * <li>item2
+ * </ul>
+ * </td>
+ * </tr>
+ * </table>
+ * </p>
+ *
+ * <p>
+ * These tags may be nested.
+ * </p>
+ *
+ * @return component's description <code>String</code>
+ */
+ public String getDescription() {
+ return getState().getDescription();
+ }
+
+ /**
+ * Sets the component's description. See {@link #getDescription()} for more
+ * information on what the description is. This method will trigger a
+ * {@link RepaintRequestEvent}.
+ *
+ * The description is displayed as HTML/XHTML in tooltips or directly in
+ * certain components so care should be taken to avoid creating the
+ * possibility for HTML injection and possibly XSS vulnerabilities.
+ *
+ * @param description
+ * the new description string for the component.
+ */
+ public void setDescription(String description) {
+ getState().setDescription(description);
+ requestRepaint();
+ }
+
+ /*
+ * Gets the component's parent component. Don't add a JavaDoc comment here,
+ * we use the default documentation from implemented interface.
+ */
+ @Override
+ public HasComponents getParent() {
+ return (HasComponents) super.getParent();
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ if (parent == null || parent instanceof HasComponents) {
+ super.setParent(parent);
+ } else {
+ throw new IllegalArgumentException(
+ "The parent of a Component must implement HasComponents, which "
+ + parent.getClass() + " doesn't do.");
+ }
+ }
+
+ /**
+ * Returns the closest ancestor with the given type.
+ * <p>
+ * To find the Window that contains the component, use {@code Window w =
+ * getParent(Window.class);}
+ * </p>
+ *
+ * @param <T>
+ * The type of the ancestor
+ * @param parentType
+ * The ancestor class we are looking for
+ * @return The first ancestor that can be assigned to the given class. Null
+ * if no ancestor with the correct type could be found.
+ */
+ public <T extends HasComponents> T findAncestor(Class<T> parentType) {
+ HasComponents p = getParent();
+ while (p != null) {
+ if (parentType.isAssignableFrom(p.getClass())) {
+ return parentType.cast(p);
+ }
+ p = p.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the error message for this component.
+ *
+ * @return ErrorMessage containing the description of the error state of the
+ * component or null, if the component contains no errors. Extending
+ * classes should override this method if they support other error
+ * message types such as validation errors or buffering errors. The
+ * returned error message contains information about all the errors.
+ */
+ public ErrorMessage getErrorMessage() {
+ return componentError;
+ }
+
+ /**
+ * Gets the component's error message.
+ *
+ * @link Terminal.ErrorMessage#ErrorMessage(String, int)
+ *
+ * @return the component's error message.
+ */
+ public ErrorMessage getComponentError() {
+ return componentError;
+ }
+
+ /**
+ * Sets the component's error message. The message may contain certain XML
+ * tags, for more information see
+ *
+ * @link Component.ErrorMessage#ErrorMessage(String, int)
+ *
+ * @param componentError
+ * the new <code>ErrorMessage</code> of the component.
+ */
+ public void setComponentError(ErrorMessage componentError) {
+ this.componentError = componentError;
+ fireComponentErrorEvent();
+ requestRepaint();
+ }
+
+ /*
+ * Tests if the component is in read-only mode. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return getState().isReadOnly();
+ }
+
+ /*
+ * Sets the component's read-only mode. Don't add a JavaDoc comment here, we
+ * use the default documentation from implemented interface.
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ getState().setReadOnly(readOnly);
+ requestRepaint();
+ }
+
+ /*
+ * Gets the parent window of the component. Don't add a JavaDoc comment
+ * here, we use the default documentation from implemented interface.
+ */
+ @Override
+ public Root getRoot() {
+ // Just make method from implemented Component interface public
+ return super.getRoot();
+ }
+
+ /*
+ * Notify the component that it's attached to a window. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void attach() {
+ super.attach();
+ if (delayedFocus) {
+ focus();
+ }
+ setActionManagerViewer();
+ }
+
+ /*
+ * Detach the component from application. Don't add a JavaDoc comment here,
+ * we use the default documentation from implemented interface.
+ */
+ @Override
+ public void detach() {
+ super.detach();
+ if (actionManager != null) {
+ // Remove any existing viewer. Root cast is just to make the
+ // compiler happy
+ actionManager.setViewer((Root) null);
+ }
+ }
+
+ /**
+ * Sets the focus for this component if the component is {@link Focusable}.
+ */
+ protected void focus() {
+ if (this instanceof Focusable) {
+ final Application app = getApplication();
+ if (app != null) {
+ getRoot().setFocusedComponent((Focusable) this);
+ delayedFocus = false;
+ } else {
+ delayedFocus = true;
+ }
+ }
+ }
+
+ /**
+ * Gets the application object to which the component is attached.
+ *
+ * <p>
+ * The method will return {@code null} if the component is not currently
+ * attached to an application. This is often a problem in constructors of
+ * regular components and in the initializers of custom composite
+ * components. A standard workaround is to move the problematic
+ * initialization to {@link #attach()}, as described in the documentation of
+ * the method.
+ * </p>
+ * <p>
+ * <b>This method is not meant to be overridden. Due to CDI requirements we
+ * cannot declare it as final even though it should be final.</b>
+ * </p>
+ *
+ * @return the parent application of the component or <code>null</code>.
+ * @see #attach()
+ */
+ @Override
+ public Application getApplication() {
+ // Just make method inherited from Component interface public
+ return super.getApplication();
+ }
+
+ /**
+ * Build CSS compatible string representation of height.
+ *
+ * @return CSS height
+ */
+ private String getCSSHeight() {
+ if (getHeightUnits() == Unit.PIXELS) {
+ return ((int) getHeight()) + getHeightUnits().getSymbol();
+ } else {
+ return getHeight() + getHeightUnits().getSymbol();
+ }
+ }
+
+ /**
+ * Build CSS compatible string representation of width.
+ *
+ * @return CSS width
+ */
+ private String getCSSWidth() {
+ if (getWidthUnits() == Unit.PIXELS) {
+ return ((int) getWidth()) + getWidthUnits().getSymbol();
+ } else {
+ return getWidth() + getWidthUnits().getSymbol();
+ }
+ }
+
+ /**
+ * Returns the shared state bean with information to be sent from the server
+ * to the client.
+ *
+ * Subclasses should override this method and set any relevant fields of the
+ * state returned by super.getState().
+ *
+ * @since 7.0
+ *
+ * @return updated component shared state
+ */
+ @Override
+ public ComponentState getState() {
+ return (ComponentState) super.getState();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component#updateState()
+ */
+ @Override
+ public void updateState() {
+ // TODO This logic should be on the client side and the state should
+ // simply be a data object with "width" and "height".
+ if (getHeight() >= 0
+ && (getHeightUnits() != Unit.PERCENTAGE || ComponentSizeValidator
+ .parentCanDefineHeight(this))) {
+ getState().setHeight("" + getCSSHeight());
+ } else {
+ getState().setHeight("");
+ }
+
+ if (getWidth() >= 0
+ && (getWidthUnits() != Unit.PERCENTAGE || ComponentSizeValidator
+ .parentCanDefineWidth(this))) {
+ getState().setWidth("" + getCSSWidth());
+ } else {
+ getState().setWidth("");
+ }
+
+ ErrorMessage error = getErrorMessage();
+ if (null != error) {
+ getState().setErrorMessage(error.getFormattedHtmlMessage());
+ } else {
+ getState().setErrorMessage(null);
+ }
+ }
+
+ /* Documentation copied from interface */
+ @Override
+ public void requestRepaint() {
+ // Invisible components (by flag in this particular component) do not
+ // need repaints
+ if (!getState().isVisible()) {
+ return;
+ }
+ super.requestRepaint();
+ }
+
+ /* General event framework */
+
+ private static final Method COMPONENT_EVENT_METHOD = ReflectTools
+ .findMethod(Component.Listener.class, "componentEvent",
+ Component.Event.class);
+
+ /**
+ * <p>
+ * Registers a new listener with the specified activation method to listen
+ * events generated by this component. If the activation method does not
+ * have any arguments the event object will not be passed to it when it's
+ * called.
+ * </p>
+ *
+ * <p>
+ * This method additionally informs the event-api to route events with the
+ * given eventIdentifier to the components handleEvent function call.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventIdentifier
+ * the identifier of the event to listen for
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param target
+ * the object instance who owns the activation method.
+ * @param method
+ * the activation method.
+ *
+ * @since 6.2
+ */
+ protected void addListener(String eventIdentifier, Class<?> eventType,
+ Object target, Method method) {
+ if (eventRouter == null) {
+ eventRouter = new EventRouter();
+ }
+ boolean needRepaint = !eventRouter.hasListeners(eventType);
+ eventRouter.addListener(eventType, target, method);
+
+ if (needRepaint) {
+ getState().addRegisteredEventListener(eventIdentifier);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Checks if the given {@link Event} type is listened for this component.
+ *
+ * @param eventType
+ * the event type to be checked
+ * @return true if a listener is registered for the given event type
+ */
+ protected boolean hasListeners(Class<?> eventType) {
+ return eventRouter != null && eventRouter.hasListeners(eventType);
+ }
+
+ /**
+ * Removes all registered listeners matching the given parameters. Since
+ * this method receives the event type and the listener object as
+ * parameters, it will unregister all <code>object</code>'s methods that are
+ * registered to listen to events of type <code>eventType</code> generated
+ * by this component.
+ *
+ * <p>
+ * This method additionally informs the event-api to stop routing events
+ * with the given eventIdentifier to the components handleEvent function
+ * call.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventIdentifier
+ * the identifier of the event to stop listening for
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ *
+ * @since 6.2
+ */
+ protected void removeListener(String eventIdentifier, Class<?> eventType,
+ Object target) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, target);
+ if (!eventRouter.hasListeners(eventType)) {
+ getState().removeRegisteredEventListener(eventIdentifier);
+ requestRepaint();
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * Registers a new listener with the specified activation method to listen
+ * events generated by this component. If the activation method does not
+ * have any arguments the event object will not be passed to it when it's
+ * called.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param target
+ * the object instance who owns the activation method.
+ * @param method
+ * the activation method.
+ */
+ @Override
+ public void addListener(Class<?> eventType, Object target, Method method) {
+ if (eventRouter == null) {
+ eventRouter = new EventRouter();
+ }
+ eventRouter.addListener(eventType, target, method);
+ }
+
+ /**
+ * <p>
+ * Convenience method for registering a new listener with the specified
+ * activation method to listen events generated by this component. If the
+ * activation method does not have any arguments the event object will not
+ * be passed to it when it's called.
+ * </p>
+ *
+ * <p>
+ * This version of <code>addListener</code> gets the name of the activation
+ * method as a parameter. The actual method is reflected from
+ * <code>object</code>, and unless exactly one match is found,
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * <p>
+ * Note: Using this method is discouraged because it cannot be checked
+ * during compilation. Use {@link #addListener(Class, Object, Method)} or
+ * {@link #addListener(com.vaadin.ui.Component.Listener)} instead.
+ * </p>
+ *
+ * @param eventType
+ * the type of the listened event. Events of this type or its
+ * subclasses activate the listener.
+ * @param target
+ * the object instance who owns the activation method.
+ * @param methodName
+ * the name of the activation method.
+ */
+ @Override
+ public void addListener(Class<?> eventType, Object target, String methodName) {
+ if (eventRouter == null) {
+ eventRouter = new EventRouter();
+ }
+ eventRouter.addListener(eventType, target, methodName);
+ }
+
+ /**
+ * Removes all registered listeners matching the given parameters. Since
+ * this method receives the event type and the listener object as
+ * parameters, it will unregister all <code>object</code>'s methods that are
+ * registered to listen to events of type <code>eventType</code> generated
+ * by this component.
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, target);
+ }
+ }
+
+ /**
+ * Removes one registered listener method. The given method owned by the
+ * given object will no longer be called when the specified events are
+ * generated by this component.
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * target object that has registered to listen to events of type
+ * <code>eventType</code> with one or more methods.
+ * @param method
+ * the method owned by <code>target</code> that's registered to
+ * listen to events of type <code>eventType</code>.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target, Method method) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, target, method);
+ }
+ }
+
+ /**
+ * <p>
+ * Removes one registered listener method. The given method owned by the
+ * given object will no longer be called when the specified events are
+ * generated by this component.
+ * </p>
+ *
+ * <p>
+ * This version of <code>removeListener</code> gets the name of the
+ * activation method as a parameter. The actual method is reflected from
+ * <code>target</code>, and unless exactly one match is found,
+ * <code>java.lang.IllegalArgumentException</code> is thrown.
+ * </p>
+ *
+ * <p>
+ * For more information on the inheritable event mechanism see the
+ * {@link com.vaadin.event com.vaadin.event package documentation}.
+ * </p>
+ *
+ * @param eventType
+ * the exact event type the <code>object</code> listens to.
+ * @param target
+ * the target object that has registered to listen to events of
+ * type <code>eventType</code> with one or more methods.
+ * @param methodName
+ * the name of the method owned by <code>target</code> that's
+ * registered to listen to events of type <code>eventType</code>.
+ */
+ @Override
+ public void removeListener(Class<?> eventType, Object target,
+ String methodName) {
+ if (eventRouter != null) {
+ eventRouter.removeListener(eventType, target, methodName);
+ }
+ }
+
+ /**
+ * Returns all listeners that are registered for the given event type or one
+ * of its subclasses.
+ *
+ * @param eventType
+ * The type of event to return listeners for.
+ * @return A collection with all registered listeners. Empty if no listeners
+ * are found.
+ */
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (eventRouter == null) {
+ return Collections.EMPTY_LIST;
+ }
+
+ return eventRouter.getListeners(eventType);
+ }
+
+ /**
+ * Sends the event to all listeners.
+ *
+ * @param event
+ * the Event to be sent to all listeners.
+ */
+ protected void fireEvent(Component.Event event) {
+ if (eventRouter != null) {
+ eventRouter.fireEvent(event);
+ }
+
+ }
+
+ /* Component event framework */
+
+ /*
+ * Registers a new listener to listen events generated by this component.
+ * Don't add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Component.Listener listener) {
+ addListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD);
+ }
+
+ /*
+ * Removes a previously registered listener from this component. Don't add a
+ * JavaDoc comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Component.Listener listener) {
+ removeListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD);
+ }
+
+ /**
+ * Emits the component event. It is transmitted to all registered listeners
+ * interested in such events.
+ */
+ protected void fireComponentEvent() {
+ fireEvent(new Component.Event(this));
+ }
+
+ /**
+ * Emits the component error event. It is transmitted to all registered
+ * listeners interested in such events.
+ */
+ protected void fireComponentErrorEvent() {
+ fireEvent(new Component.ErrorEvent(getComponentError(), this));
+ }
+
+ /**
+ * Sets the data object, that can be used for any application specific data.
+ * The component does not use or modify this data.
+ *
+ * @param data
+ * the Application specific data.
+ * @since 3.1
+ */
+ public void setData(Object data) {
+ applicationData = data;
+ }
+
+ /**
+ * Gets the application specific data. See {@link #setData(Object)}.
+ *
+ * @return the Application specific data set with setData function.
+ * @since 3.1
+ */
+ public Object getData() {
+ return applicationData;
+ }
+
+ /* Sizeable and other size related methods */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#getHeight()
+ */
+ @Override
+ public float getHeight() {
+ return height;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#getHeightUnits()
+ */
+ @Override
+ public Unit getHeightUnits() {
+ return heightUnit;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#getWidth()
+ */
+ @Override
+ public float getWidth() {
+ return width;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#getWidthUnits()
+ */
+ @Override
+ public Unit getWidthUnits() {
+ return widthUnit;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setHeight(float, Unit)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ if (unit == null) {
+ throw new IllegalArgumentException("Unit can not be null");
+ }
+ this.height = height;
+ heightUnit = unit;
+ requestRepaint();
+ // ComponentSizeValidator.setHeightLocation(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setSizeFull()
+ */
+ @Override
+ public void setSizeFull() {
+ setWidth(100, Unit.PERCENTAGE);
+ setHeight(100, Unit.PERCENTAGE);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setSizeUndefined()
+ */
+ @Override
+ public void setSizeUndefined() {
+ setWidth(-1, Unit.PIXELS);
+ setHeight(-1, Unit.PIXELS);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setWidth(float, Unit)
+ */
+ @Override
+ public void setWidth(float width, Unit unit) {
+ if (unit == null) {
+ throw new IllegalArgumentException("Unit can not be null");
+ }
+ this.width = width;
+ widthUnit = unit;
+ requestRepaint();
+ // ComponentSizeValidator.setWidthLocation(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setWidth(java.lang.String)
+ */
+ @Override
+ public void setWidth(String width) {
+ Size size = parseStringSize(width);
+ if (size != null) {
+ setWidth(size.getSize(), size.getUnit());
+ } else {
+ setWidth(-1, Unit.PIXELS);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Sizeable#setHeight(java.lang.String)
+ */
+ @Override
+ public void setHeight(String height) {
+ Size size = parseStringSize(height);
+ if (size != null) {
+ setHeight(size.getSize(), size.getUnit());
+ } else {
+ setHeight(-1, Unit.PIXELS);
+ }
+ }
+
+ /*
+ * Returns array with size in index 0 unit in index 1. Null or empty string
+ * will produce {-1,Unit#PIXELS}
+ */
+ private static Size parseStringSize(String s) {
+ if (s == null) {
+ return null;
+ }
+ s = s.trim();
+ if ("".equals(s)) {
+ return null;
+ }
+ float size = 0;
+ Unit unit = null;
+ Matcher matcher = sizePattern.matcher(s);
+ if (matcher.find()) {
+ size = Float.parseFloat(matcher.group(1));
+ if (size < 0) {
+ size = -1;
+ unit = Unit.PIXELS;
+ } else {
+ String symbol = matcher.group(3);
+ unit = Unit.getUnitFromSymbol(symbol);
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid size argument: \"" + s
+ + "\" (should match " + sizePattern.pattern() + ")");
+ }
+ return new Size(size, unit);
+ }
+
+ private static class Size implements Serializable {
+ float size;
+ Unit unit;
+
+ public Size(float size, Unit unit) {
+ this.size = size;
+ this.unit = unit;
+ }
+
+ public float getSize() {
+ return size;
+ }
+
+ public Unit getUnit() {
+ return unit;
+ }
+ }
+
+ public interface ComponentErrorEvent extends Terminal.ErrorEvent {
+ }
+
+ public interface ComponentErrorHandler extends Serializable {
+ /**
+ * Handle the component error
+ *
+ * @param event
+ * @return True if the error has been handled False, otherwise
+ */
+ public boolean handleComponentError(ComponentErrorEvent event);
+ }
+
+ /**
+ * Gets the error handler for the component.
+ *
+ * The error handler is dispatched whenever there is an error processing the
+ * data coming from the client.
+ *
+ * @return
+ */
+ public ComponentErrorHandler getErrorHandler() {
+ return errorHandler;
+ }
+
+ /**
+ * Sets the error handler for the component.
+ *
+ * The error handler is dispatched whenever there is an error processing the
+ * data coming from the client.
+ *
+ * If the error handler is not set, the application error handler is used to
+ * handle the exception.
+ *
+ * @param errorHandler
+ * AbstractField specific error handler
+ */
+ public void setErrorHandler(ComponentErrorHandler errorHandler) {
+ this.errorHandler = errorHandler;
+ }
+
+ /**
+ * Handle the component error event.
+ *
+ * @param error
+ * Error event to handle
+ * @return True if the error has been handled False, otherwise. If the error
+ * haven't been handled by this component, it will be handled in the
+ * application error handler.
+ */
+ public boolean handleError(ComponentErrorEvent error) {
+ if (errorHandler != null) {
+ return errorHandler.handleComponentError(error);
+ }
+ return false;
+
+ }
+
+ /*
+ * Actions
+ */
+
+ /**
+ * Gets the {@link ActionManager} used to manage the
+ * {@link ShortcutListener}s added to this {@link Field}.
+ *
+ * @return the ActionManager in use
+ */
+ protected ActionManager getActionManager() {
+ if (actionManager == null) {
+ actionManager = new ActionManager();
+ setActionManagerViewer();
+ }
+ return actionManager;
+ }
+
+ /**
+ * Set a viewer for the action manager to be the parent sub window (if the
+ * component is in a window) or the root (otherwise). This is still a
+ * simplification of the real case as this should be handled by the parent
+ * VOverlay (on the client side) if the component is inside an VOverlay
+ * component.
+ */
+ private void setActionManagerViewer() {
+ if (actionManager != null && getRoot() != null) {
+ // Attached and has action manager
+ Window w = findAncestor(Window.class);
+ if (w != null) {
+ actionManager.setViewer(w);
+ } else {
+ actionManager.setViewer(getRoot());
+ }
+ }
+
+ }
+
+ public void addShortcutListener(ShortcutListener shortcut) {
+ getActionManager().addAction(shortcut);
+ }
+
+ public void removeShortcutListener(ShortcutListener shortcut) {
+ if (actionManager != null) {
+ actionManager.removeAction(shortcut);
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbstractComponentContainer.java b/server/src/com/vaadin/ui/AbstractComponentContainer.java
new file mode 100644
index 0000000000..bc27242bb8
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractComponentContainer.java
@@ -0,0 +1,351 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.terminal.gwt.server.ComponentSizeValidator;
+
+/**
+ * Extension to {@link AbstractComponent} that defines the default
+ * implementation for the methods in {@link ComponentContainer}. Basic UI
+ * components that need to contain other components inherit this class to easily
+ * qualify as a component container.
+ *
+ * @author Vaadin Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractComponentContainer extends AbstractComponent
+ implements ComponentContainer {
+
+ /**
+ * Constructs a new component container.
+ */
+ public AbstractComponentContainer() {
+ super();
+ }
+
+ /**
+ * Removes all components from the container. This should probably be
+ * re-implemented in extending classes for a more powerful implementation.
+ */
+ @Override
+ public void removeAllComponents() {
+ final LinkedList<Component> l = new LinkedList<Component>();
+
+ // Adds all components
+ for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) {
+ l.add(i.next());
+ }
+
+ // Removes all component
+ for (final Iterator<Component> i = l.iterator(); i.hasNext();) {
+ removeComponent(i.next());
+ }
+ }
+
+ /*
+ * Moves all components from an another container into this container. Don't
+ * add a JavaDoc comment here, we use the default documentation from
+ * implemented interface.
+ */
+ @Override
+ public void moveComponentsFrom(ComponentContainer source) {
+ final LinkedList<Component> components = new LinkedList<Component>();
+ for (final Iterator<Component> i = source.getComponentIterator(); i
+ .hasNext();) {
+ components.add(i.next());
+ }
+
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component c = i.next();
+ source.removeComponent(c);
+ addComponent(c);
+ }
+ }
+
+ /* Events */
+
+ private static final Method COMPONENT_ATTACHED_METHOD;
+
+ private static final Method COMPONENT_DETACHED_METHOD;
+
+ static {
+ try {
+ COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class
+ .getDeclaredMethod("componentAttachedToContainer",
+ new Class[] { ComponentAttachEvent.class });
+ COMPONENT_DETACHED_METHOD = ComponentDetachListener.class
+ .getDeclaredMethod("componentDetachedFromContainer",
+ new Class[] { ComponentDetachEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in AbstractComponentContainer");
+ }
+ }
+
+ /* documented in interface */
+ @Override
+ public void addListener(ComponentAttachListener listener) {
+ addListener(ComponentContainer.ComponentAttachEvent.class, listener,
+ COMPONENT_ATTACHED_METHOD);
+ }
+
+ /* documented in interface */
+ @Override
+ public void addListener(ComponentDetachListener listener) {
+ addListener(ComponentContainer.ComponentDetachEvent.class, listener,
+ COMPONENT_DETACHED_METHOD);
+ }
+
+ /* documented in interface */
+ @Override
+ public void removeListener(ComponentAttachListener listener) {
+ removeListener(ComponentContainer.ComponentAttachEvent.class, listener,
+ COMPONENT_ATTACHED_METHOD);
+ }
+
+ /* documented in interface */
+ @Override
+ public void removeListener(ComponentDetachListener listener) {
+ removeListener(ComponentContainer.ComponentDetachEvent.class, listener,
+ COMPONENT_DETACHED_METHOD);
+ }
+
+ /**
+ * Fires the component attached event. This should be called by the
+ * addComponent methods after the component have been added to this
+ * container.
+ *
+ * @param component
+ * the component that has been added to this container.
+ */
+ protected void fireComponentAttachEvent(Component component) {
+ fireEvent(new ComponentAttachEvent(this, component));
+ }
+
+ /**
+ * Fires the component detached event. This should be called by the
+ * removeComponent methods after the component have been removed from this
+ * container.
+ *
+ * @param component
+ * the component that has been removed from this container.
+ */
+ protected void fireComponentDetachEvent(Component component) {
+ fireEvent(new ComponentDetachEvent(this, component));
+ }
+
+ /**
+ * This only implements the events and component parent calls. The extending
+ * classes must implement component list maintenance and call this method
+ * after component list maintenance.
+ *
+ * @see com.vaadin.ui.ComponentContainer#addComponent(Component)
+ */
+ @Override
+ public void addComponent(Component c) {
+ if (c instanceof ComponentContainer) {
+ // Make sure we're not adding the component inside it's own content
+ for (Component parent = this; parent != null; parent = parent
+ .getParent()) {
+ if (parent == c) {
+ throw new IllegalArgumentException(
+ "Component cannot be added inside it's own content");
+ }
+ }
+ }
+
+ if (c.getParent() != null) {
+ // If the component already has a parent, try to remove it
+ ComponentContainer oldParent = (ComponentContainer) c.getParent();
+ oldParent.removeComponent(c);
+
+ }
+
+ c.setParent(this);
+ fireComponentAttachEvent(c);
+ }
+
+ /**
+ * This only implements the events and component parent calls. The extending
+ * classes must implement component list maintenance and call this method
+ * before component list maintenance.
+ *
+ * @see com.vaadin.ui.ComponentContainer#removeComponent(Component)
+ */
+ @Override
+ public void removeComponent(Component c) {
+ if (c.getParent() == this) {
+ c.setParent(null);
+ fireComponentDetachEvent(c);
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (getState().isVisible() == visible) {
+ return;
+ }
+
+ super.setVisible(visible);
+ // If the visibility state is toggled it might affect all children
+ // aswell, e.g. make container visible should make children visible if
+ // they were only hidden because the container was hidden.
+ requestRepaintAll();
+ }
+
+ @Override
+ public void setWidth(float width, Unit unit) {
+ /*
+ * child tree repaints may be needed, due to our fall back support for
+ * invalid relative sizes
+ */
+ Collection<Component> dirtyChildren = null;
+ boolean childrenMayBecomeUndefined = false;
+ if (getWidth() == SIZE_UNDEFINED && width != SIZE_UNDEFINED) {
+ // children currently in invalid state may need repaint
+ dirtyChildren = getInvalidSizedChildren(false);
+ } else if ((width == SIZE_UNDEFINED && getWidth() != SIZE_UNDEFINED)
+ || (unit == Unit.PERCENTAGE
+ && getWidthUnits() != Unit.PERCENTAGE && !ComponentSizeValidator
+ .parentCanDefineWidth(this))) {
+ /*
+ * relative width children may get to invalid state if width becomes
+ * invalid. Width may also become invalid if units become percentage
+ * due to the fallback support
+ */
+ childrenMayBecomeUndefined = true;
+ dirtyChildren = getInvalidSizedChildren(false);
+ }
+ super.setWidth(width, unit);
+ repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined,
+ false);
+ }
+
+ private void repaintChangedChildTrees(
+ Collection<Component> invalidChildren,
+ boolean childrenMayBecomeUndefined, boolean vertical) {
+ if (childrenMayBecomeUndefined) {
+ Collection<Component> previouslyInvalidComponents = invalidChildren;
+ invalidChildren = getInvalidSizedChildren(vertical);
+ if (previouslyInvalidComponents != null && invalidChildren != null) {
+ for (Iterator<Component> iterator = invalidChildren.iterator(); iterator
+ .hasNext();) {
+ Component component = iterator.next();
+ if (previouslyInvalidComponents.contains(component)) {
+ // still invalid don't repaint
+ iterator.remove();
+ }
+ }
+ }
+ } else if (invalidChildren != null) {
+ Collection<Component> stillInvalidChildren = getInvalidSizedChildren(vertical);
+ if (stillInvalidChildren != null) {
+ for (Component component : stillInvalidChildren) {
+ // didn't become valid
+ invalidChildren.remove(component);
+ }
+ }
+ }
+ if (invalidChildren != null) {
+ repaintChildTrees(invalidChildren);
+ }
+ }
+
+ private Collection<Component> getInvalidSizedChildren(final boolean vertical) {
+ HashSet<Component> components = null;
+ if (this instanceof Panel) {
+ Panel p = (Panel) this;
+ ComponentContainer content = p.getContent();
+ boolean valid = vertical ? ComponentSizeValidator
+ .checkHeights(content) : ComponentSizeValidator
+ .checkWidths(content);
+
+ if (!valid) {
+ components = new HashSet<Component>(1);
+ components.add(content);
+ }
+ } else {
+ for (Iterator<Component> componentIterator = getComponentIterator(); componentIterator
+ .hasNext();) {
+ Component component = componentIterator.next();
+ boolean valid = vertical ? ComponentSizeValidator
+ .checkHeights(component) : ComponentSizeValidator
+ .checkWidths(component);
+ if (!valid) {
+ if (components == null) {
+ components = new HashSet<Component>();
+ }
+ components.add(component);
+ }
+ }
+ }
+ return components;
+ }
+
+ private void repaintChildTrees(Collection<Component> dirtyChildren) {
+ for (Component c : dirtyChildren) {
+ if (c instanceof ComponentContainer) {
+ ComponentContainer cc = (ComponentContainer) c;
+ cc.requestRepaintAll();
+ } else {
+ c.requestRepaint();
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(float height, Unit unit) {
+ /*
+ * child tree repaints may be needed, due to our fall back support for
+ * invalid relative sizes
+ */
+ Collection<Component> dirtyChildren = null;
+ boolean childrenMayBecomeUndefined = false;
+ if (getHeight() == SIZE_UNDEFINED && height != SIZE_UNDEFINED) {
+ // children currently in invalid state may need repaint
+ dirtyChildren = getInvalidSizedChildren(true);
+ } else if ((height == SIZE_UNDEFINED && getHeight() != SIZE_UNDEFINED)
+ || (unit == Unit.PERCENTAGE
+ && getHeightUnits() != Unit.PERCENTAGE && !ComponentSizeValidator
+ .parentCanDefineHeight(this))) {
+ /*
+ * relative height children may get to invalid state if height
+ * becomes invalid. Height may also become invalid if units become
+ * percentage due to the fallback support.
+ */
+ childrenMayBecomeUndefined = true;
+ dirtyChildren = getInvalidSizedChildren(true);
+ }
+ super.setHeight(height, unit);
+ repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined,
+ true);
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ return getComponentIterator();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.HasComponents#isComponentVisible(com.vaadin.ui.Component)
+ */
+ @Override
+ public boolean isComponentVisible(Component childComponent) {
+ return true;
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java
new file mode 100644
index 0000000000..6fe7f54df5
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractField.java
@@ -0,0 +1,1657 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+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.converter.Converter;
+import com.vaadin.data.util.converter.Converter.ConversionException;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.Action;
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.event.ShortcutListener;
+import com.vaadin.shared.AbstractFieldState;
+import com.vaadin.terminal.AbstractErrorMessage;
+import com.vaadin.terminal.CompositeErrorMessage;
+import com.vaadin.terminal.ErrorMessage;
+
+/**
+ * <p>
+ * Abstract field component for implementing buffered property editors. The
+ * field may hold an internal value, or it may be connected to any data source
+ * that implements the {@link com.vaadin.data.Property}interface.
+ * <code>AbstractField</code> implements that interface itself, too, so
+ * accessing the Property value represented by it is straightforward.
+ * </p>
+ *
+ * <p>
+ * AbstractField also provides the {@link com.vaadin.data.Buffered} interface
+ * for buffering the data source value. By default the Field is in write
+ * through-mode and {@link #setWriteThrough(boolean)}should be called to enable
+ * buffering.
+ * </p>
+ *
+ * <p>
+ * The class also supports {@link com.vaadin.data.Validator validators} to make
+ * sure the value contained in the field is valid.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractField<T> extends AbstractComponent implements
+ Field<T>, Property.ReadOnlyStatusChangeListener,
+ Property.ReadOnlyStatusChangeNotifier, Action.ShortcutNotifier {
+
+ /* Private members */
+
+ private static final Logger logger = Logger.getLogger(AbstractField.class
+ .getName());
+
+ /**
+ * Value of the abstract field.
+ */
+ private T value;
+
+ /**
+ * A converter used to convert from the data model type to the field type
+ * and vice versa.
+ */
+ private Converter<T, Object> converter = null;
+ /**
+ * Connected data-source.
+ */
+ private Property<?> dataSource = null;
+
+ /**
+ * The list of validators.
+ */
+ private LinkedList<Validator> validators = null;
+
+ /**
+ * Auto commit mode.
+ */
+ private boolean writeThroughMode = true;
+
+ /**
+ * Reads the value from data-source, when it is not modified.
+ */
+ private boolean readThroughMode = true;
+
+ /**
+ * Flag to indicate that the field is currently committing its value to the
+ * datasource.
+ */
+ private boolean committingValueToDataSource = false;
+
+ /**
+ * Current source exception.
+ */
+ private Buffered.SourceException currentBufferedSourceException = null;
+
+ /**
+ * Are the invalid values allowed in fields ?
+ */
+ private boolean invalidAllowed = true;
+
+ /**
+ * Are the invalid values committed ?
+ */
+ private boolean invalidCommitted = false;
+
+ /**
+ * The error message for the exception that is thrown when the field is
+ * required but empty.
+ */
+ private String requiredError = "";
+
+ /**
+ * The error message that is shown when the field value cannot be converted.
+ */
+ private String conversionError = "Could not convert value to {0}";
+
+ /**
+ * Is automatic validation enabled.
+ */
+ private boolean validationVisible = true;
+
+ private boolean valueWasModifiedByDataSourceDuringCommit;
+
+ /**
+ * Whether this field is currently registered as listening to events from
+ * its data source.
+ *
+ * @see #setPropertyDataSource(Property)
+ * @see #addPropertyListeners()
+ * @see #removePropertyListeners()
+ */
+ private boolean isListeningToPropertyEvents = false;
+
+ /* Component basics */
+
+ /*
+ * Paints the field. Don't add a JavaDoc comment here, we use the default
+ * documentation from the implemented interface.
+ */
+
+ /**
+ * Returns true if the error indicator be hidden when painting the component
+ * even when there are errors.
+ *
+ * This is a mostly internal method, but can be overridden in subclasses
+ * e.g. if the error indicator should also be shown for empty fields in some
+ * cases.
+ *
+ * @return true to hide the error indicator, false to use the normal logic
+ * to show it when there are errors
+ */
+ protected boolean shouldHideErrors() {
+ // getErrorMessage() can still return something else than null based on
+ // validation etc.
+ return isRequired() && isEmpty() && getComponentError() == null;
+ }
+
+ /**
+ * Returns the type of the Field. The methods <code>getValue</code> and
+ * <code>setValue</code> must be compatible with this type: one must be able
+ * to safely cast the value returned from <code>getValue</code> to the given
+ * type and pass any variable assignable to this type as an argument to
+ * <code>setValue</code>.
+ *
+ * @return the type of the Field
+ */
+ @Override
+ public abstract Class<? extends T> getType();
+
+ /**
+ * The abstract field is read only also if the data source is in read only
+ * mode.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly()
+ || (dataSource != null && dataSource.isReadOnly());
+ }
+
+ /**
+ * Changes the readonly state and throw read-only status change events.
+ *
+ * @see com.vaadin.ui.Component#setReadOnly(boolean)
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ super.setReadOnly(readOnly);
+ fireReadOnlyStatusChange();
+ }
+
+ /**
+ * Tests if the invalid data is committed to datasource.
+ *
+ * @see com.vaadin.data.BufferedValidatable#isInvalidCommitted()
+ */
+ @Override
+ public boolean isInvalidCommitted() {
+ return invalidCommitted;
+ }
+
+ /**
+ * Sets if the invalid data should be committed to datasource.
+ *
+ * @see com.vaadin.data.BufferedValidatable#setInvalidCommitted(boolean)
+ */
+ @Override
+ public void setInvalidCommitted(boolean isCommitted) {
+ invalidCommitted = isCommitted;
+ }
+
+ /*
+ * Saves the current value to the data source Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void commit() throws Buffered.SourceException, InvalidValueException {
+ if (dataSource != null && !dataSource.isReadOnly()) {
+ if ((isInvalidCommitted() || isValid())) {
+ try {
+
+ // Commits the value to datasource.
+ valueWasModifiedByDataSourceDuringCommit = false;
+ committingValueToDataSource = true;
+ getPropertyDataSource().setValue(getConvertedValue());
+ } catch (final Throwable e) {
+
+ // Sets the buffering state.
+ SourceException sourceException = new Buffered.SourceException(
+ this, e);
+ setCurrentBufferedSourceException(sourceException);
+
+ // Throws the source exception.
+ throw sourceException;
+ } finally {
+ committingValueToDataSource = false;
+ }
+ } else {
+ /* An invalid value and we don't allow them, throw the exception */
+ validate();
+ }
+ }
+
+ // The abstract field is not modified anymore
+ if (isModified()) {
+ setModified(false);
+ }
+
+ // If successful, remove set the buffering state to be ok
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+
+ if (valueWasModifiedByDataSourceDuringCommit) {
+ valueWasModifiedByDataSourceDuringCommit = false;
+ fireValueChange(false);
+ }
+
+ }
+
+ /*
+ * Updates the value from the data source. Don't add a JavaDoc comment here,
+ * we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void discard() throws Buffered.SourceException {
+ if (dataSource != null) {
+
+ // Gets the correct value from datasource
+ T newFieldValue;
+ try {
+
+ // Discards buffer by overwriting from datasource
+ newFieldValue = convertFromDataSource(getDataSourceValue());
+
+ // If successful, remove set the buffering state to be ok
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+ } catch (final Throwable e) {
+ // FIXME: What should really be done here if conversion fails?
+
+ // Sets the buffering state
+ currentBufferedSourceException = new Buffered.SourceException(
+ this, e);
+ requestRepaint();
+
+ // Throws the source exception
+ throw currentBufferedSourceException;
+ }
+
+ final boolean wasModified = isModified();
+ setModified(false);
+
+ // If the new value differs from the previous one
+ if (!equals(newFieldValue, getInternalValue())) {
+ setInternalValue(newFieldValue);
+ fireValueChange(false);
+ } else if (wasModified) {
+ // If the value did not change, but the modification status did
+ requestRepaint();
+ }
+ }
+ }
+
+ /**
+ * Gets the value from the data source. This is only here because of clarity
+ * in the code that handles both the data model value and the field value.
+ *
+ * @return The value of the property data source
+ */
+ private Object getDataSourceValue() {
+ return dataSource.getValue();
+ }
+
+ /**
+ * Returns the field value. This is always identical to {@link #getValue()}
+ * and only here because of clarity in the code that handles both the data
+ * model value and the field value.
+ *
+ * @return The value of the field
+ */
+ private T getFieldValue() {
+ // Give the value from abstract buffers if the field if possible
+ if (dataSource == null || !isReadThrough() || isModified()) {
+ return getInternalValue();
+ }
+
+ // There is no buffered value so use whatever the data model provides
+ return convertFromDataSource(getDataSourceValue());
+ }
+
+ /*
+ * Has the field been modified since the last commit()? Don't add a JavaDoc
+ * comment here, we use the default documentation from the implemented
+ * interface.
+ */
+ @Override
+ public boolean isModified() {
+ return getState().isModified();
+ }
+
+ private void setModified(boolean modified) {
+ getState().setModified(modified);
+ requestRepaint();
+ }
+
+ /*
+ * Tests if the field is in write-through mode. Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public boolean isWriteThrough() {
+ return writeThroughMode;
+ }
+
+ /**
+ * Sets the field's write-through mode to the specified status. When
+ * switching the write-through mode on, a {@link #commit()} will be
+ * performed.
+ *
+ * @see #setBuffered(boolean) for an easier way to control read through and
+ * write through modes
+ *
+ * @param writeThrough
+ * Boolean value to indicate if the object should be in
+ * write-through mode after the call.
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source.
+ * @throws InvalidValueException
+ * If the implicit commit operation fails because of a
+ * validation error.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Override
+ @Deprecated
+ public void setWriteThrough(boolean writeThrough)
+ throws Buffered.SourceException, InvalidValueException {
+ if (writeThroughMode == writeThrough) {
+ return;
+ }
+ writeThroughMode = writeThrough;
+ if (writeThroughMode) {
+ commit();
+ }
+ }
+
+ /*
+ * Tests if the field is in read-through mode. Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public boolean isReadThrough() {
+ return readThroughMode;
+ }
+
+ /**
+ * Sets the field's read-through mode to the specified status. When
+ * switching read-through mode on, the object's value is updated from the
+ * data source.
+ *
+ * @see #setBuffered(boolean) for an easier way to control read through and
+ * write through modes
+ *
+ * @param readThrough
+ * Boolean value to indicate if the object should be in
+ * read-through mode after the call.
+ *
+ * @throws SourceException
+ * If the operation fails because of an exception is thrown by
+ * the data source. The cause is included in the exception.
+ * @deprecated Use {@link #setBuffered(boolean)} instead. Note that
+ * setReadThrough(true), setWriteThrough(true) equals
+ * setBuffered(false)
+ */
+ @Override
+ @Deprecated
+ public void setReadThrough(boolean readThrough)
+ throws Buffered.SourceException {
+ if (readThroughMode == readThrough) {
+ return;
+ }
+ readThroughMode = readThrough;
+ if (!isModified() && readThroughMode && getPropertyDataSource() != null) {
+ setInternalValue(convertFromDataSource(getDataSourceValue()));
+ fireValueChange(false);
+ }
+ }
+
+ /**
+ * Sets the buffered mode of this Field.
+ * <p>
+ * When the field is in buffered mode, changes will not be committed to the
+ * property data source until {@link #commit()} is called.
+ * </p>
+ * <p>
+ * Changing buffered mode will change the read through and write through
+ * state for the field.
+ * </p>
+ * <p>
+ * Mixing calls to {@link #setBuffered(boolean)} and
+ * {@link #setReadThrough(boolean)} or {@link #setWriteThrough(boolean)} is
+ * generally a bad idea.
+ * </p>
+ *
+ * @param buffered
+ * true if buffered mode should be turned on, false otherwise
+ */
+ @Override
+ public void setBuffered(boolean buffered) {
+ setReadThrough(!buffered);
+ setWriteThrough(!buffered);
+ }
+
+ /**
+ * Checks the buffered mode of this Field.
+ * <p>
+ * This method only returns true if both read and write buffering is used.
+ *
+ * @return true if buffered mode is on, false otherwise
+ */
+ @Override
+ public boolean isBuffered() {
+ return !isReadThrough() && !isWriteThrough();
+ }
+
+ /* Property interface implementation */
+
+ /**
+ * Returns the (field) value converted to a String using toString().
+ *
+ * @see java.lang.Object#toString()
+ * @deprecated Instead 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
+ * of the data source.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ logger.warning("You are using AbstractField.toString() to get the value for a "
+ + getClass().getSimpleName()
+ + ". This is not recommended and will not be supported in future versions.");
+ final Object value = getFieldValue();
+ if (value == null) {
+ return null;
+ }
+ return value.toString();
+ }
+
+ /**
+ * Gets the current value of the field.
+ *
+ * <p>
+ * This is the visible, modified and possible invalid value the user have
+ * entered to the field.
+ * </p>
+ *
+ * <p>
+ * Note that the object returned is compatible with getType(). For example,
+ * if the type is String, this returns Strings even when the underlying
+ * datasource is of some other type. In order to access the converted value,
+ * use {@link #getConvertedValue()} and to access the value of the property
+ * data source, use {@link Property#getValue()} for the property data
+ * source.
+ * </p>
+ *
+ * <p>
+ * Since Vaadin 7.0, no implicit conversions between other data types and
+ * String are performed, but a converter is used if set.
+ * </p>
+ *
+ * @return the current value of the field.
+ */
+ @Override
+ public T getValue() {
+ return getFieldValue();
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param newFieldValue
+ * the New value of the field.
+ * @throws Property.ReadOnlyException
+ */
+ @Override
+ public void setValue(Object newFieldValue)
+ throws Property.ReadOnlyException, Converter.ConversionException {
+ // This check is needed as long as setValue accepts Object instead of T
+ if (newFieldValue != null) {
+ if (!getType().isAssignableFrom(newFieldValue.getClass())) {
+ throw new Converter.ConversionException("Value of type "
+ + newFieldValue.getClass() + " cannot be assigned to "
+ + getType().getName());
+ }
+ }
+ setValue((T) newFieldValue, false);
+ }
+
+ /**
+ * Sets the value of the field.
+ *
+ * @param newFieldValue
+ * the New value of the field.
+ * @param repaintIsNotNeeded
+ * True iff caller is sure that repaint is not needed.
+ * @throws Property.ReadOnlyException
+ */
+ protected void setValue(T newFieldValue, boolean repaintIsNotNeeded)
+ throws Property.ReadOnlyException, Converter.ConversionException,
+ InvalidValueException {
+
+ if (!equals(newFieldValue, getInternalValue())) {
+
+ // Read only fields can not be changed
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Repaint is needed even when the client thinks that it knows the
+ // new state if validity of the component may change
+ if (repaintIsNotNeeded
+ && (isRequired() || getValidators() != null || getConverter() != null)) {
+ repaintIsNotNeeded = false;
+ }
+
+ if (!isInvalidAllowed()) {
+ /*
+ * If invalid values are not allowed the value must be validated
+ * before it is set. If validation fails, the
+ * InvalidValueException is thrown and the internal value is not
+ * updated.
+ */
+ validate(newFieldValue);
+ }
+
+ // Changes the value
+ setInternalValue(newFieldValue);
+ setModified(dataSource != null);
+
+ valueWasModifiedByDataSourceDuringCommit = false;
+ // In write through mode , try to commit
+ if (isWriteThrough() && dataSource != null
+ && (isInvalidCommitted() || isValid())) {
+ try {
+
+ // Commits the value to datasource
+ committingValueToDataSource = true;
+ getPropertyDataSource().setValue(
+ convertToModel(newFieldValue));
+
+ // The buffer is now unmodified
+ setModified(false);
+
+ } catch (final Throwable e) {
+
+ // Sets the buffering state
+ currentBufferedSourceException = new Buffered.SourceException(
+ this, e);
+ requestRepaint();
+
+ // Throws the source exception
+ throw currentBufferedSourceException;
+ } finally {
+ committingValueToDataSource = false;
+ }
+ }
+
+ // If successful, remove set the buffering state to be ok
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+
+ if (valueWasModifiedByDataSourceDuringCommit) {
+ /*
+ * Value was modified by datasource. Force repaint even if
+ * repaint was not requested.
+ */
+ valueWasModifiedByDataSourceDuringCommit = repaintIsNotNeeded = false;
+ }
+
+ // Fires the value change
+ fireValueChange(repaintIsNotNeeded);
+
+ }
+ }
+
+ private static boolean equals(Object value1, Object value2) {
+ if (value1 == null) {
+ return value2 == null;
+ }
+ return value1.equals(value2);
+ }
+
+ /* External data source */
+
+ /**
+ * Gets the current data source of the field, if any.
+ *
+ * @return the current data source as a Property, or <code>null</code> if
+ * none defined.
+ */
+ @Override
+ public Property getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * <p>
+ * Sets the specified Property as the data source for the field. All
+ * uncommitted changes are replaced with a value from the new data source.
+ * </p>
+ *
+ * <p>
+ * If the datasource has any validators, the same validators are added to
+ * the field. Because the default behavior of the field is to allow invalid
+ * values, but not to allow committing them, this only adds visual error
+ * messages to fields and do not allow committing them as long as the value
+ * is invalid. After the value is valid, the error message is not shown and
+ * the commit can be done normally.
+ * </p>
+ *
+ * <p>
+ * If the data source implements
+ * {@link com.vaadin.data.Property.ValueChangeNotifier} and/or
+ * {@link com.vaadin.data.Property.ReadOnlyStatusChangeNotifier}, the field
+ * registers itself as a listener and updates itself according to the events
+ * it receives. To avoid memory leaks caused by references to a field no
+ * longer in use, the listener registrations are removed on
+ * {@link AbstractField#detach() detach} and re-added on
+ * {@link AbstractField#attach() attach}.
+ * </p>
+ *
+ * <p>
+ * Note: before 6.5 we actually called discard() method in the beginning of
+ * the method. This was removed to simplify implementation, avoid excess
+ * calls to backing property and to avoid odd value change events that were
+ * previously fired (developer expects 0-1 value change events if this
+ * method is called). Some complex field implementations might now need to
+ * override this method to do housekeeping similar to discard().
+ * </p>
+ *
+ * @param newDataSource
+ * the new data source Property.
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+
+ // Saves the old value
+ final Object oldValue = getInternalValue();
+
+ // Stop listening to the old data source
+ removePropertyListeners();
+
+ // Sets the new data source
+ dataSource = newDataSource;
+ getState().setPropertyReadOnly(
+ dataSource == null ? false : dataSource.isReadOnly());
+
+ // Check if the current converter is compatible.
+ if (newDataSource != null
+ && !ConverterUtil.canConverterHandle(getConverter(), getType(),
+ newDataSource.getType())) {
+ // Changing from e.g. Number -> Double should set a new converter,
+ // changing from Double -> Number can keep the old one (Property
+ // accepts Number)
+
+ // Set a new converter if there is a new data source and
+ // there is no old converter or the old is incompatible.
+ setConverter(newDataSource.getType());
+ }
+ // Gets the value from source
+ try {
+ if (dataSource != null) {
+ T fieldValue = convertFromDataSource(getDataSourceValue());
+ setInternalValue(fieldValue);
+ }
+ setModified(false);
+ if (getCurrentBufferedSourceException() != null) {
+ setCurrentBufferedSourceException(null);
+ }
+ } catch (final Throwable e) {
+ setCurrentBufferedSourceException(new Buffered.SourceException(
+ this, e));
+ setModified(true);
+ }
+
+ // Listen to new data source if possible
+ addPropertyListeners();
+
+ // Copy the validators from the data source
+ if (dataSource instanceof Validatable) {
+ final Collection<Validator> validators = ((Validatable) dataSource)
+ .getValidators();
+ if (validators != null) {
+ for (final Iterator<Validator> i = validators.iterator(); i
+ .hasNext();) {
+ addValidator(i.next());
+ }
+ }
+ }
+
+ // Fires value change if the value has changed
+ T value = getInternalValue();
+ if ((value != oldValue)
+ && ((value != null && !value.equals(oldValue)) || value == null)) {
+ fireValueChange(false);
+ }
+ }
+
+ /**
+ * Retrieves a converter for the field from the converter factory defined
+ * for the application. Clears the converter if no application reference is
+ * available or if the factory returns null.
+ *
+ * @param datamodelType
+ * The type of the data model that we want to be able to convert
+ * from
+ */
+ public void setConverter(Class<?> datamodelType) {
+ Converter<T, ?> c = (Converter<T, ?>) ConverterUtil.getConverter(
+ getType(), datamodelType, getApplication());
+ setConverter(c);
+ }
+
+ /**
+ * Convert the given value from the data source type to the UI type.
+ *
+ * @param newValue
+ * The data source value to convert.
+ * @return The converted value that is compatible with the UI type or the
+ * original value if its type is compatible and no converter is set.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the data source type.
+ */
+ private T convertFromDataSource(Object newValue) {
+ return ConverterUtil.convertFromModel(newValue, getType(),
+ getConverter(), getLocale());
+ }
+
+ /**
+ * Convert the given value from the UI type to the data source type.
+ *
+ * @param fieldValue
+ * The value to convert. Typically returned by
+ * {@link #getFieldValue()}
+ * @return The converted value that is compatible with the data source type.
+ * @throws Converter.ConversionException
+ * if there is no converter and the type is not compatible with
+ * the data source type.
+ */
+ private Object convertToModel(T fieldValue)
+ throws Converter.ConversionException {
+ try {
+ Class<?> modelType = null;
+ Property pd = getPropertyDataSource();
+ if (pd != null) {
+ modelType = pd.getType();
+ } else if (getConverter() != null) {
+ modelType = getConverter().getModelType();
+ }
+ return ConverterUtil.convertToModel(fieldValue,
+ (Class<Object>) modelType, getConverter(), getLocale());
+ } catch (ConversionException e) {
+ throw new ConversionException(
+ getConversionError(converter.getModelType()), e);
+ }
+ }
+
+ /**
+ * Returns the conversion error with {0} replaced by the data source type.
+ *
+ * @param dataSourceType
+ * The type of the data source
+ * @return The value conversion error string with parameters replaced.
+ */
+ protected String getConversionError(Class<?> dataSourceType) {
+ if (dataSourceType == null) {
+ return getConversionError();
+ } else {
+ return getConversionError().replace("{0}",
+ dataSourceType.getSimpleName());
+ }
+ }
+
+ /**
+ * Returns the current value (as returned by {@link #getValue()}) converted
+ * to the data source type.
+ * <p>
+ * This returns the same as {@link AbstractField#getValue()} if no converter
+ * has been set. The value is not necessarily the same as the data source
+ * value e.g. if the field is in buffered mode and has been modified.
+ * </p>
+ *
+ * @return The converted value that is compatible with the data source type
+ */
+ public Object getConvertedValue() {
+ return convertToModel(getFieldValue());
+ }
+
+ /**
+ * Sets the value of the field using a value of the data source type. The
+ * value given is converted to the field type and then assigned to the
+ * field. This will update the property data source in the same way as when
+ * {@link #setValue(Object)} is called.
+ *
+ * @param value
+ * The value to set. Must be the same type as the data source.
+ */
+ public void setConvertedValue(Object value) {
+ setValue(convertFromDataSource(value));
+ }
+
+ /* Validation */
+
+ /**
+ * Adds a new validator for the field's value. All validators added to a
+ * field are checked each time the its value changes.
+ *
+ * @param validator
+ * the new validator to be added.
+ */
+ @Override
+ public void addValidator(Validator validator) {
+ if (validators == null) {
+ validators = new LinkedList<Validator>();
+ }
+ validators.add(validator);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the validators of the field.
+ *
+ * @return the Unmodifiable collection that holds all validators for the
+ * field.
+ */
+ @Override
+ public Collection<Validator> getValidators() {
+ if (validators == null || validators.isEmpty()) {
+ return null;
+ }
+ return Collections.unmodifiableCollection(validators);
+ }
+
+ /**
+ * Removes the validator from the field.
+ *
+ * @param validator
+ * the validator to remove.
+ */
+ @Override
+ public void removeValidator(Validator validator) {
+ if (validators != null) {
+ validators.remove(validator);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Removes all validators from the field.
+ */
+ public void removeAllValidators() {
+ if (validators != null) {
+ validators.clear();
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Tests the current value against registered validators if the field is not
+ * empty. If the field is empty it is considered valid if it is not required
+ * and invalid otherwise. Validators are never checked for empty fields.
+ *
+ * In most cases, {@link #validate()} should be used instead of
+ * {@link #isValid()} to also get the error message.
+ *
+ * @return <code>true</code> if all registered validators claim that the
+ * current value is valid or if the field is empty and not required,
+ * <code>false</code> otherwise.
+ */
+ @Override
+ public boolean isValid() {
+
+ try {
+ validate();
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks the validity of the Field.
+ *
+ * A field is invalid if it is set as required (using
+ * {@link #setRequired(boolean)} and is empty, if one or several of the
+ * validators added to the field indicate it is invalid or if the value
+ * cannot be converted provided a converter has been set.
+ *
+ * The "required" validation is a built-in validation feature. If the field
+ * is required and empty this method throws an EmptyValueException with the
+ * error message set using {@link #setRequiredError(String)}.
+ *
+ * @see com.vaadin.data.Validatable#validate()
+ */
+ @Override
+ public void validate() throws Validator.InvalidValueException {
+
+ if (isRequired() && isEmpty()) {
+ throw new Validator.EmptyValueException(requiredError);
+ }
+ validate(getFieldValue());
+ }
+
+ /**
+ * Validates that the given value pass the validators for the field.
+ * <p>
+ * This method does not check the requiredness of the field.
+ *
+ * @param fieldValue
+ * The value to check
+ * @throws Validator.InvalidValueException
+ * if one or several validators fail
+ */
+ protected void validate(T fieldValue)
+ throws Validator.InvalidValueException {
+
+ Object valueToValidate = fieldValue;
+
+ // If there is a converter we start by converting the value as we want
+ // to validate the converted value
+ if (getConverter() != null) {
+ try {
+ valueToValidate = getConverter().convertToModel(fieldValue,
+ getLocale());
+ } catch (Exception e) {
+ throw new InvalidValueException(
+ getConversionError(getConverter().getModelType()));
+ }
+ }
+
+ List<InvalidValueException> validationExceptions = new ArrayList<InvalidValueException>();
+ if (validators != null) {
+ // Gets all the validation errors
+ for (Validator v : validators) {
+ try {
+ v.validate(valueToValidate);
+ } catch (final Validator.InvalidValueException e) {
+ validationExceptions.add(e);
+ }
+ }
+ }
+
+ // If there were no errors
+ if (validationExceptions.isEmpty()) {
+ return;
+ }
+
+ // If only one error occurred, throw it forwards
+ if (validationExceptions.size() == 1) {
+ throw validationExceptions.get(0);
+ }
+
+ InvalidValueException[] exceptionArray = validationExceptions
+ .toArray(new InvalidValueException[validationExceptions.size()]);
+
+ // Create a composite validator and include all exceptions
+ throw new Validator.InvalidValueException(null, exceptionArray);
+ }
+
+ /**
+ * Fields allow invalid values by default. In most cases this is wanted,
+ * because the field otherwise visually forget the user input immediately.
+ *
+ * @return true iff the invalid values are allowed.
+ * @see com.vaadin.data.Validatable#isInvalidAllowed()
+ */
+ @Override
+ public boolean isInvalidAllowed() {
+ return invalidAllowed;
+ }
+
+ /**
+ * Fields allow invalid values by default. In most cases this is wanted,
+ * because the field otherwise visually forget the user input immediately.
+ * <p>
+ * In common setting where the user wants to assure the correctness of the
+ * datasource, but allow temporarily invalid contents in the field, the user
+ * should add the validators to datasource, that should not allow invalid
+ * values. The validators are automatically copied to the field when the
+ * datasource is set.
+ * </p>
+ *
+ * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean)
+ */
+ @Override
+ public void setInvalidAllowed(boolean invalidAllowed)
+ throws UnsupportedOperationException {
+ this.invalidAllowed = invalidAllowed;
+ }
+
+ /**
+ * Error messages shown by the fields are composites of the error message
+ * thrown by the superclasses (that is the component error message),
+ * validation errors and buffered source errors.
+ *
+ * @see com.vaadin.ui.AbstractComponent#getErrorMessage()
+ */
+ @Override
+ public ErrorMessage getErrorMessage() {
+
+ /*
+ * Check validation errors only if automatic validation is enabled.
+ * Empty, required fields will generate a validation error containing
+ * the requiredError string. For these fields the exclamation mark will
+ * be hidden but the error must still be sent to the client.
+ */
+ Validator.InvalidValueException validationError = null;
+ if (isValidationVisible()) {
+ try {
+ validate();
+ } catch (Validator.InvalidValueException e) {
+ if (!e.isInvisible()) {
+ validationError = e;
+ }
+ }
+ }
+
+ // Check if there are any systems errors
+ final ErrorMessage superError = super.getErrorMessage();
+
+ // Return if there are no errors at all
+ if (superError == null && validationError == null
+ && getCurrentBufferedSourceException() == null) {
+ return null;
+ }
+
+ // Throw combination of the error types
+ return new CompositeErrorMessage(
+ new ErrorMessage[] {
+ superError,
+ AbstractErrorMessage
+ .getErrorMessageForException(validationError),
+ AbstractErrorMessage
+ .getErrorMessageForException(getCurrentBufferedSourceException()) });
+
+ }
+
+ /* Value change events */
+
+ private static final Method VALUE_CHANGE_METHOD;
+
+ static {
+ try {
+ VALUE_CHANGE_METHOD = Property.ValueChangeListener.class
+ .getDeclaredMethod("valueChange",
+ new Class[] { Property.ValueChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in AbstractField");
+ }
+ }
+
+ /*
+ * Adds a value change listener for the field. Don't add a JavaDoc comment
+ * here, we use the default documentation from the implemented interface.
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ addListener(AbstractField.ValueChangeEvent.class, listener,
+ VALUE_CHANGE_METHOD);
+ }
+
+ /*
+ * Removes a value change listener from the field. Don't add a JavaDoc
+ * comment here, we use the default documentation from the implemented
+ * interface.
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ removeListener(AbstractField.ValueChangeEvent.class, listener,
+ VALUE_CHANGE_METHOD);
+ }
+
+ /**
+ * Emits the value change event. The value contained in the field is
+ * validated before the event is created.
+ */
+ protected void fireValueChange(boolean repaintIsNotNeeded) {
+ fireEvent(new AbstractField.ValueChangeEvent(this));
+ if (!repaintIsNotNeeded) {
+ requestRepaint();
+ }
+ }
+
+ /* Read-only status change events */
+
+ private static final Method READ_ONLY_STATUS_CHANGE_METHOD;
+
+ static {
+ try {
+ READ_ONLY_STATUS_CHANGE_METHOD = Property.ReadOnlyStatusChangeListener.class
+ .getDeclaredMethod(
+ "readOnlyStatusChange",
+ new Class[] { Property.ReadOnlyStatusChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in AbstractField");
+ }
+ }
+
+ /**
+ * React to read only status changes of the property by requesting a
+ * repaint.
+ *
+ * @see Property.ReadOnlyStatusChangeListener
+ */
+ @Override
+ public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) {
+ getState().setPropertyReadOnly(event.getProperty().isReadOnly());
+ requestRepaint();
+ }
+
+ /**
+ * An <code>Event</code> object specifying the Property whose read-only
+ * status has changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class ReadOnlyStatusChangeEvent extends Component.Event
+ implements Property.ReadOnlyStatusChangeEvent, Serializable {
+
+ /**
+ * New instance of text change event.
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public ReadOnlyStatusChangeEvent(AbstractField source) {
+ super(source);
+ }
+
+ /**
+ * Property where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+ }
+
+ /*
+ * Adds a read-only status change listener for the field. Don't add a
+ * JavaDoc comment here, we use the default documentation from the
+ * implemented interface.
+ */
+ @Override
+ public void addListener(Property.ReadOnlyStatusChangeListener listener) {
+ addListener(Property.ReadOnlyStatusChangeEvent.class, listener,
+ READ_ONLY_STATUS_CHANGE_METHOD);
+ }
+
+ /*
+ * Removes a read-only status change listener from the field. Don't add a
+ * JavaDoc comment here, we use the default documentation from the
+ * implemented interface.
+ */
+ @Override
+ public void removeListener(Property.ReadOnlyStatusChangeListener listener) {
+ removeListener(Property.ReadOnlyStatusChangeEvent.class, listener,
+ READ_ONLY_STATUS_CHANGE_METHOD);
+ }
+
+ /**
+ * Emits the read-only status change event. The value contained in the field
+ * is validated before the event is created.
+ */
+ protected void fireReadOnlyStatusChange() {
+ fireEvent(new AbstractField.ReadOnlyStatusChangeEvent(this));
+ }
+
+ /**
+ * This method listens to data source value changes and passes the changes
+ * forwards.
+ *
+ * Changes are not forwarded to the listeners of the field during internal
+ * operations of the field to avoid duplicate notifications.
+ *
+ * @param event
+ * the value change event telling the data source contents have
+ * changed.
+ */
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ if (isReadThrough()) {
+ if (committingValueToDataSource) {
+ boolean propertyNotifiesOfTheBufferedValue = equals(event
+ .getProperty().getValue(), getInternalValue());
+ if (!propertyNotifiesOfTheBufferedValue) {
+ /*
+ * Property (or chained property like PropertyFormatter) now
+ * reports different value than the one the field has just
+ * committed to it. In this case we respect the property
+ * value.
+ *
+ * Still, we don't fire value change yet, but instead
+ * postpone it until "commit" is done. See setValue(Object,
+ * boolean) and commit().
+ */
+ readValueFromProperty(event);
+ valueWasModifiedByDataSourceDuringCommit = true;
+ }
+ } else if (!isModified()) {
+ readValueFromProperty(event);
+ fireValueChange(false);
+ }
+ }
+ }
+
+ private void readValueFromProperty(Property.ValueChangeEvent event) {
+ setInternalValue(convertFromDataSource(event.getProperty().getValue()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#getTabIndex()
+ */
+ @Override
+ public int getTabIndex() {
+ return getState().getTabIndex();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ getState().setTabIndex(tabIndex);
+ requestRepaint();
+ }
+
+ /**
+ * Returns the internal field value, which might not match the data source
+ * value e.g. if the field has been modified and is not in write-through
+ * mode.
+ *
+ * This method can be overridden by subclasses together with
+ * {@link #setInternalValue(Object)} to compute internal field value at
+ * runtime. When doing so, typically also {@link #isModified()} needs to be
+ * overridden and care should be taken in the management of the empty state
+ * and buffering support.
+ *
+ * @return internal field value
+ */
+ protected T getInternalValue() {
+ return value;
+ }
+
+ /**
+ * Sets the internal field value. This is purely used by AbstractField to
+ * change the internal Field value. It does not trigger valuechange events.
+ * It can be overridden by the inheriting classes to update all dependent
+ * variables.
+ *
+ * Subclasses can also override {@link #getInternalValue()} if necessary.
+ *
+ * @param newValue
+ * the new value to be set.
+ */
+ protected void setInternalValue(T newValue) {
+ value = newValue;
+ if (validators != null && !validators.isEmpty()) {
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+ @Override
+ public void attach() {
+ super.attach();
+
+ if (!isListeningToPropertyEvents) {
+ addPropertyListeners();
+ if (!isModified() && isReadThrough()) {
+ // Update value from data source
+ discard();
+ }
+ }
+ }
+
+ @Override
+ public void detach() {
+ super.detach();
+ // Stop listening to data source events on detach to avoid a potential
+ // memory leak. See #6155.
+ removePropertyListeners();
+ }
+
+ /**
+ * Is this field required. Required fields must filled by the user.
+ *
+ * If the field is required, it is visually indicated in the user interface.
+ * Furthermore, setting field to be required implicitly adds "non-empty"
+ * validator and thus isValid() == false or any isEmpty() fields. In those
+ * cases validation errors are not painted as it is obvious that the user
+ * must fill in the required fields.
+ *
+ * On the other hand, for the non-required fields isValid() == true if the
+ * field isEmpty() regardless of any attached validators.
+ *
+ *
+ * @return <code>true</code> if the field is required, otherwise
+ * <code>false</code>.
+ */
+ @Override
+ public boolean isRequired() {
+ return getState().isRequired();
+ }
+
+ /**
+ * Sets the field required. Required fields must filled by the user.
+ *
+ * If the field is required, it is visually indicated in the user interface.
+ * Furthermore, setting field to be required implicitly adds "non-empty"
+ * validator and thus isValid() == false or any isEmpty() fields. In those
+ * cases validation errors are not painted as it is obvious that the user
+ * must fill in the required fields.
+ *
+ * On the other hand, for the non-required fields isValid() == true if the
+ * field isEmpty() regardless of any attached validators.
+ *
+ * @param required
+ * Is the field required.
+ */
+ @Override
+ public void setRequired(boolean required) {
+ getState().setRequired(required);
+ requestRepaint();
+ }
+
+ /**
+ * Set the error that is show if this field is required, but empty. When
+ * setting requiredMessage to be "" or null, no error pop-up or exclamation
+ * mark is shown for a empty required field. This faults to "". Even in
+ * those cases isValid() returns false for empty required fields.
+ *
+ * @param requiredMessage
+ * Message to be shown when this field is required, but empty.
+ */
+ @Override
+ public void setRequiredError(String requiredMessage) {
+ requiredError = requiredMessage;
+ requestRepaint();
+ }
+
+ @Override
+ public String getRequiredError() {
+ return requiredError;
+ }
+
+ /**
+ * Gets the error that is shown if the field value cannot be converted to
+ * the data source type.
+ *
+ * @return The error that is shown if conversion of the field value fails
+ */
+ public String getConversionError() {
+ return conversionError;
+ }
+
+ /**
+ * Sets the error that is shown if the field value cannot be converted to
+ * the data source type. If {0} is present in the message, it will be
+ * replaced by the simple name of the data source type.
+ *
+ * @param valueConversionError
+ * Message to be shown when conversion of the value fails
+ */
+ public void setConversionError(String valueConversionError) {
+ this.conversionError = valueConversionError;
+ requestRepaint();
+ }
+
+ /**
+ * Is the field empty?
+ *
+ * In general, "empty" state is same as null. As an exception, TextField
+ * also treats empty string as "empty".
+ */
+ protected boolean isEmpty() {
+ return (getFieldValue() == null);
+ }
+
+ /**
+ * Is automatic, visible validation enabled?
+ *
+ * If automatic validation is enabled, any validators connected to this
+ * component are evaluated while painting the component and potential error
+ * messages are sent to client. If the automatic validation is turned off,
+ * isValid() and validate() methods still work, but one must show the
+ * validation in their own code.
+ *
+ * @return True, if automatic validation is enabled.
+ */
+ public boolean isValidationVisible() {
+ return validationVisible;
+ }
+
+ /**
+ * Enable or disable automatic, visible validation.
+ *
+ * If automatic validation is enabled, any validators connected to this
+ * component are evaluated while painting the component and potential error
+ * messages are sent to client. If the automatic validation is turned off,
+ * isValid() and validate() methods still work, but one must show the
+ * validation in their own code.
+ *
+ * @param validateAutomatically
+ * True, if automatic validation is enabled.
+ */
+ public void setValidationVisible(boolean validateAutomatically) {
+ if (validationVisible != validateAutomatically) {
+ requestRepaint();
+ validationVisible = validateAutomatically;
+ }
+ }
+
+ /**
+ * Sets the current buffered source exception.
+ *
+ * @param currentBufferedSourceException
+ */
+ public void setCurrentBufferedSourceException(
+ Buffered.SourceException currentBufferedSourceException) {
+ this.currentBufferedSourceException = currentBufferedSourceException;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the current buffered source exception.
+ *
+ * @return The current source exception
+ */
+ protected Buffered.SourceException getCurrentBufferedSourceException() {
+ return currentBufferedSourceException;
+ }
+
+ /**
+ * A ready-made {@link ShortcutListener} that focuses the given
+ * {@link Focusable} (usually a {@link Field}) when the keyboard shortcut is
+ * invoked.
+ *
+ */
+ public static class FocusShortcut extends ShortcutListener {
+ protected Focusable focusable;
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}
+ * using the shorthand notation defined in {@link ShortcutAction}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param shorthandCaption
+ * caption with keycode and modifiers indicated
+ */
+ public FocusShortcut(Focusable focusable, String shorthandCaption) {
+ super(shorthandCaption);
+ this.focusable = focusable;
+ }
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param keyCode
+ * keycode that invokes the shortcut
+ * @param modifiers
+ * modifiers required to invoke the shortcut
+ */
+ public FocusShortcut(Focusable focusable, int keyCode, int... modifiers) {
+ super(null, keyCode, modifiers);
+ this.focusable = focusable;
+ }
+
+ /**
+ * Creates a keyboard shortcut for focusing the given {@link Focusable}.
+ *
+ * @param focusable
+ * to focused when the shortcut is invoked
+ * @param keyCode
+ * keycode that invokes the shortcut
+ */
+ public FocusShortcut(Focusable focusable, int keyCode) {
+ this(focusable, keyCode, null);
+ }
+
+ @Override
+ public void handleAction(Object sender, Object target) {
+ focusable.focus();
+ }
+ }
+
+ /**
+ * Gets the converter used to convert the property data source value to the
+ * field value.
+ *
+ * @return The converter or null if none is set.
+ */
+ public Converter<T, Object> getConverter() {
+ return converter;
+ }
+
+ /**
+ * Sets the converter used to convert the field value to property data
+ * source type. The converter must have a presentation type that matches the
+ * field type.
+ *
+ * @param converter
+ * The new converter to use.
+ */
+ public void setConverter(Converter<T, ?> converter) {
+ this.converter = (Converter<T, Object>) converter;
+ requestRepaint();
+ }
+
+ @Override
+ public AbstractFieldState getState() {
+ return (AbstractFieldState) super.getState();
+ }
+
+ @Override
+ public void updateState() {
+ super.updateState();
+
+ // Hide the error indicator if needed
+ getState().setHideErrors(shouldHideErrors());
+ }
+
+ /**
+ * Registers this as an event listener for events sent by the data source
+ * (if any). Does nothing if
+ * <code>isListeningToPropertyEvents == true</code>.
+ */
+ private void addPropertyListeners() {
+ if (!isListeningToPropertyEvents) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .addListener(this);
+ }
+ isListeningToPropertyEvents = true;
+ }
+ }
+
+ /**
+ * Stops listening to events sent by the data source (if any). Does nothing
+ * if <code>isListeningToPropertyEvents == false</code>.
+ */
+ private void removePropertyListeners() {
+ if (isListeningToPropertyEvents) {
+ if (dataSource instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) {
+ ((Property.ReadOnlyStatusChangeNotifier) dataSource)
+ .removeListener(this);
+ }
+ isListeningToPropertyEvents = false;
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java
new file mode 100644
index 0000000000..5ec80573ab
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java
@@ -0,0 +1,165 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import com.vaadin.shared.ui.JavaScriptComponentState;
+import com.vaadin.terminal.JavaScriptCallbackHelper;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ui.JavaScriptWidget;
+
+/**
+ * Base class for Components with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript component is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * component. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyComponent</code> for the
+ * server-side
+ * <code>com.example.MyComponent extends AbstractJavaScriptComponent</code>
+ * class. If MyComponent instead extends <code>com.example.SuperComponent</code>
+ * , then <code>com_example_SuperComponent</code> will also be attempted if
+ * <code>com_example_MyComponent</code> has not been defined.
+ * <p>
+ * JavaScript components have a very simple GWT widget ({@link JavaScriptWidget}
+ * ) just consisting of a <code>div</code> element to which the JavaScript code
+ * should initialize its own user interface.
+ * <p>
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin with the following
+ * functions:
+ * <ul>
+ * <li><code>getConnectorId()</code> - returns a string with the id of the
+ * connector.</li>
+ * <li><code>getParentId([connectorId])</code> - returns a string with the id of
+ * the connector's parent. If <code>connectorId</code> is provided, the id of
+ * the parent of the corresponding connector with the passed id is returned
+ * instead.</li>
+ * <li><code>getElement([connectorId])</code> - returns the DOM Element that is
+ * the root of a connector's widget. <code>null</code> is returned if the
+ * connector can not be found or if the connector doesn't have a widget. If
+ * <code>connectorId</code> is not provided, the connector id of the current
+ * connector will be used.</li>
+ * <li><code>getState()</code> - returns an object corresponding to the shared
+ * state defined on the server. The scheme for conversion between Java and
+ * JavaScript types is described bellow.</li>
+ * <li><code>registerRpc([name, ] rpcObject)</code> - registers the
+ * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an
+ * object with field containing functions for all eligible RPC functions. If
+ * <code>name</code> is provided, the RPC handler will only used for RPC calls
+ * for the RPC interface with the same fully qualified Java name. If no
+ * <code>name</code> is provided, the RPC handler will be used for all incoming
+ * RPC invocations where the RPC method name is defined as a function field in
+ * the handler. The scheme for conversion between Java types in the RPC
+ * interface definition and the JavaScript values passed as arguments to the
+ * handler functions is described bellow.</li>
+ * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If
+ * <code>name</code> is provided, the proxy object will contain functions for
+ * all methods in the RPC interface with the same fully qualified name, provided
+ * a RPC handler has been registered by the server-side code. If no
+ * <code>name</code> is provided, the returned RPC proxy object will contain
+ * functions for all methods in all RPC interfaces registered for the connector
+ * on the server. If the same method name is present in multiple registered RPC
+ * interfaces, the corresponding function in the RPC proxy object will throw an
+ * exception when called. The scheme for conversion between Java types in the
+ * RPC interface and the JavaScript values that should be passed to the
+ * functions is described bellow.</li>
+ * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL
+ * that can be used in the browser. This is just way of accessing
+ * {@link ApplicationConnection#translateVaadinUri(String)}</li>
+ * </ul>
+ * The connector wrapper also supports these special functions:
+ * <ul>
+ * <li><code>onStateChange</code> - If the JavaScript code assigns a function to
+ * the field, that function is called whenever the contents of the shared state
+ * is changed.</li>
+ * <li>Any field name corresponding to a call to
+ * {@link #addFunction(String, JavaScriptFunction)} on the server will
+ * automatically be present as a function that triggers the registered function
+ * on the server.</li>
+ * <li>Any field name referred to using
+ * {@link #callFunction(String, Object...)} on the server will be called if a
+ * function has been assigned to the field.</li>
+ * </ul>
+ * <p>
+ *
+ * Values in the Shared State and in RPC calls are converted between Java and
+ * JavaScript using the following conventions:
+ * <ul>
+ * <li>Primitive Java numbers (byte, char, int, long, float, double) and their
+ * boxed types (Byte, Character, Integer, Long, Float, Double) are represented
+ * by JavaScript numbers.</li>
+ * <li>The primitive Java boolean and the boxed Boolean are represented by
+ * JavaScript booleans.</li>
+ * <li>Java Strings are represented by JavaScript strings.</li>
+ * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li>
+ * <li>Map<String, ?> in Java is represented by JavaScript object with fields
+ * corresponding to the map keys.</li>
+ * <li>Any other Java Map is represented by a JavaScript array containing two
+ * arrays, the first contains the keys and the second contains the values in the
+ * same order.</li>
+ * <li>A Java Bean is represented by a JavaScript object with fields
+ * corresponding to the bean's properties.</li>
+ * <li>A Java Connector is represented by a JavaScript string containing the
+ * connector's id.</li>
+ * <li>A pluggable serialization mechanism is provided for types not described
+ * here. Please refer to the documentation for specific types for serialization
+ * information.</li>
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public abstract class AbstractJavaScriptComponent extends AbstractComponent {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ @Override
+ protected <T> void registerRpc(T implementation, Class<T> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side function
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName, JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments should only contain
+ * data types that can be represented in JavaScript including primitives,
+ * their boxed types, arrays, String, List, Set, Map, Connector and
+ * JavaBeans.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ public JavaScriptComponentState getState() {
+ return (JavaScriptComponentState) super.getState();
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbstractLayout.java b/server/src/com/vaadin/ui/AbstractLayout.java
new file mode 100644
index 0000000000..7b3a537d06
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractLayout.java
@@ -0,0 +1,77 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.shared.ui.AbstractLayoutState;
+import com.vaadin.ui.Layout.MarginHandler;
+
+/**
+ * An abstract class that defines default implementation for the {@link Layout}
+ * interface.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public abstract class AbstractLayout extends AbstractComponentContainer
+ implements Layout, MarginHandler {
+
+ protected MarginInfo margins = new MarginInfo(false);
+
+ @Override
+ public AbstractLayoutState getState() {
+ return (AbstractLayoutState) super.getState();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout#setMargin(boolean)
+ */
+ @Override
+ public void setMargin(boolean enabled) {
+ margins.setMargins(enabled);
+ getState().setMarginsBitmask(margins.getBitMask());
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#getMargin()
+ */
+ @Override
+ public MarginInfo getMargin() {
+ return margins;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.MarginHandler#setMargin(MarginInfo)
+ */
+ @Override
+ public void setMargin(MarginInfo marginInfo) {
+ margins.setMargins(marginInfo);
+ getState().setMarginsBitmask(margins.getBitMask());
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout#setMargin(boolean, boolean, boolean, boolean)
+ */
+ @Override
+ public void setMargin(boolean topEnabled, boolean rightEnabled,
+ boolean bottomEnabled, boolean leftEnabled) {
+ margins.setMargins(topEnabled, rightEnabled, bottomEnabled, leftEnabled);
+ getState().setMarginsBitmask(margins.getBitMask());
+ requestRepaint();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/AbstractMedia.java b/server/src/com/vaadin/ui/AbstractMedia.java
new file mode 100644
index 0000000000..71b2e38ef3
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractMedia.java
@@ -0,0 +1,196 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.communication.URLReference;
+import com.vaadin.shared.ui.AbstractMediaState;
+import com.vaadin.shared.ui.MediaControl;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.server.ResourceReference;
+
+/**
+ * Abstract base class for the HTML5 media components.
+ *
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractMedia extends AbstractComponent {
+
+ @Override
+ public AbstractMediaState getState() {
+ return (AbstractMediaState) super.getState();
+ }
+
+ /**
+ * Sets a single media file as the source of the media component.
+ *
+ * @param source
+ */
+ public void setSource(Resource source) {
+ clearSources();
+
+ addSource(source);
+ }
+
+ private void clearSources() {
+ getState().getSources().clear();
+ getState().getSourceTypes().clear();
+ }
+
+ /**
+ * Adds an alternative media file to the sources list. Which of the sources
+ * is used is selected by the browser depending on which file formats it
+ * supports. See <a
+ * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a
+ * table of formats supported by different browsers.
+ *
+ * @param source
+ */
+ public void addSource(Resource source) {
+ if (source != null) {
+ getState().getSources().add(new ResourceReference(source));
+ getState().getSourceTypes().add(source.getMIMEType());
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Set multiple sources at once. Which of the sources is used is selected by
+ * the browser depending on which file formats it supports. See <a
+ * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a
+ * table of formats supported by different browsers.
+ *
+ * @param sources
+ */
+ public void setSources(Resource... sources) {
+ clearSources();
+ for (Resource source : sources) {
+ addSource(source);
+ }
+ }
+
+ /**
+ * @return The sources pointed to in this media.
+ */
+ public List<Resource> getSources() {
+ ArrayList<Resource> sources = new ArrayList<Resource>();
+ for (URLReference ref : getState().getSources()) {
+ sources.add(((ResourceReference) ref).getResource());
+ }
+ return sources;
+ }
+
+ /**
+ * Sets whether or not the browser should show native media controls.
+ *
+ * @param showControls
+ */
+ public void setShowControls(boolean showControls) {
+ getState().setShowControls(showControls);
+ requestRepaint();
+ }
+
+ /**
+ * @return true if the browser is to show native media controls.
+ */
+ public boolean isShowControls() {
+ return getState().isShowControls();
+ }
+
+ /**
+ * Sets the alternative text to be displayed if the browser does not support
+ * HTML5. This text is rendered as HTML if
+ * {@link #setHtmlContentAllowed(boolean)} is set to true. With HTML
+ * rendering, this method can also be used to implement fallback to a
+ * flash-based player, see the <a href=
+ * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash"
+ * >Mozilla Developer Network</a> for details.
+ *
+ * @param altText
+ */
+ public void setAltText(String altText) {
+ getState().setAltText(altText);
+ requestRepaint();
+ }
+
+ /**
+ * @return The text/html that is displayed when a browser doesn't support
+ * HTML5.
+ */
+ public String getAltText() {
+ return getState().getAltText();
+ }
+
+ /**
+ * Set whether the alternative text ({@link #setAltText(String)}) is
+ * rendered as HTML or not.
+ *
+ * @param htmlContentAllowed
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ getState().setHtmlContentAllowed(htmlContentAllowed);
+ requestRepaint();
+ }
+
+ /**
+ * @return true if the alternative text ({@link #setAltText(String)}) is to
+ * be rendered as HTML.
+ */
+ public boolean isHtmlContentAllowed() {
+ return getState().isHtmlContentAllowed();
+ }
+
+ /**
+ * Sets whether the media is to automatically start playback when enough
+ * data has been loaded.
+ *
+ * @param autoplay
+ */
+ public void setAutoplay(boolean autoplay) {
+ getState().setAutoplay(autoplay);
+ requestRepaint();
+ }
+
+ /**
+ * @return true if the media is set to automatically start playback.
+ */
+ public boolean isAutoplay() {
+ return getState().isAutoplay();
+ }
+
+ /**
+ * Set whether to mute the audio or not.
+ *
+ * @param muted
+ */
+ public void setMuted(boolean muted) {
+ getState().setMuted(muted);
+ requestRepaint();
+ }
+
+ /**
+ * @return true if the audio is muted.
+ */
+ public boolean isMuted() {
+ return getState().isMuted();
+ }
+
+ /**
+ * Pauses the media.
+ */
+ public void pause() {
+ getRpcProxy(MediaControl.class).pause();
+ }
+
+ /**
+ * Starts playback of the media.
+ */
+ public void play() {
+ getRpcProxy(MediaControl.class).play();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
new file mode 100644
index 0000000000..0581d0a279
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java
@@ -0,0 +1,383 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState;
+import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState.ChildComponentData;
+import com.vaadin.terminal.Sizeable;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+
+@SuppressWarnings("serial")
+public abstract class AbstractOrderedLayout extends AbstractLayout implements
+ Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier {
+
+ private AbstractOrderedLayoutServerRpc rpc = new AbstractOrderedLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(AbstractOrderedLayout.this,
+ mouseDetails, clickedConnector));
+ }
+ };
+
+ public static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT;
+
+ /**
+ * Custom layout slots containing the components.
+ */
+ protected LinkedList<Component> components = new LinkedList<Component>();
+
+ /* Child component alignments */
+
+ /**
+ * Mapping from components to alignments (horizontal + vertical).
+ */
+ public AbstractOrderedLayout() {
+ registerRpc(rpc);
+ }
+
+ @Override
+ public AbstractOrderedLayoutState getState() {
+ return (AbstractOrderedLayoutState) super.getState();
+ }
+
+ /**
+ * Add a component into this container. The component is added to the right
+ * or under the previous component.
+ *
+ * @param c
+ * the component to be added.
+ */
+ @Override
+ public void addComponent(Component c) {
+ // Add to components before calling super.addComponent
+ // so that it is available to AttachListeners
+ components.add(c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ componentAdded(c);
+ }
+
+ /**
+ * Adds a component into this container. The component is added to the left
+ * or on top of the other components.
+ *
+ * @param c
+ * the component to be added.
+ */
+ public void addComponentAsFirst(Component c) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (c.getParent() == this) {
+ removeComponent(c);
+ }
+ components.addFirst(c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ componentAdded(c);
+
+ }
+
+ /**
+ * Adds a component into indexed position in this container.
+ *
+ * @param c
+ * the component to be added.
+ * @param index
+ * the index of the component position. The components currently
+ * in and after the position are shifted forwards.
+ */
+ public void addComponent(Component c, int index) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (c.getParent() == this) {
+ // When c is removed, all components after it are shifted down
+ if (index > getComponentIndex(c)) {
+ index--;
+ }
+ removeComponent(c);
+ }
+ components.add(index, c);
+ try {
+ super.addComponent(c);
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+
+ componentAdded(c);
+ }
+
+ private void componentRemoved(Component c) {
+ getState().getChildData().remove(c);
+ requestRepaint();
+ }
+
+ private void componentAdded(Component c) {
+ getState().getChildData().put(c, new ChildComponentData());
+ requestRepaint();
+
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component c) {
+ components.remove(c);
+ super.removeComponent(c);
+ componentRemoved(c);
+ }
+
+ /**
+ * Gets the component container iterator for going trough all the components
+ * in the container.
+ *
+ * @return the Iterator of the components inside the container.
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return components.iterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ int oldLocation = -1;
+ int newLocation = -1;
+ int location = 0;
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+
+ location++;
+ }
+
+ if (oldLocation == -1) {
+ addComponent(newComponent);
+ } else if (newLocation == -1) {
+ removeComponent(oldComponent);
+ addComponent(newComponent, oldLocation);
+ } else {
+ // Both old and new are in the layout
+ if (oldLocation > newLocation) {
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ } else {
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ }
+
+ requestRepaint();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com
+ * .vaadin.ui.Component, int, int)
+ */
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ int horizontalAlignment, int verticalAlignment) {
+ Alignment a = new Alignment(horizontalAlignment + verticalAlignment);
+ setComponentAlignment(childComponent, a);
+ }
+
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ Alignment alignment) {
+ ChildComponentData childData = getState().getChildData().get(
+ childComponent);
+ if (childData != null) {
+ // Alignments are bit masks
+ childData.setAlignmentBitmask(alignment.getBitMask());
+ requestRepaint();
+ } else {
+ throw new IllegalArgumentException(
+ "Component must be added to layout before using setComponentAlignment()");
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com
+ * .vaadin.ui.Component)
+ */
+ @Override
+ public Alignment getComponentAlignment(Component childComponent) {
+ ChildComponentData childData = getState().getChildData().get(
+ childComponent);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ return new Alignment(childData.getAlignmentBitmask());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean)
+ */
+ @Override
+ public void setSpacing(boolean spacing) {
+ getState().setSpacing(spacing);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing()
+ */
+ @Override
+ public boolean isSpacing() {
+ return getState().isSpacing();
+ }
+
+ /**
+ * <p>
+ * This method is used to control how excess space in layout is distributed
+ * among components. Excess space may exist if layout is sized and contained
+ * non relatively sized components don't consume all available space.
+ *
+ * <p>
+ * Example how to distribute 1:3 (33%) for component1 and 2:3 (67%) for
+ * component2 :
+ *
+ * <code>
+ * layout.setExpandRatio(component1, 1);<br>
+ * layout.setExpandRatio(component2, 2);
+ * </code>
+ *
+ * <p>
+ * If no ratios have been set, the excess space is distributed evenly among
+ * all components.
+ *
+ * <p>
+ * Note, that width or height (depending on orientation) needs to be defined
+ * for this method to have any effect.
+ *
+ * @see Sizeable
+ *
+ * @param component
+ * the component in this layout which expand ratio is to be set
+ * @param ratio
+ */
+ public void setExpandRatio(Component component, float ratio) {
+ ChildComponentData childData = getState().getChildData().get(component);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ childData.setExpandRatio(ratio);
+ requestRepaint();
+ };
+
+ /**
+ * Returns the expand ratio of given component.
+ *
+ * @param component
+ * which expand ratios is requested
+ * @return expand ratio of given component, 0.0f by default.
+ */
+ public float getExpandRatio(Component component) {
+ ChildComponentData childData = getState().getChildData().get(component);
+ if (childData == null) {
+ throw new IllegalArgumentException(
+ "The given component is not a child of this layout");
+ }
+
+ return childData.getExpandRatio();
+ }
+
+ @Override
+ public void addListener(LayoutClickListener listener) {
+ addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ @Override
+ public void removeListener(LayoutClickListener listener) {
+ removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+ /**
+ * Returns the index of the given component.
+ *
+ * @param component
+ * The component to look up.
+ * @return The index of the component or -1 if the component is not a child.
+ */
+ public int getComponentIndex(Component component) {
+ return components.indexOf(component);
+ }
+
+ /**
+ * Returns the component at the given position.
+ *
+ * @param index
+ * The position of the component.
+ * @return The component at the given index.
+ * @throws IndexOutOfBoundsException
+ * If the index is out of range.
+ */
+ public Component getComponent(int index) throws IndexOutOfBoundsException {
+ return components.get(index);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java
new file mode 100644
index 0000000000..0a97ceb649
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractSelect.java
@@ -0,0 +1,2029 @@
+/*
+ * @VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor;
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.KeyMapper;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.ui.AbstractSelect.ItemCaptionMode;
+
+/**
+ * <p>
+ * A class representing a selection of items the user has selected in a UI. The
+ * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a
+ * {@link com.vaadin.data.Container}.
+ * </p>
+ *
+ * <p>
+ * A <code>Select</code> component may be in single- or multiselect mode.
+ * Multiselect mode means that more than one item can be selected
+ * simultaneously.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+// TODO currently cannot specify type more precisely in case of multi-select
+public abstract class AbstractSelect extends AbstractField<Object> implements
+ Container, Container.Viewer, Container.PropertySetChangeListener,
+ Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier,
+ Container.ItemSetChangeListener, Vaadin6Component {
+
+ public enum ItemCaptionMode {
+ /**
+ * Item caption mode: Item's ID's <code>String</code> representation is
+ * used as caption.
+ */
+ ID,
+ /**
+ * Item caption mode: Item's <code>String</code> representation is used
+ * as caption.
+ */
+ ITEM,
+ /**
+ * Item caption mode: Index of the item is used as caption. The index
+ * mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX,
+ /**
+ * Item caption mode: If an Item has a caption it's used, if not, Item's
+ * ID's <code>String</code> representation is used as caption. <b>This
+ * is the default</b>.
+ */
+ EXPLICIT_DEFAULTS_ID,
+ /**
+ * Item caption mode: Captions must be explicitly specified.
+ */
+ EXPLICIT,
+ /**
+ * Item caption mode: Only icons are shown, captions are hidden.
+ */
+ ICON_ONLY,
+ /**
+ * Item caption mode: Item captions are read from property specified
+ * with <code>setItemCaptionPropertyId</code>.
+ */
+ PROPERTY;
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY;
+
+ /**
+ * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_PROPERTY = ItemCaptionMode.PROPERTY;
+
+ /**
+ * Interface for option filtering, used to filter options based on user
+ * entered value. The value is matched to the item caption.
+ * <code>FILTERINGMODE_OFF</code> (0) turns the filtering off.
+ * <code>FILTERINGMODE_STARTSWITH</code> (1) matches from the start of the
+ * caption. <code>FILTERINGMODE_CONTAINS</code> (1) matches anywhere in the
+ * caption.
+ */
+ public interface Filtering extends Serializable {
+ public static final int FILTERINGMODE_OFF = 0;
+ public static final int FILTERINGMODE_STARTSWITH = 1;
+ public static final int FILTERINGMODE_CONTAINS = 2;
+
+ /**
+ * Sets the option filtering mode.
+ *
+ * @param filteringMode
+ * the filtering mode to use
+ */
+ public void setFilteringMode(int filteringMode);
+
+ /**
+ * Gets the current filtering mode.
+ *
+ * @return the filtering mode in use
+ */
+ public int getFilteringMode();
+
+ }
+
+ /**
+ * Multi select modes that controls how multi select behaves.
+ */
+ public enum MultiSelectMode {
+ /**
+ * The default behavior of the multi select mode
+ */
+ DEFAULT,
+
+ /**
+ * The previous more simple behavior of the multselect
+ */
+ SIMPLE
+ }
+
+ /**
+ * Is the select in multiselect mode?
+ */
+ private boolean multiSelect = false;
+
+ /**
+ * Select options.
+ */
+ protected Container items;
+
+ /**
+ * Is the user allowed to add new options?
+ */
+ private boolean allowNewOptions;
+
+ /**
+ * Keymapper used to map key values.
+ */
+ protected KeyMapper<Object> itemIdMapper = new KeyMapper<Object>();
+
+ /**
+ * Item icons.
+ */
+ private final HashMap<Object, Resource> itemIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Item captions.
+ */
+ private final HashMap<Object, String> itemCaptions = new HashMap<Object, String>();
+
+ /**
+ * Item caption mode.
+ */
+ private ItemCaptionMode itemCaptionMode = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Item caption source property id.
+ */
+ private Object itemCaptionPropertyId = null;
+
+ /**
+ * Item icon source property id.
+ */
+ private Object itemIconPropertyId = null;
+
+ /**
+ * List of property set change event listeners.
+ */
+ private Set<Container.PropertySetChangeListener> propertySetEventListeners = null;
+
+ /**
+ * List of item set change event listeners.
+ */
+ private Set<Container.ItemSetChangeListener> itemSetEventListeners = null;
+
+ /**
+ * Item id that represents null selection of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ */
+ private Object nullSelectionItemId = null;
+
+ // Null (empty) selection is enabled by default
+ private boolean nullSelectionAllowed = true;
+ private NewItemHandler newItemHandler;
+
+ // Caption (Item / Property) change listeners
+ CaptionChangeListener captionChangeListener;
+
+ /* Constructors */
+
+ /**
+ * Creates an empty Select. The caption is not used.
+ */
+ public AbstractSelect() {
+ setContainerDataSource(new IndexedContainer());
+ }
+
+ /**
+ * Creates an empty Select with caption.
+ */
+ public AbstractSelect(String caption) {
+ setContainerDataSource(new IndexedContainer());
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new select that is connected to a data-source.
+ *
+ * @param caption
+ * the Caption of the component.
+ * @param dataSource
+ * the Container datasource to be selected from by this select.
+ */
+ public AbstractSelect(String caption, Container dataSource) {
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /**
+ * Creates a new select that is filled from a collection of option values.
+ *
+ * @param caption
+ * the Caption of this field.
+ * @param options
+ * the Collection containing the options.
+ */
+ public AbstractSelect(String caption, Collection<?> options) {
+
+ // Creates the options container and add given options to it
+ final Container c = new IndexedContainer();
+ if (options != null) {
+ for (final Iterator<?> i = options.iterator(); i.hasNext();) {
+ c.addItem(i.next());
+ }
+ }
+
+ setCaption(caption);
+ setContainerDataSource(c);
+ }
+
+ /* Component methods */
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ // Paints select attributes
+ if (isMultiSelect()) {
+ target.addAttribute("selectmode", "multi");
+ }
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ if (getNullSelectionItemId() != null) {
+ target.addAttribute("nullselectitem", true);
+ }
+ }
+
+ // Constructs selected keys array
+ String[] selectedKeys;
+ if (isMultiSelect()) {
+ selectedKeys = new String[((Set<?>) getValue()).size()];
+ } else {
+ selectedKeys = new String[(getValue() == null
+ && getNullSelectionItemId() == null ? 0 : 1)];
+ }
+
+ // ==
+ // first remove all previous item/property listeners
+ getCaptionChangeListener().clear();
+ // Paints the options and create array of selected id keys
+
+ target.startTag("options");
+ int keyIndex = 0;
+ // Support for external null selection item id
+ final Collection<?> ids = getItemIds();
+ if (isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && !ids.contains(getNullSelectionItemId())) {
+ final Object id = getNullSelectionItemId();
+ // Paints option
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id)) {
+ selectedKeys[keyIndex++] = itemIdMapper.key(id);
+ }
+ target.endTag("so");
+ }
+
+ final Iterator<?> i = getItemIds().iterator();
+ // Paints the available selection options from data source
+ while (i.hasNext()) {
+ // Gets the option attribute values
+ final Object id = i.next();
+ if (!isNullSelectionAllowed() && id != null
+ && id.equals(getNullSelectionItemId())) {
+ // Remove item if it's the null selection item but null
+ // selection is not allowed
+ continue;
+ }
+ final String key = itemIdMapper.key(id);
+ // add listener for each item, to cause repaint if an item changes
+ getCaptionChangeListener().addNotifierForItem(id);
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id) && keyIndex < selectedKeys.length) {
+ selectedKeys[keyIndex++] = key;
+ }
+ target.endTag("so");
+ }
+ target.endTag("options");
+ // ==
+
+ // Paint variables
+ target.addVariable(this, "selected", selectedKeys);
+ if (isNewItemsAllowed()) {
+ target.addVariable(this, "newitem", "");
+ }
+
+ }
+
+ protected void paintItem(PaintTarget target, Object itemId)
+ throws PaintException {
+ final String key = itemIdMapper.key(itemId);
+ final String caption = getItemCaption(itemId);
+ final Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ target.addAttribute("icon", icon);
+ }
+ target.addAttribute("caption", caption);
+ if (itemId != null && itemId.equals(getNullSelectionItemId())) {
+ target.addAttribute("nullselection", true);
+ }
+ target.addAttribute("key", key);
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ // New option entered (and it is allowed)
+ if (isNewItemsAllowed()) {
+ final String newitem = (String) variables.get("newitem");
+ if (newitem != null && newitem.length() > 0) {
+ getNewItemHandler().addNewItem(newitem);
+ }
+ }
+
+ // Selection change
+ if (variables.containsKey("selected")) {
+ final String[] clientSideSelectedKeys = (String[]) variables
+ .get("selected");
+
+ // Multiselect mode
+ if (isMultiSelect()) {
+
+ // TODO Optimize by adding repaintNotNeeded when applicable
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> acceptedSelections = new LinkedList<Object>();
+ for (int i = 0; i < clientSideSelectedKeys.length; i++) {
+ final Object id = itemIdMapper
+ .get(clientSideSelectedKeys[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ requestRepaint();
+ } else if (id != null && containsId(id)) {
+ acceptedSelections.add(id);
+ }
+ }
+
+ if (!isNullSelectionAllowed() && acceptedSelections.size() < 1) {
+ // empty selection not allowed, keep old value
+ requestRepaint();
+ return;
+ }
+
+ // Limits the deselection to the set of visible items
+ // (non-visible items can not be deselected)
+ Collection<?> visibleNotSelected = getVisibleItemIds();
+ if (visibleNotSelected != null) {
+ visibleNotSelected = new HashSet<Object>(visibleNotSelected);
+ // Don't remove those that will be added to preserve order
+ visibleNotSelected.removeAll(acceptedSelections);
+
+ @SuppressWarnings("unchecked")
+ Set<Object> newsel = (Set<Object>) getValue();
+ if (newsel == null) {
+ newsel = new LinkedHashSet<Object>();
+ } else {
+ newsel = new LinkedHashSet<Object>(newsel);
+ }
+ newsel.removeAll(visibleNotSelected);
+ newsel.addAll(acceptedSelections);
+ setValue(newsel, true);
+ }
+ } else {
+ // Single select mode
+ if (!isNullSelectionAllowed()
+ && (clientSideSelectedKeys.length == 0
+ || clientSideSelectedKeys[0] == null || clientSideSelectedKeys[0] == getNullSelectionItemId())) {
+ requestRepaint();
+ return;
+ }
+ if (clientSideSelectedKeys.length == 0) {
+ // Allows deselection only if the deselected item is
+ // visible
+ final Object current = getValue();
+ final Collection<?> visible = getVisibleItemIds();
+ if (visible != null && visible.contains(current)) {
+ setValue(null, true);
+ }
+ } else {
+ final Object id = itemIdMapper
+ .get(clientSideSelectedKeys[0]);
+ if (!isNullSelectionAllowed() && id == null) {
+ requestRepaint();
+ } else if (id != null
+ && id.equals(getNullSelectionItemId())) {
+ setValue(null, true);
+ } else {
+ setValue(id, true);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * TODO refine doc Setter for new item handler that is called when user adds
+ * new item in newItemAllowed mode.
+ *
+ * @param newItemHandler
+ */
+ public void setNewItemHandler(NewItemHandler newItemHandler) {
+ this.newItemHandler = newItemHandler;
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * @return
+ */
+ public NewItemHandler getNewItemHandler() {
+ if (newItemHandler == null) {
+ newItemHandler = new DefaultNewItemHandler();
+ }
+ return newItemHandler;
+ }
+
+ public interface NewItemHandler extends Serializable {
+ void addNewItem(String newItemCaption);
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * This is a default class that handles adding new items that are typed by
+ * user to selects container.
+ *
+ * By extending this class one may implement some logic on new item addition
+ * like database inserts.
+ *
+ */
+ public class DefaultNewItemHandler implements NewItemHandler {
+ @Override
+ public void addNewItem(String newItemCaption) {
+ // Checks for readonly
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Adds new option
+ if (addItem(newItemCaption) != null) {
+
+ // Sets the caption property, if used
+ if (getItemCaptionPropertyId() != null) {
+ getContainerProperty(newItemCaption,
+ getItemCaptionPropertyId())
+ .setValue(newItemCaption);
+ }
+ if (isMultiSelect()) {
+ Set values = new HashSet((Collection) getValue());
+ values.add(newItemCaption);
+ setValue(values);
+ } else {
+ setValue(newItemCaption);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the visible item ids. In Select, this returns list of all item ids,
+ * but can be overriden in subclasses if they paint only part of the items
+ * to the terminal or null if no items is visible.
+ */
+ public Collection<?> getVisibleItemIds() {
+ return getItemIds();
+ }
+
+ /* Property methods */
+
+ /**
+ * Returns the type of the property. <code>getValue</code> and
+ * <code>setValue</code> methods must be compatible with this type: one can
+ * safely cast <code>getValue</code> to given type and pass any variable
+ * assignable to this type as a parameter to <code>setValue</code>.
+ *
+ * @return the Type of the property.
+ */
+ @Override
+ public Class<?> getType() {
+ if (isMultiSelect()) {
+ return Set.class;
+ } else {
+ return Object.class;
+ }
+ }
+
+ /**
+ * Gets the selected item id or in multiselect mode a set of selected ids.
+ *
+ * @see com.vaadin.ui.AbstractField#getValue()
+ */
+ @Override
+ public Object getValue() {
+ final Object retValue = super.getValue();
+
+ if (isMultiSelect()) {
+
+ // If the return value is not a set
+ if (retValue == null) {
+ return new HashSet<Object>();
+ }
+ if (retValue instanceof Set) {
+ return Collections.unmodifiableSet((Set<?>) retValue);
+ } else if (retValue instanceof Collection) {
+ return new HashSet<Object>((Collection<?>) retValue);
+ } else {
+ final Set<Object> s = new HashSet<Object>();
+ if (items.containsId(retValue)) {
+ s.add(retValue);
+ }
+ return s;
+ }
+
+ } else {
+ return retValue;
+ }
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+ if (newValue == getNullSelectionItemId()) {
+ newValue = null;
+ }
+
+ setValue(newValue, false);
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @param repaintIsNotNeeded
+ * True if caller is sure that repaint is not needed.
+ * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object,
+ * java.lang.Boolean)
+ */
+ @Override
+ protected void setValue(Object newValue, boolean repaintIsNotNeeded)
+ throws Property.ReadOnlyException {
+
+ if (isMultiSelect()) {
+ if (newValue == null) {
+ super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded);
+ } else if (Collection.class.isAssignableFrom(newValue.getClass())) {
+ super.setValue(new LinkedHashSet<Object>(
+ (Collection<?>) newValue), repaintIsNotNeeded);
+ }
+ } else if (newValue == null || items.containsId(newValue)) {
+ super.setValue(newValue, repaintIsNotNeeded);
+ }
+ }
+
+ /* Container methods */
+
+ /**
+ * Gets the item from the container with given id. If the container does not
+ * contain the requested item, null is returned.
+ *
+ * @param itemId
+ * the item id.
+ * @return the item from the container.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return items.getItem(itemId);
+ }
+
+ /**
+ * Gets the item Id collection from the container.
+ *
+ * @return the Collection of item ids.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return items.getItemIds();
+ }
+
+ /**
+ * Gets the property Id collection from the container.
+ *
+ * @return the Collection of property ids.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return items.getContainerPropertyIds();
+ }
+
+ /**
+ * Gets the property type.
+ *
+ * @param propertyId
+ * the Id identifying the property.
+ * @see com.vaadin.data.Container#getType(java.lang.Object)
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return items.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of items in the container.
+ *
+ * @return the Number of items in the container.
+ *
+ * @see com.vaadin.data.Container#size()
+ */
+ @Override
+ public int size() {
+ return items.size();
+ }
+
+ /**
+ * Tests, if the collection contains an item with given id.
+ *
+ * @param itemId
+ * the Id the of item to be tested.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ if (itemId != null) {
+ return items.containsId(itemId);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container
+ *
+ * @see com.vaadin.data.Container#getContainerProperty(Object, Object)
+ */
+ @Override
+ public Property<?> getContainerProperty(Object itemId, Object propertyId) {
+ return items.getContainerProperty(itemId, propertyId);
+ }
+
+ /**
+ * Adds the new property to all items. Adds a property with given id, type
+ * and default value to all items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.vaadin.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ final boolean retval = items.addContainerProperty(propertyId, type,
+ defaultValue);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Removes all items from the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+
+ final boolean retval = items.removeAllItems();
+ itemIdMapper.removeAll();
+ if (retval) {
+ setValue(null);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ }
+ return retval;
+ }
+
+ /**
+ * Creates a new item into container with container managed id. The id of
+ * the created new item is returned. The item can be fetched with getItem()
+ * method. if the creation fails, null is returned.
+ *
+ * @return the Id of the created item or null in case of failure.
+ * @see com.vaadin.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object retval = items.addItem();
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Create a new item into container. The created new item is returned and
+ * ready for setting property values. if the creation fails, null is
+ * returned. In case the container already contains the item, null is
+ * returned.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns null.
+ *
+ * @param itemId
+ * the Identification of the item to be created.
+ * @return the Created item with the given id, or null in case of failure.
+ * @see com.vaadin.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+
+ final Item retval = items.addItem(itemId);
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ unselect(itemId);
+ final boolean retval = items.removeItem(itemId);
+ itemIdMapper.remove(itemId);
+ if (retval && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Removes the property from all items. Removes a property with given id
+ * from all the items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.vaadin.data.Container#removeContainerProperty(java.lang.Object)
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ final boolean retval = items.removeContainerProperty(propertyId);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /* Container.Viewer methods */
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * As a side-effect the fields value (selection) is set to null due old
+ * selection not necessary exists in new Container.
+ *
+ * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container)
+ *
+ * @param newDataSource
+ * the new data source.
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+
+ getCaptionChangeListener().clear();
+
+ if (items != newDataSource) {
+
+ // Removes listeners from the old datasource
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items)
+ .removeListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .removeListener(this);
+ }
+ }
+
+ // Assigns new data source
+ items = newDataSource;
+
+ // Clears itemIdMapper also
+ itemIdMapper.removeAll();
+
+ // Adds listeners
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items).addListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .addListener(this);
+ }
+ }
+
+ /*
+ * We expect changing the data source should also clean value. See
+ * #810, #4607, #5281
+ */
+ setValue(null);
+
+ requestRepaint();
+
+ }
+ }
+
+ /**
+ * Gets the viewing data-source container.
+ *
+ * @see com.vaadin.data.Container.Viewer#getContainerDataSource()
+ */
+ @Override
+ public Container getContainerDataSource() {
+ return items;
+ }
+
+ /* Select attributes */
+
+ /**
+ * Is the select in multiselect mode? In multiselect mode
+ *
+ * @return the Value of property multiSelect.
+ */
+ public boolean isMultiSelect() {
+ return multiSelect;
+ }
+
+ /**
+ * Sets the multiselect mode. Setting multiselect mode false may lose
+ * selection information: if selected items set contains one or more
+ * selected items, only one of the selected items is kept as selected.
+ *
+ * Subclasses of AbstractSelect can choose not to support changing the
+ * multiselect mode, and may throw {@link UnsupportedOperationException}.
+ *
+ * @param multiSelect
+ * the New value of property multiSelect.
+ */
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect && getNullSelectionItemId() != null) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ if (multiSelect != this.multiSelect) {
+
+ // Selection before mode change
+ final Object oldValue = getValue();
+
+ this.multiSelect = multiSelect;
+
+ // Convert the value type
+ if (multiSelect) {
+ final Set<Object> s = new HashSet<Object>();
+ if (oldValue != null) {
+ s.add(oldValue);
+ }
+ setValue(s);
+ } else {
+ final Set<?> s = (Set<?>) oldValue;
+ if (s == null || s.isEmpty()) {
+ setValue(null);
+ } else {
+ // Set the single select to contain only the first
+ // selected value in the multiselect
+ setValue(s.iterator().next());
+ }
+ }
+
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Does the select allow adding new options by the user. If true, the new
+ * options can be added to the Container. The text entered by the user is
+ * used as id. Note that data-source must allow adding new items.
+ *
+ * @return True if additions are allowed.
+ */
+ public boolean isNewItemsAllowed() {
+
+ return allowNewOptions;
+ }
+
+ /**
+ * Enables or disables possibility to add new options by the user.
+ *
+ * @param allowNewOptions
+ * the New value of property allowNewOptions.
+ */
+ public void setNewItemsAllowed(boolean allowNewOptions) {
+
+ // Only handle change requests
+ if (this.allowNewOptions != allowNewOptions) {
+
+ this.allowNewOptions = allowNewOptions;
+
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Override the caption of an item. Setting caption explicitly overrides id,
+ * item and index captions.
+ *
+ * @param itemId
+ * the id of the item to be recaptioned.
+ * @param caption
+ * the New caption.
+ */
+ public void setItemCaption(Object itemId, String caption) {
+ if (itemId != null) {
+ itemCaptions.put(itemId, caption);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the caption of an item. The caption is generated as specified by the
+ * item caption mode. See <code>setItemCaptionMode()</code> for more
+ * details.
+ *
+ * @param itemId
+ * the id of the item to be queried.
+ * @return the caption for specified item.
+ */
+ public String getItemCaption(Object itemId) {
+
+ // Null items can not be found
+ if (itemId == null) {
+ return null;
+ }
+
+ String caption = null;
+
+ switch (getItemCaptionMode()) {
+
+ case ID:
+ caption = itemId.toString();
+ break;
+
+ case INDEX:
+ if (items instanceof Container.Indexed) {
+ caption = String.valueOf(((Container.Indexed) items)
+ .indexOfId(itemId));
+ } else {
+ caption = "ERROR: Container is not indexed";
+ }
+ break;
+
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i != null) {
+ caption = i.toString();
+ }
+ break;
+
+ case EXPLICIT:
+ caption = itemCaptions.get(itemId);
+ break;
+
+ case EXPLICIT_DEFAULTS_ID:
+ caption = itemCaptions.get(itemId);
+ if (caption == null) {
+ caption = itemId.toString();
+ }
+ break;
+
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null) {
+ Object value = p.getValue();
+ if (value != null) {
+ caption = value.toString();
+ }
+ }
+ break;
+ }
+
+ // All items must have some captions
+ return caption != null ? caption : "";
+ }
+
+ /**
+ * Sets tqhe icon for an item.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param icon
+ * the icon to use or null.
+ */
+ public void setItemIcon(Object itemId, Resource icon) {
+ if (itemId != null) {
+ if (icon == null) {
+ itemIcons.remove(itemId);
+ } else {
+ itemIcons.put(itemId, icon);
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the item icon.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @return the icon for the item or null, if not specified.
+ */
+ public Resource getItemIcon(Object itemId) {
+ final Resource explicit = itemIcons.get(itemId);
+ if (explicit != null) {
+ return explicit;
+ }
+
+ if (getItemIconPropertyId() == null) {
+ return null;
+ }
+
+ final Property<?> ip = getContainerProperty(itemId,
+ getItemIconPropertyId());
+ if (ip == null) {
+ return null;
+ }
+ final Object icon = ip.getValue();
+ if (icon instanceof Resource) {
+ return (Resource) icon;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the item caption mode.
+ *
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items
+ * Id-objects <code>toString</code> is used as item caption. If caption is
+ * explicitly specified, it overrides the id-caption.
+ * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used
+ * as item caption. The index mode can only be used with the containers
+ * implementing <code>Container.Indexed</code> interface.</li>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be
+ * explicitly specified.</li>
+ * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read
+ * from property, that must be specified with
+ * <code>setItemCaptionPropertyId</code>.</li>
+ * </ul>
+ * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default
+ * mode.
+ * </p>
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setItemCaptionMode(ItemCaptionMode mode) {
+ if (mode != null) {
+ itemCaptionMode = mode;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the item caption mode.
+ *
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items
+ * Id-objects <code>toString</code> is used as item caption. If caption is
+ * explicitly specified, it overrides the id-caption.
+ * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used
+ * as item caption. The index mode can only be used with the containers
+ * implementing <code>Container.Indexed</code> interface.</li>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be
+ * explicitly specified.</li>
+ * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read
+ * from property, that must be specified with
+ * <code>setItemCaptionPropertyId</code>.</li>
+ * </ul>
+ * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default
+ * mode.
+ * </p>
+ *
+ * @return the One of the modes listed above.
+ */
+ public ItemCaptionMode getItemCaptionMode() {
+ return itemCaptionMode;
+ }
+
+ /**
+ * Sets the item caption property.
+ *
+ * <p>
+ * Setting the id to a existing property implicitly sets the item caption
+ * mode to <code>ITEM_CAPTION_MODE_PROPERTY</code>. If the object is in
+ * <code>ITEM_CAPTION_MODE_PROPERTY</code> mode, setting caption property id
+ * null resets the item caption mode to
+ * <code>ITEM_CAPTION_EXPLICIT_DEFAULTS_ID</code>.
+ * </p>
+ * <p>
+ * Note that the type of the property used for caption must be String
+ * </p>
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property.
+ *
+ */
+ public void setItemCaptionPropertyId(Object propertyId) {
+ if (propertyId != null) {
+ itemCaptionPropertyId = propertyId;
+ setItemCaptionMode(ITEM_CAPTION_MODE_PROPERTY);
+ requestRepaint();
+ } else {
+ itemCaptionPropertyId = null;
+ if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) {
+ setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID);
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the item caption property.
+ *
+ * @return the Id of the property used as item caption source.
+ */
+ public Object getItemCaptionPropertyId() {
+ return itemCaptionPropertyId;
+ }
+
+ /**
+ * Sets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Resource.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property that specifies icons for items or null
+ * @throws IllegalArgumentException
+ * If the propertyId is not in the container or is not of a
+ * valid type
+ */
+ public void setItemIconPropertyId(Object propertyId)
+ throws IllegalArgumentException {
+ if (propertyId == null) {
+ itemIconPropertyId = null;
+ } else if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "Property id not found in the container");
+ } else if (Resource.class.isAssignableFrom(getType(propertyId))) {
+ itemIconPropertyId = propertyId;
+ } else {
+ throw new IllegalArgumentException(
+ "Property type must be assignable to Resource");
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Gets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Icon.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @return the Id of the property containing the item icons.
+ */
+ public Object getItemIconPropertyId() {
+ return itemIconPropertyId;
+ }
+
+ /**
+ * Tests if an item is selected.
+ *
+ * <p>
+ * In single select mode testing selection status of the item identified by
+ * {@link #getNullSelectionItemId()} returns true if the value of the
+ * property is null.
+ * </p>
+ *
+ * @param itemId
+ * the Id the of the item to be tested.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public boolean isSelected(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ if (isMultiSelect()) {
+ return ((Set<?>) getValue()).contains(itemId);
+ } else {
+ final Object value = getValue();
+ return itemId.equals(value == null ? getNullSelectionItemId()
+ : value);
+ }
+ }
+
+ /**
+ * Selects an item.
+ *
+ * <p>
+ * In single select mode selecting item identified by
+ * {@link #getNullSelectionItemId()} sets the value of the property to null.
+ * </p>
+ *
+ * @param itemId
+ * the identifier of Item to be selected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void select(Object itemId) {
+ if (!isMultiSelect()) {
+ setValue(itemId);
+ } else if (!isSelected(itemId) && itemId != null
+ && items.containsId(itemId)) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.add(itemId);
+ setValue(s);
+ }
+ }
+
+ /**
+ * Unselects an item.
+ *
+ * @param itemId
+ * the identifier of the Item to be unselected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void unselect(Object itemId) {
+ if (isSelected(itemId)) {
+ if (isMultiSelect()) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.remove(itemId);
+ setValue(s);
+ } else {
+ setValue(null);
+ }
+ }
+ }
+
+ /**
+ * Notifies this listener that the Containers contents has changed.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent)
+ */
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ firePropertySetChange();
+ }
+
+ /**
+ * Adds a new Property set change listener for this Container.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void addListener(Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners == null) {
+ propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>();
+ }
+ propertySetEventListeners.add(listener);
+ }
+
+ /**
+ * Removes a previously registered Property set change listener.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners != null) {
+ propertySetEventListeners.remove(listener);
+ if (propertySetEventListeners.isEmpty()) {
+ propertySetEventListeners = null;
+ }
+ }
+ }
+
+ /**
+ * Adds an Item set change listener for the object.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void addListener(Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners == null) {
+ itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>();
+ }
+ itemSetEventListeners.add(listener);
+ }
+
+ /**
+ * Removes the Item set change listener from the object.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners != null) {
+ itemSetEventListeners.remove(listener);
+ if (itemSetEventListeners.isEmpty()) {
+ itemSetEventListeners = null;
+ }
+ }
+ }
+
+ @Override
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Container.ItemSetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (itemSetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(itemSetEventListeners);
+ }
+ } else if (Container.PropertySetChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (propertySetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetEventListeners);
+ }
+ }
+
+ return super.getListeners(eventType);
+ }
+
+ /**
+ * Lets the listener know a Containers Item set has changed.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent)
+ */
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ // Clears the item id mapping table
+ itemIdMapper.removeAll();
+
+ // Notify all listeners
+ fireItemSetChange();
+ }
+
+ /**
+ * Fires the property set change event.
+ */
+ protected void firePropertySetChange() {
+ if (propertySetEventListeners != null
+ && !propertySetEventListeners.isEmpty()) {
+ final Container.PropertySetChangeEvent event = new PropertySetChangeEvent();
+ final Object[] listeners = propertySetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.PropertySetChangeListener) listeners[i])
+ .containerPropertySetChange(event);
+ }
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Fires the item set change event.
+ */
+ protected void fireItemSetChange() {
+ if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) {
+ final Container.ItemSetChangeEvent event = new ItemSetChangeEvent();
+ final Object[] listeners = itemSetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.ItemSetChangeListener) listeners[i])
+ .containerItemSetChange(event);
+ }
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Implementation of item set change event.
+ */
+ private class ItemSetChangeEvent implements Serializable,
+ Container.ItemSetChangeEvent {
+
+ /**
+ * Gets the Property where the event occurred.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return AbstractSelect.this;
+ }
+
+ }
+
+ /**
+ * Implementation of property set change event.
+ */
+ private class PropertySetChangeEvent implements
+ Container.PropertySetChangeEvent, Serializable {
+
+ /**
+ * Retrieves the Container whose contents have been modified.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return AbstractSelect.this;
+ }
+
+ }
+
+ /**
+ * For multi-selectable fields, also an empty collection of values is
+ * considered to be an empty field.
+ *
+ * @see AbstractField#isEmpty().
+ */
+ @Override
+ protected boolean isEmpty() {
+ if (!multiSelect) {
+ return super.isEmpty();
+ } else {
+ Object value = getValue();
+ return super.isEmpty()
+ || (value instanceof Collection && ((Collection<?>) value)
+ .isEmpty());
+ }
+ }
+
+ /**
+ * Allow or disallow empty selection by the user. If the select is in
+ * single-select mode, you can make an item represent the empty selection by
+ * calling <code>setNullSelectionItemId()</code>. This way you can for
+ * instance set an icon and caption for the null selection item.
+ *
+ * @param nullSelectionAllowed
+ * whether or not to allow empty selection
+ * @see #setNullSelectionItemId(Object)
+ * @see #isNullSelectionAllowed()
+ */
+ public void setNullSelectionAllowed(boolean nullSelectionAllowed) {
+ if (nullSelectionAllowed != this.nullSelectionAllowed) {
+ this.nullSelectionAllowed = nullSelectionAllowed;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Checks if null empty selection is allowed by the user.
+ *
+ * @return whether or not empty selection is allowed
+ * @see #setNullSelectionAllowed(boolean)
+ */
+ public boolean isNullSelectionAllowed() {
+ return nullSelectionAllowed;
+ }
+
+ /**
+ * Returns the item id that represents null value of this select in single
+ * select mode.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @return the Object Null value item id.
+ * @see #setNullSelectionItemId(Object)
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public Object getNullSelectionItemId() {
+ return nullSelectionItemId;
+ }
+
+ /**
+ * Sets the item id that represents null value of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @param nullSelectionItemId
+ * the nullSelectionItemId to set.
+ * @see #getNullSelectionItemId()
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public void setNullSelectionItemId(Object nullSelectionItemId) {
+ if (nullSelectionItemId != null && isMultiSelect()) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ this.nullSelectionItemId = nullSelectionItemId;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.AbstractField#attach()
+ */
+ @Override
+ public void attach() {
+ super.attach();
+ }
+
+ /**
+ * Detaches the component from application.
+ *
+ * @see com.vaadin.ui.AbstractComponent#detach()
+ */
+ @Override
+ public void detach() {
+ getCaptionChangeListener().clear();
+ super.detach();
+ }
+
+ // Caption change listener
+ protected CaptionChangeListener getCaptionChangeListener() {
+ if (captionChangeListener == null) {
+ captionChangeListener = new CaptionChangeListener();
+ }
+ return captionChangeListener;
+ }
+
+ /**
+ * This is a listener helper for Item and Property changes that should cause
+ * a repaint. It should be attached to all items that are displayed, and the
+ * default implementation does this in paintContent(). Especially
+ * "lazyloading" components should take care to add and remove listeners as
+ * appropriate. Call addNotifierForItem() for each painted item (and
+ * remember to clear).
+ *
+ * NOTE: singleton, use getCaptionChangeListener().
+ *
+ */
+ protected class CaptionChangeListener implements
+ Item.PropertySetChangeListener, Property.ValueChangeListener {
+
+ // TODO clean this up - type is either Item.PropertySetChangeNotifier or
+ // Property.ValueChangeNotifier
+ HashSet<Object> captionChangeNotifiers = new HashSet<Object>();
+
+ public void addNotifierForItem(Object itemId) {
+ switch (getItemCaptionMode()) {
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i == null) {
+ return;
+ }
+ if (i instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) i)
+ .addListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(i);
+ }
+ Collection<?> pids = i.getItemPropertyIds();
+ if (pids != null) {
+ for (Iterator<?> it = pids.iterator(); it.hasNext();) {
+ Property<?> p = i.getItemProperty(it.next());
+ if (p != null
+ && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ }
+
+ }
+ break;
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ break;
+
+ }
+ }
+
+ public void clear() {
+ for (Iterator<Object> it = captionChangeNotifiers.iterator(); it
+ .hasNext();) {
+ Object notifier = it.next();
+ if (notifier instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) notifier)
+ .removeListener(getCaptionChangeListener());
+ } else {
+ ((Property.ValueChangeNotifier) notifier)
+ .removeListener(getCaptionChangeListener());
+ }
+ }
+ captionChangeNotifiers.clear();
+ }
+
+ @Override
+ public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
+ requestRepaint();
+ }
+
+ @Override
+ public void itemPropertySetChange(
+ com.vaadin.data.Item.PropertySetChangeEvent event) {
+ requestRepaint();
+ }
+
+ }
+
+ /**
+ * Criterion which accepts a drop only if the drop target is (one of) the
+ * given Item identifier(s). Criterion can be used only on a drop targets
+ * that extends AbstractSelect like {@link Table} and {@link Tree}. The
+ * target and identifiers of valid Items are given in constructor.
+ *
+ * @since 6.3
+ */
+ public static class TargetItemIs extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select implementation that is used as a drop target
+ * @param itemId
+ * the identifier(s) that are valid drop locations
+ */
+ public TargetItemIs(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ if (dropTargetData.getTarget() != select) {
+ return false;
+ }
+ return itemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ }
+
+ /**
+ * Abstract helper class to implement item id based criterion.
+ *
+ * Note, inner class used not to open itemIdMapper for public access.
+ *
+ * @since 6.3
+ *
+ */
+ private static abstract class AbstractItemSetCriterion extends
+ ClientSideCriterion {
+ protected final Collection<Object> itemIds = new HashSet<Object>();
+ protected AbstractSelect select;
+
+ public AbstractItemSetCriterion(AbstractSelect select, Object... itemId) {
+ if (itemIds == null || select == null) {
+ throw new IllegalArgumentException(
+ "Accepted item identifiers must be accepted.");
+ }
+ Collections.addAll(itemIds, itemId);
+ this.select = select;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ String[] keys = new String[itemIds.size()];
+ int i = 0;
+ for (Object itemId : itemIds) {
+ String key = select.itemIdMapper.key(itemId);
+ keys[i++] = key;
+ }
+ target.addAttribute("keys", keys);
+ target.addAttribute("s", select);
+ }
+
+ }
+
+ /**
+ * This criterion accepts a only a {@link Transferable} that contains given
+ * Item (practically its identifier) from a specific AbstractSelect.
+ *
+ * @since 6.3
+ */
+ public static class AcceptItem extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select from which the item id's are checked
+ * @param itemId
+ * the item identifier(s) of the select that are accepted
+ */
+ public AcceptItem(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ DataBoundTransferable transferable = (DataBoundTransferable) dragEvent
+ .getTransferable();
+ if (transferable.getSourceComponent() != select) {
+ return false;
+ }
+ return itemIds.contains(transferable.getItemId());
+ }
+
+ /**
+ * A simple accept criterion which ensures that {@link Transferable}
+ * contains an {@link Item} (or actually its identifier). In other words
+ * the criterion check that drag is coming from a {@link Container} like
+ * {@link Tree} or {@link Table}.
+ */
+ public static final ClientSideCriterion ALL = new ContainsDataFlavor(
+ "itemId");
+
+ }
+
+ /**
+ * TargetDetails implementation for subclasses of {@link AbstractSelect}
+ * that implement {@link DropTarget}.
+ *
+ * @since 6.3
+ */
+ public class AbstractSelectTargetDetails extends TargetDetailsImpl {
+
+ /**
+ * The item id over which the drag event happened.
+ */
+ protected Object idOver;
+
+ /**
+ * Constructor that automatically converts itemIdOver key to
+ * corresponding item Id
+ *
+ */
+ protected AbstractSelectTargetDetails(Map<String, Object> rawVariables) {
+ super(rawVariables, (DropTarget) AbstractSelect.this);
+ // eagar fetch itemid, mapper may be emptied
+ String keyover = (String) getData("itemIdOver");
+ if (keyover != null) {
+ idOver = itemIdMapper.get(keyover);
+ }
+ }
+
+ /**
+ * If the drag operation is currently over an {@link Item}, this method
+ * returns the identifier of that {@link Item}.
+ *
+ */
+ public Object getItemIdOver() {
+ return idOver;
+ }
+
+ /**
+ * Returns a detailed vertical location where the drop happened on Item.
+ */
+ public VerticalDropLocation getDropLocation() {
+ String detail = (String) getData("detail");
+ if (detail == null) {
+ return null;
+ }
+ return VerticalDropLocation.valueOf(detail);
+ }
+
+ }
+
+ /**
+ * An accept criterion to accept drops only on a specific vertical location
+ * of an item.
+ * <p>
+ * This accept criterion is currently usable in Tree and Table
+ * implementations.
+ */
+ public static class VerticalLocationIs extends TargetDetailIs {
+ public static VerticalLocationIs TOP = new VerticalLocationIs(
+ VerticalDropLocation.TOP);
+ public static VerticalLocationIs BOTTOM = new VerticalLocationIs(
+ VerticalDropLocation.BOTTOM);
+ public static VerticalLocationIs MIDDLE = new VerticalLocationIs(
+ VerticalDropLocation.MIDDLE);
+
+ private VerticalLocationIs(VerticalDropLocation l) {
+ super("detail", l.name());
+ }
+ }
+
+ /**
+ * Implement this interface and pass it to Tree.setItemDescriptionGenerator
+ * or Table.setItemDescriptionGenerator to generate mouse over descriptions
+ * ("tooltips") for the rows and cells in Table or for the items in Tree.
+ */
+ public interface ItemDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell (and row) is painted or a item is painted
+ * in Tree
+ *
+ * @param source
+ * The source of the generator, the Tree or Table the
+ * generator is attached to
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row
+ * description
+ * @return The description or "tooltip" of the item.
+ */
+ public String generateDescription(Component source, Object itemId,
+ Object propertyId);
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbstractSplitPanel.java b/server/src/com/vaadin/ui/AbstractSplitPanel.java
new file mode 100644
index 0000000000..90dc38ff65
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractSplitPanel.java
@@ -0,0 +1,521 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+
+import com.vaadin.event.ComponentEventListener;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState;
+import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState;
+import com.vaadin.terminal.Sizeable;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.tools.ReflectTools;
+
+/**
+ * AbstractSplitPanel.
+ *
+ * <code>AbstractSplitPanel</code> is base class for a component container that
+ * can contain two components. The components are split by a divider element.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.5
+ */
+public abstract class AbstractSplitPanel extends AbstractComponentContainer {
+
+ // TODO use Unit in AbstractSplitPanelState and remove these
+ private Unit posUnit;
+ private Unit posMinUnit;
+ private Unit posMaxUnit;
+
+ private AbstractSplitPanelRpc rpc = new AbstractSplitPanelRpc() {
+
+ @Override
+ public void splitterClick(MouseEventDetails mouseDetails) {
+ fireEvent(new SplitterClickEvent(AbstractSplitPanel.this,
+ mouseDetails));
+ }
+
+ @Override
+ public void setSplitterPosition(float position) {
+ getSplitterState().setPosition(position);
+ }
+ };
+
+ public AbstractSplitPanel() {
+ registerRpc(rpc);
+ setSplitPosition(50, Unit.PERCENTAGE, false);
+ setSplitPositionLimits(0, Unit.PERCENTAGE, 100, Unit.PERCENTAGE);
+ }
+
+ /**
+ * Modifiable and Serializable Iterator for the components, used by
+ * {@link AbstractSplitPanel#getComponentIterator()}.
+ */
+ private class ComponentIterator implements Iterator<Component>,
+ Serializable {
+
+ int i = 0;
+
+ @Override
+ public boolean hasNext() {
+ if (i < getComponentCount()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Component next() {
+ if (!hasNext()) {
+ return null;
+ }
+ i++;
+ if (i == 1) {
+ return (getFirstComponent() == null ? getSecondComponent()
+ : getFirstComponent());
+ } else if (i == 2) {
+ return getSecondComponent();
+ }
+ return null;
+ }
+
+ @Override
+ public void remove() {
+ if (i == 1) {
+ if (getFirstComponent() != null) {
+ setFirstComponent(null);
+ i = 0;
+ } else {
+ setSecondComponent(null);
+ }
+ } else if (i == 2) {
+ setSecondComponent(null);
+ }
+ }
+ }
+
+ /**
+ * Add a component into this container. The component is added to the right
+ * or under the previous component.
+ *
+ * @param c
+ * the component to be added.
+ */
+
+ @Override
+ public void addComponent(Component c) {
+ if (getFirstComponent() == null) {
+ setFirstComponent(c);
+ } else if (getSecondComponent() == null) {
+ setSecondComponent(c);
+ } else {
+ throw new UnsupportedOperationException(
+ "Split panel can contain only two components");
+ }
+ }
+
+ /**
+ * Sets the first component of this split panel. Depending on the direction
+ * the first component is shown at the top or to the left.
+ *
+ * @param c
+ * The component to use as first component
+ */
+ public void setFirstComponent(Component c) {
+ if (getFirstComponent() == c) {
+ // Nothing to do
+ return;
+ }
+
+ if (getFirstComponent() != null) {
+ // detach old
+ removeComponent(getFirstComponent());
+ }
+ getState().setFirstChild(c);
+ if (c != null) {
+ super.addComponent(c);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Sets the second component of this split panel. Depending on the direction
+ * the second component is shown at the bottom or to the left.
+ *
+ * @param c
+ * The component to use as first component
+ */
+ public void setSecondComponent(Component c) {
+ if (getSecondComponent() == c) {
+ // Nothing to do
+ return;
+ }
+
+ if (getSecondComponent() != null) {
+ // detach old
+ removeComponent(getSecondComponent());
+ }
+ getState().setSecondChild(c);
+ if (c != null) {
+ super.addComponent(c);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Gets the first component of this split panel. Depending on the direction
+ * this is either the component shown at the top or to the left.
+ *
+ * @return the first component of this split panel
+ */
+ public Component getFirstComponent() {
+ return (Component) getState().getFirstChild();
+ }
+
+ /**
+ * Gets the second component of this split panel. Depending on the direction
+ * this is either the component shown at the top or to the left.
+ *
+ * @return the second component of this split panel
+ */
+ public Component getSecondComponent() {
+ return (Component) getState().getSecondChild();
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+
+ @Override
+ public void removeComponent(Component c) {
+ super.removeComponent(c);
+ if (c == getFirstComponent()) {
+ getState().setFirstChild(null);
+ } else if (c == getSecondComponent()) {
+ getState().setSecondChild(null);
+ }
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.ComponentContainer#getComponentIterator()
+ */
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return new ComponentIterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components (zero, one or two)
+ */
+
+ @Override
+ public int getComponentCount() {
+ int count = 0;
+ if (getFirstComponent() != null) {
+ count++;
+ }
+ if (getSecondComponent() != null) {
+ count++;
+ }
+ return count;
+ }
+
+ /* Documented in superclass */
+
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+ if (oldComponent == getFirstComponent()) {
+ setFirstComponent(newComponent);
+ } else if (oldComponent == getSecondComponent()) {
+ setSecondComponent(newComponent);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Moves the position of the splitter.
+ *
+ * @param pos
+ * the new size of the first region in the unit that was last
+ * used (default is percentage). Fractions are only allowed when
+ * unit is percentage.
+ */
+ public void setSplitPosition(float pos) {
+ setSplitPosition(pos, posUnit, false);
+ }
+
+ /**
+ * Moves the position of the splitter.
+ *
+ * @param pos
+ * the new size of the region in the unit that was last used
+ * (default is percentage). Fractions are only allowed when unit
+ * is percentage.
+ *
+ * @param reverse
+ * if set to true the split splitter position is measured by the
+ * second region else it is measured by the first region
+ */
+ public void setSplitPosition(float pos, boolean reverse) {
+ setSplitPosition(pos, posUnit, reverse);
+ }
+
+ /**
+ * Moves the position of the splitter with given position and unit.
+ *
+ * @param pos
+ * the new size of the first region. Fractions are only allowed
+ * when unit is percentage.
+ * @param unit
+ * the unit (from {@link Sizeable}) in which the size is given.
+ */
+ public void setSplitPosition(float pos, Unit unit) {
+ setSplitPosition(pos, unit, false);
+ }
+
+ /**
+ * Moves the position of the splitter with given position and unit.
+ *
+ * @param pos
+ * the new size of the first region. Fractions are only allowed
+ * when unit is percentage.
+ * @param unit
+ * the unit (from {@link Sizeable}) in which the size is given.
+ * @param reverse
+ * if set to true the split splitter position is measured by the
+ * second region else it is measured by the first region
+ *
+ */
+ public void setSplitPosition(float pos, Unit unit, boolean reverse) {
+ if (unit != Unit.PERCENTAGE && unit != Unit.PIXELS) {
+ throw new IllegalArgumentException(
+ "Only percentage and pixel units are allowed");
+ }
+ if (unit != Unit.PERCENTAGE) {
+ pos = Math.round(pos);
+ }
+ SplitterState splitterState = getSplitterState();
+ splitterState.setPosition(pos);
+ splitterState.setPositionUnit(unit.getSymbol());
+ splitterState.setPositionReversed(reverse);
+ posUnit = unit;
+
+ requestRepaint();
+ }
+
+ /**
+ * Returns the current position of the splitter, in
+ * {@link #getSplitPositionUnit()} units.
+ *
+ * @return position of the splitter
+ */
+ public float getSplitPosition() {
+ return getSplitterState().getPosition();
+ }
+
+ /**
+ * Returns the unit of position of the splitter
+ *
+ * @return unit of position of the splitter
+ */
+ public Unit getSplitPositionUnit() {
+ return posUnit;
+ }
+
+ /**
+ * Sets the minimum split position to the given position and unit. If the
+ * split position is reversed, maximum and minimum are also reversed.
+ *
+ * @param pos
+ * the minimum position of the split
+ * @param unit
+ * the unit (from {@link Sizeable}) in which the size is given.
+ * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS
+ */
+ public void setMinSplitPosition(int pos, Unit unit) {
+ setSplitPositionLimits(pos, unit, getSplitterState().getMaxPosition(),
+ posMaxUnit);
+ }
+
+ /**
+ * Returns the current minimum position of the splitter, in
+ * {@link #getMinSplitPositionUnit()} units.
+ *
+ * @return the minimum position of the splitter
+ */
+ public float getMinSplitPosition() {
+ return getSplitterState().getMinPosition();
+ }
+
+ /**
+ * Returns the unit of the minimum position of the splitter.
+ *
+ * @return the unit of the minimum position of the splitter
+ */
+ public Unit getMinSplitPositionUnit() {
+ return posMinUnit;
+ }
+
+ /**
+ * Sets the maximum split position to the given position and unit. If the
+ * split position is reversed, maximum and minimum are also reversed.
+ *
+ * @param pos
+ * the maximum position of the split
+ * @param unit
+ * the unit (from {@link Sizeable}) in which the size is given.
+ * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS
+ */
+ public void setMaxSplitPosition(float pos, Unit unit) {
+ setSplitPositionLimits(getSplitterState().getMinPosition(), posMinUnit,
+ pos, unit);
+ }
+
+ /**
+ * Returns the current maximum position of the splitter, in
+ * {@link #getMaxSplitPositionUnit()} units.
+ *
+ * @return the maximum position of the splitter
+ */
+ public float getMaxSplitPosition() {
+ return getSplitterState().getMaxPosition();
+ }
+
+ /**
+ * Returns the unit of the maximum position of the splitter
+ *
+ * @return the unit of the maximum position of the splitter
+ */
+ public Unit getMaxSplitPositionUnit() {
+ return posMaxUnit;
+ }
+
+ /**
+ * Sets the maximum and minimum position of the splitter. If the split
+ * position is reversed, maximum and minimum are also reversed.
+ *
+ * @param minPos
+ * the new minimum position
+ * @param minPosUnit
+ * the unit (from {@link Sizeable}) in which the minimum position
+ * is given.
+ * @param maxPos
+ * the new maximum position
+ * @param maxPosUnit
+ * the unit (from {@link Sizeable}) in which the maximum position
+ * is given.
+ */
+ private void setSplitPositionLimits(float minPos, Unit minPosUnit,
+ float maxPos, Unit maxPosUnit) {
+ if ((minPosUnit != Unit.PERCENTAGE && minPosUnit != Unit.PIXELS)
+ || (maxPosUnit != Unit.PERCENTAGE && maxPosUnit != Unit.PIXELS)) {
+ throw new IllegalArgumentException(
+ "Only percentage and pixel units are allowed");
+ }
+
+ SplitterState state = getSplitterState();
+
+ state.setMinPosition(minPos);
+ state.setMinPositionUnit(minPosUnit.getSymbol());
+ posMinUnit = minPosUnit;
+
+ state.setMaxPosition(maxPos);
+ state.setMaxPositionUnit(maxPosUnit.getSymbol());
+ posMaxUnit = maxPosUnit;
+
+ requestRepaint();
+ }
+
+ /**
+ * Lock the SplitPanels position, disabling the user from dragging the split
+ * handle.
+ *
+ * @param locked
+ * Set <code>true</code> if locked, <code>false</code> otherwise.
+ */
+ public void setLocked(boolean locked) {
+ getSplitterState().setLocked(locked);
+ requestRepaint();
+ }
+
+ /**
+ * Is the SplitPanel handle locked (user not allowed to change split
+ * position by dragging).
+ *
+ * @return <code>true</code> if locked, <code>false</code> otherwise.
+ */
+ public boolean isLocked() {
+ return getSplitterState().isLocked();
+ }
+
+ /**
+ * <code>SplitterClickListener</code> interface for listening for
+ * <code>SplitterClickEvent</code> fired by a <code>SplitPanel</code>.
+ *
+ * @see SplitterClickEvent
+ * @since 6.2
+ */
+ public interface SplitterClickListener extends ComponentEventListener {
+
+ public static final Method clickMethod = ReflectTools.findMethod(
+ SplitterClickListener.class, "splitterClick",
+ SplitterClickEvent.class);
+
+ /**
+ * SplitPanel splitter has been clicked
+ *
+ * @param event
+ * SplitterClickEvent event.
+ */
+ public void splitterClick(SplitterClickEvent event);
+ }
+
+ public class SplitterClickEvent extends ClickEvent {
+
+ public SplitterClickEvent(Component source,
+ MouseEventDetails mouseEventDetails) {
+ super(source, mouseEventDetails);
+ }
+
+ }
+
+ public void addListener(SplitterClickListener listener) {
+ addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER,
+ SplitterClickEvent.class, listener,
+ SplitterClickListener.clickMethod);
+ }
+
+ public void removeListener(SplitterClickListener listener) {
+ removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER,
+ SplitterClickEvent.class, listener);
+ }
+
+ @Override
+ public AbstractSplitPanelState getState() {
+ return (AbstractSplitPanelState) super.getState();
+ }
+
+ private SplitterState getSplitterState() {
+ return getState().getSplitterState();
+ }
+}
diff --git a/server/src/com/vaadin/ui/AbstractTextField.java b/server/src/com/vaadin/ui/AbstractTextField.java
new file mode 100644
index 0000000000..2326c07d97
--- /dev/null
+++ b/server/src/com/vaadin/ui/AbstractTextField.java
@@ -0,0 +1,674 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Map;
+
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.BlurNotifier;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.FieldEvents.FocusNotifier;
+import com.vaadin.event.FieldEvents.TextChangeEvent;
+import com.vaadin.event.FieldEvents.TextChangeListener;
+import com.vaadin.event.FieldEvents.TextChangeNotifier;
+import com.vaadin.shared.ui.textfield.AbstractTextFieldState;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.textfield.VTextField;
+
+public abstract class AbstractTextField extends AbstractField<String> implements
+ BlurNotifier, FocusNotifier, TextChangeNotifier, Vaadin6Component {
+
+ /**
+ * Null representation.
+ */
+ private String nullRepresentation = "null";
+ /**
+ * Is setting to null from non-null value allowed by setting with null
+ * representation .
+ */
+ private boolean nullSettingAllowed = false;
+ /**
+ * The text content when the last messages to the server was sent. Cleared
+ * when value is changed.
+ */
+ private String lastKnownTextContent;
+
+ /**
+ * The position of the cursor when the last message to the server was sent.
+ */
+ private int lastKnownCursorPosition;
+
+ /**
+ * Flag indicating that a text change event is pending to be triggered.
+ * Cleared by {@link #setInternalValue(Object)} and when the event is fired.
+ */
+ private boolean textChangeEventPending;
+
+ private boolean isFiringTextChangeEvent = false;
+
+ private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY;
+
+ private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400;
+
+ private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT;
+
+ /**
+ * Temporarily holds the new selection position. Cleared on paint.
+ */
+ private int selectionPosition = -1;
+
+ /**
+ * Temporarily holds the new selection length.
+ */
+ private int selectionLength;
+
+ /**
+ * Flag used to determine whether we are currently handling a state change
+ * triggered by a user. Used to properly fire text change event before value
+ * change event triggered by the client side.
+ */
+ private boolean changingVariables;
+
+ protected AbstractTextField() {
+ super();
+ }
+
+ @Override
+ public AbstractTextFieldState getState() {
+ return (AbstractTextFieldState) super.getState();
+ }
+
+ @Override
+ public void updateState() {
+ super.updateState();
+
+ String value = getValue();
+ if (value == null) {
+ value = getNullRepresentation();
+ }
+ getState().setText(value);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ if (selectionPosition != -1) {
+ target.addAttribute("selpos", selectionPosition);
+ target.addAttribute("sellen", selectionLength);
+ selectionPosition = -1;
+ }
+
+ if (hasListeners(TextChangeEvent.class)) {
+ target.addAttribute(VTextField.ATTR_TEXTCHANGE_EVENTMODE,
+ getTextChangeEventMode().toString());
+ target.addAttribute(VTextField.ATTR_TEXTCHANGE_TIMEOUT,
+ getTextChangeTimeout());
+ if (lastKnownTextContent != null) {
+ /*
+ * The field has be repainted for some reason (e.g. caption,
+ * size, stylename), but the value has not been changed since
+ * the last text change event. Let the client side know about
+ * the value the server side knows. Client side may then ignore
+ * the actual value, depending on its state.
+ */
+ target.addAttribute(
+ VTextField.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS, true);
+ }
+ }
+
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ changingVariables = true;
+
+ try {
+
+ if (variables.containsKey(VTextField.VAR_CURSOR)) {
+ Integer object = (Integer) variables.get(VTextField.VAR_CURSOR);
+ lastKnownCursorPosition = object.intValue();
+ }
+
+ if (variables.containsKey(VTextField.VAR_CUR_TEXT)) {
+ /*
+ * NOTE, we might want to develop this further so that on a
+ * value change event the whole text content don't need to be
+ * sent from the client to server. Just "commit" the value from
+ * currentText to the value.
+ */
+ handleInputEventTextChange(variables);
+ }
+
+ // Sets the text
+ if (variables.containsKey("text") && !isReadOnly()) {
+
+ // Only do the setting if the string representation of the value
+ // has been updated
+ String newValue = (String) variables.get("text");
+
+ // server side check for max length
+ if (getMaxLength() != -1 && newValue.length() > getMaxLength()) {
+ newValue = newValue.substring(0, getMaxLength());
+ }
+ final String oldValue = getValue();
+ if (newValue != null
+ && (oldValue == null || isNullSettingAllowed())
+ && newValue.equals(getNullRepresentation())) {
+ newValue = null;
+ }
+ if (newValue != oldValue
+ && (newValue == null || !newValue.equals(oldValue))) {
+ boolean wasModified = isModified();
+ setValue(newValue, true);
+
+ // If the modified status changes, or if we have a
+ // formatter, repaint is needed after all.
+ if (wasModified != isModified()) {
+ requestRepaint();
+ }
+ }
+ }
+ firePendingTextChangeEvent();
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+ } finally {
+ changingVariables = false;
+
+ }
+
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Gets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'.
+ * </p>
+ *
+ * @return the String Textual representation for null strings.
+ * @see TextField#isNullSettingAllowed()
+ */
+ public String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Is setting nulls with null-string representation allowed.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false
+ * </p>
+ *
+ * @return boolean Should the null-string represenation be always converted
+ * to null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public boolean isNullSettingAllowed() {
+ return nullSettingAllowed;
+ }
+
+ /**
+ * Sets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'
+ * </p>
+ *
+ * @param nullRepresentation
+ * Textual representation for null strings.
+ * @see TextField#setNullSettingAllowed(boolean)
+ */
+ public void setNullRepresentation(String nullRepresentation) {
+ this.nullRepresentation = nullRepresentation;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the null conversion mode.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false.
+ * </p>
+ *
+ * @param nullSettingAllowed
+ * Should the null-string representation always be converted to
+ * null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public void setNullSettingAllowed(boolean nullSettingAllowed) {
+ this.nullSettingAllowed = nullSettingAllowed;
+ requestRepaint();
+ }
+
+ @Override
+ protected boolean isEmpty() {
+ return super.isEmpty() || getValue().length() == 0;
+ }
+
+ /**
+ * Returns the maximum number of characters in the field. Value -1 is
+ * considered unlimited. Terminal may however have some technical limits.
+ *
+ * @return the maxLength
+ */
+ public int getMaxLength() {
+ return getState().getMaxLength();
+ }
+
+ /**
+ * Sets the maximum number of characters in the field. Value -1 is
+ * considered unlimited. Terminal may however have some technical limits.
+ *
+ * @param maxLength
+ * the maxLength to set
+ */
+ public void setMaxLength(int maxLength) {
+ getState().setMaxLength(maxLength);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ *
+ * @return the number of columns in the editor.
+ */
+ public int getColumns() {
+ return getState().getColumns();
+ }
+
+ /**
+ * Sets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ *
+ * @param columns
+ * the number of columns to set.
+ */
+ public void setColumns(int columns) {
+ if (columns < 0) {
+ columns = 0;
+ }
+ getState().setColumns(columns);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the current input prompt.
+ *
+ * @see #setInputPrompt(String)
+ * @return the current input prompt, or null if not enabled
+ */
+ public String getInputPrompt() {
+ return getState().getInputPrompt();
+ }
+
+ /**
+ * Sets the input prompt - a textual prompt that is displayed when the field
+ * would otherwise be empty, to prompt the user for input.
+ *
+ * @param inputPrompt
+ */
+ public void setInputPrompt(String inputPrompt) {
+ getState().setInputPrompt(inputPrompt);
+ requestRepaint();
+ }
+
+ /* ** Text Change Events ** */
+
+ private void firePendingTextChangeEvent() {
+ if (textChangeEventPending && !isFiringTextChangeEvent) {
+ isFiringTextChangeEvent = true;
+ textChangeEventPending = false;
+ try {
+ fireEvent(new TextChangeEventImpl(this));
+ } finally {
+ isFiringTextChangeEvent = false;
+ }
+ }
+ }
+
+ @Override
+ protected void setInternalValue(String newValue) {
+ if (changingVariables && !textChangeEventPending) {
+
+ /*
+ * TODO check for possible (minor?) issue (not tested)
+ *
+ * -field with e.g. PropertyFormatter.
+ *
+ * -TextChangeListener and it changes value.
+ *
+ * -if formatter again changes the value, do we get an extra
+ * simulated text change event ?
+ */
+
+ /*
+ * Fire a "simulated" text change event before value change event if
+ * change is coming from the client side.
+ *
+ * Iff there is both value change and textChangeEvent in same
+ * variable burst, it is a text field in non immediate mode and the
+ * text change event "flushed" queued value change event. In this
+ * case textChangeEventPending flag is already on and text change
+ * event will be fired after the value change event.
+ */
+ if (newValue == null && lastKnownTextContent != null
+ && !lastKnownTextContent.equals(getNullRepresentation())) {
+ // Value was changed from something to null representation
+ lastKnownTextContent = getNullRepresentation();
+ textChangeEventPending = true;
+ } else if (newValue != null
+ && !newValue.toString().equals(lastKnownTextContent)) {
+ // Value was changed to something else than null representation
+ lastKnownTextContent = newValue.toString();
+ textChangeEventPending = true;
+ }
+ firePendingTextChangeEvent();
+ }
+
+ super.setInternalValue(newValue);
+ }
+
+ @Override
+ public void setValue(Object newValue) throws ReadOnlyException {
+ super.setValue(newValue);
+ /*
+ * Make sure w reset lastKnownTextContent field on value change. The
+ * clearing must happen here as well because TextChangeListener can
+ * revert the original value. Client must respect the value in this
+ * case. AbstractField optimizes value change if the existing value is
+ * reset. Also we need to force repaint if the flag is on.
+ */
+ if (lastKnownTextContent != null) {
+ lastKnownTextContent = null;
+ requestRepaint();
+ }
+ }
+
+ private void handleInputEventTextChange(Map<String, Object> variables) {
+ /*
+ * TODO we could vastly optimize the communication of values by using
+ * some sort of diffs instead of always sending the whole text content.
+ * Also on value change events we could use the mechanism.
+ */
+ String object = (String) variables.get(VTextField.VAR_CUR_TEXT);
+ lastKnownTextContent = object;
+ textChangeEventPending = true;
+ }
+
+ /**
+ * Sets the mode how the TextField triggers {@link TextChangeEvent}s.
+ *
+ * @param inputEventMode
+ * the new mode
+ *
+ * @see TextChangeEventMode
+ */
+ public void setTextChangeEventMode(TextChangeEventMode inputEventMode) {
+ textChangeEventMode = inputEventMode;
+ requestRepaint();
+ }
+
+ /**
+ * @return the mode used to trigger {@link TextChangeEvent}s.
+ */
+ public TextChangeEventMode getTextChangeEventMode() {
+ return textChangeEventMode;
+ }
+
+ /**
+ * Different modes how the TextField can trigger {@link TextChangeEvent}s.
+ */
+ public enum TextChangeEventMode {
+
+ /**
+ * An event is triggered on each text content change, most commonly key
+ * press events.
+ */
+ EAGER,
+ /**
+ * Each text change event in the UI causes the event to be communicated
+ * to the application after a timeout. The length of the timeout can be
+ * controlled with {@link TextField#setInputEventTimeout(int)}. Only the
+ * last input event is reported to the server side if several text
+ * change events happen during the timeout.
+ * <p>
+ * In case of a {@link ValueChangeEvent} the schedule is not kept
+ * strictly. Before a {@link ValueChangeEvent} a {@link TextChangeEvent}
+ * is triggered if the text content has changed since the previous
+ * TextChangeEvent regardless of the schedule.
+ */
+ TIMEOUT,
+ /**
+ * An event is triggered when there is a pause of text modifications.
+ * The length of the pause can be modified with
+ * {@link TextField#setInputEventTimeout(int)}. Like with the
+ * {@link #TIMEOUT} mode, an event is forced before
+ * {@link ValueChangeEvent}s, even if the user did not keep a pause
+ * while entering the text.
+ * <p>
+ * This is the default mode.
+ */
+ LAZY
+ }
+
+ @Override
+ public void addListener(TextChangeListener listener) {
+ addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
+ listener, TextChangeListener.EVENT_METHOD);
+ }
+
+ @Override
+ public void removeListener(TextChangeListener listener) {
+ removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
+ listener);
+ }
+
+ /**
+ * The text change timeout modifies how often text change events are
+ * communicated to the application when {@link #getTextChangeEventMode()} is
+ * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}.
+ *
+ *
+ * @see #getTextChangeEventMode()
+ *
+ * @param timeout
+ * the timeout in milliseconds
+ */
+ public void setTextChangeTimeout(int timeout) {
+ textChangeEventTimeout = timeout;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the timeout used to fire {@link TextChangeEvent}s when the
+ * {@link #getTextChangeEventMode()} is {@link TextChangeEventMode#LAZY} or
+ * {@link TextChangeEventMode#TIMEOUT}.
+ *
+ * @return the timeout value in milliseconds
+ */
+ public int getTextChangeTimeout() {
+ return textChangeEventTimeout;
+ }
+
+ public class TextChangeEventImpl extends TextChangeEvent {
+ private String curText;
+ private int cursorPosition;
+
+ private TextChangeEventImpl(final AbstractTextField tf) {
+ super(tf);
+ curText = tf.getCurrentTextContent();
+ cursorPosition = tf.getCursorPosition();
+ }
+
+ @Override
+ public AbstractTextField getComponent() {
+ return (AbstractTextField) super.getComponent();
+ }
+
+ @Override
+ public String getText() {
+ return curText;
+ }
+
+ @Override
+ public int getCursorPosition() {
+ return cursorPosition;
+ }
+
+ }
+
+ /**
+ * Gets the current (or the last known) text content in the field.
+ * <p>
+ * Note the text returned by this method is not necessary the same that is
+ * returned by the {@link #getValue()} method. The value is updated when the
+ * terminal fires a value change event via e.g. blurring the field or by
+ * pressing enter. The value returned by this method is updated also on
+ * {@link TextChangeEvent}s. Due to this high dependency to the terminal
+ * implementation this method is (at least at this point) not published.
+ *
+ * @return the text which is currently displayed in the field.
+ */
+ private String getCurrentTextContent() {
+ if (lastKnownTextContent != null) {
+ return lastKnownTextContent;
+ } else {
+ Object text = getValue();
+ if (text == null) {
+ return getNullRepresentation();
+ }
+ return text.toString();
+ }
+ }
+
+ /**
+ * Selects all text in the field.
+ *
+ * @since 6.4
+ */
+ public void selectAll() {
+ String text = getValue() == null ? "" : getValue().toString();
+ setSelectionRange(0, text.length());
+ }
+
+ /**
+ * Sets the range of text to be selected.
+ *
+ * As a side effect the field will become focused.
+ *
+ * @since 6.4
+ *
+ * @param pos
+ * the position of the first character to be selected
+ * @param length
+ * the number of characters to be selected
+ */
+ public void setSelectionRange(int pos, int length) {
+ selectionPosition = pos;
+ selectionLength = length;
+ focus();
+ requestRepaint();
+ }
+
+ /**
+ * Sets the cursor position in the field. As a side effect the field will
+ * become focused.
+ *
+ * @since 6.4
+ *
+ * @param pos
+ * the position for the cursor
+ * */
+ public void setCursorPosition(int pos) {
+ setSelectionRange(pos, 0);
+ lastKnownCursorPosition = pos;
+ }
+
+ /**
+ * Returns the last known cursor position of the field.
+ *
+ * <p>
+ * Note that due to the client server nature or the GWT terminal, Vaadin
+ * cannot provide the exact value of the cursor position in most situations.
+ * The value is updated only when the client side terminal communicates to
+ * TextField, like on {@link ValueChangeEvent}s and {@link TextChangeEvent}
+ * s. This may change later if a deep push integration is built to Vaadin.
+ *
+ * @return the cursor position
+ */
+ public int getCursorPosition() {
+ return lastKnownCursorPosition;
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Accordion.java b/server/src/com/vaadin/ui/Accordion.java
new file mode 100644
index 0000000000..b937c7bc2b
--- /dev/null
+++ b/server/src/com/vaadin/ui/Accordion.java
@@ -0,0 +1,19 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+/**
+ * An accordion is a component similar to a {@link TabSheet}, but with a
+ * vertical orientation and the selected component presented between tabs.
+ *
+ * Closable tabs are not supported by the accordion.
+ *
+ * The {@link Accordion} can be styled with the .v-accordion, .v-accordion-item,
+ * .v-accordion-item-first and .v-accordion-item-caption styles.
+ *
+ * @see TabSheet
+ */
+public class Accordion extends TabSheet {
+
+}
diff --git a/server/src/com/vaadin/ui/Alignment.java b/server/src/com/vaadin/ui/Alignment.java
new file mode 100644
index 0000000000..0d73da8504
--- /dev/null
+++ b/server/src/com/vaadin/ui/Alignment.java
@@ -0,0 +1,158 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.AlignmentInfo.Bits;
+
+/**
+ * Class containing information about alignment of a component. Use the
+ * pre-instantiated classes.
+ */
+@SuppressWarnings("serial")
+public final class Alignment implements Serializable {
+
+ public static final Alignment TOP_RIGHT = new Alignment(Bits.ALIGNMENT_TOP
+ + Bits.ALIGNMENT_RIGHT);
+ public static final Alignment TOP_LEFT = new Alignment(Bits.ALIGNMENT_TOP
+ + Bits.ALIGNMENT_LEFT);
+ public static final Alignment TOP_CENTER = new Alignment(Bits.ALIGNMENT_TOP
+ + Bits.ALIGNMENT_HORIZONTAL_CENTER);
+ public static final Alignment MIDDLE_RIGHT = new Alignment(
+ Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_RIGHT);
+ public static final Alignment MIDDLE_LEFT = new Alignment(
+ Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_LEFT);
+ public static final Alignment MIDDLE_CENTER = new Alignment(
+ Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_HORIZONTAL_CENTER);
+ public static final Alignment BOTTOM_RIGHT = new Alignment(
+ Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_RIGHT);
+ public static final Alignment BOTTOM_LEFT = new Alignment(
+ Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_LEFT);
+ public static final Alignment BOTTOM_CENTER = new Alignment(
+ Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_HORIZONTAL_CENTER);
+
+ private final int bitMask;
+
+ public Alignment(int bitMask) {
+ this.bitMask = bitMask;
+ }
+
+ /**
+ * Returns a bitmask representation of the alignment value. Used internally
+ * by terminal.
+ *
+ * @return the bitmask representation of the alignment value
+ */
+ public int getBitMask() {
+ return bitMask;
+ }
+
+ /**
+ * Checks if component is aligned to the top of the available space.
+ *
+ * @return true if aligned top
+ */
+ public boolean isTop() {
+ return (bitMask & Bits.ALIGNMENT_TOP) == Bits.ALIGNMENT_TOP;
+ }
+
+ /**
+ * Checks if component is aligned to the bottom of the available space.
+ *
+ * @return true if aligned bottom
+ */
+ public boolean isBottom() {
+ return (bitMask & Bits.ALIGNMENT_BOTTOM) == Bits.ALIGNMENT_BOTTOM;
+ }
+
+ /**
+ * Checks if component is aligned to the left of the available space.
+ *
+ * @return true if aligned left
+ */
+ public boolean isLeft() {
+ return (bitMask & Bits.ALIGNMENT_LEFT) == Bits.ALIGNMENT_LEFT;
+ }
+
+ /**
+ * Checks if component is aligned to the right of the available space.
+ *
+ * @return true if aligned right
+ */
+ public boolean isRight() {
+ return (bitMask & Bits.ALIGNMENT_RIGHT) == Bits.ALIGNMENT_RIGHT;
+ }
+
+ /**
+ * Checks if component is aligned middle (vertically center) of the
+ * available space.
+ *
+ * @return true if aligned bottom
+ */
+ public boolean isMiddle() {
+ return (bitMask & Bits.ALIGNMENT_VERTICAL_CENTER) == Bits.ALIGNMENT_VERTICAL_CENTER;
+ }
+
+ /**
+ * Checks if component is aligned center (horizontally) of the available
+ * space.
+ *
+ * @return true if aligned center
+ */
+ public boolean isCenter() {
+ return (bitMask & Bits.ALIGNMENT_HORIZONTAL_CENTER) == Bits.ALIGNMENT_HORIZONTAL_CENTER;
+ }
+
+ /**
+ * Returns string representation of vertical alignment.
+ *
+ * @return vertical alignment as CSS value
+ */
+ public String getVerticalAlignment() {
+ if (isBottom()) {
+ return "bottom";
+ } else if (isMiddle()) {
+ return "middle";
+ }
+ return "top";
+ }
+
+ /**
+ * Returns string representation of horizontal alignment.
+ *
+ * @return horizontal alignment as CSS value
+ */
+ public String getHorizontalAlignment() {
+ if (isRight()) {
+ return "right";
+ } else if (isCenter()) {
+ return "center";
+ }
+ return "left";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if ((obj == null) || (obj.getClass() != this.getClass())) {
+ return false;
+ }
+ Alignment a = (Alignment) obj;
+ return bitMask == a.bitMask;
+ }
+
+ @Override
+ public int hashCode() {
+ return bitMask;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(bitMask);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Audio.java b/server/src/com/vaadin/ui/Audio.java
new file mode 100644
index 0000000000..ac2ee869a6
--- /dev/null
+++ b/server/src/com/vaadin/ui/Audio.java
@@ -0,0 +1,55 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.terminal.Resource;
+
+/**
+ * The Audio component translates into an HTML5 &lt;audio&gt; element and as
+ * such is only supported in browsers that support HTML5 media markup. Browsers
+ * that do not support HTML5 display the text or HTML set by calling
+ * {@link #setAltText(String)}.
+ *
+ * A flash-player fallback can be implemented by setting HTML content allowed (
+ * {@link #setHtmlContentAllowed(boolean)} and calling
+ * {@link #setAltText(String)} with the flash player markup. An example of flash
+ * fallback can be found at the <a href=
+ * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash"
+ * >Mozilla Developer Network</a>.
+ *
+ * Multiple sources can be specified. Which of the sources is used is selected
+ * by the browser depending on which file formats it supports. See <a
+ * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a
+ * table of formats supported by different browsers.
+ *
+ * @author Vaadin Ltd
+ * @since 6.7.0
+ */
+public class Audio extends AbstractMedia {
+
+ public Audio() {
+ this("", null);
+ }
+
+ /**
+ * @param caption
+ * The caption of the audio component.
+ */
+ public Audio(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * @param caption
+ * The caption of the audio component
+ * @param source
+ * The audio file to play.
+ */
+ public Audio(String caption, Resource source) {
+ setCaption(caption);
+ setSource(source);
+ setShowControls(true);
+ }
+}
diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java
new file mode 100644
index 0000000000..0cb667d527
--- /dev/null
+++ b/server/src/com/vaadin/ui/Button.java
@@ -0,0 +1,539 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+
+import com.vaadin.event.Action;
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.ShortcutAction;
+import com.vaadin.event.ShortcutAction.KeyCode;
+import com.vaadin.event.ShortcutAction.ModifierKey;
+import com.vaadin.event.ShortcutListener;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.button.ButtonServerRpc;
+import com.vaadin.shared.ui.button.ButtonState;
+import com.vaadin.tools.ReflectTools;
+import com.vaadin.ui.Component.Focusable;
+
+/**
+ * A generic button component.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Button extends AbstractComponent implements
+ FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Focusable,
+ Action.ShortcutNotifier {
+
+ private ButtonServerRpc rpc = new ButtonServerRpc() {
+
+ @Override
+ public void click(MouseEventDetails mouseEventDetails) {
+ fireClick(mouseEventDetails);
+ }
+
+ @Override
+ public void disableOnClick() {
+ // Could be optimized so the button is not repainted because of
+ // this (client side has already disabled the button)
+ setEnabled(false);
+ }
+ };
+
+ FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) {
+
+ @Override
+ protected void fireEvent(Event event) {
+ Button.this.fireEvent(event);
+ }
+ };
+
+ /**
+ * Creates a new push button.
+ */
+ public Button() {
+ registerRpc(rpc);
+ registerRpc(focusBlurRpc);
+ }
+
+ /**
+ * Creates a new push button with the given caption.
+ *
+ * @param caption
+ * the Button caption.
+ */
+ public Button(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new push button with a click listener.
+ *
+ * @param caption
+ * the Button caption.
+ * @param listener
+ * the Button click listener.
+ */
+ public Button(String caption, ClickListener listener) {
+ this(caption);
+ addListener(listener);
+ }
+
+ /**
+ * Click event. This event is thrown, when the button is clicked.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class ClickEvent extends Component.Event {
+
+ private final MouseEventDetails details;
+
+ /**
+ * New instance of text change event.
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public ClickEvent(Component source) {
+ super(source);
+ details = null;
+ }
+
+ /**
+ * Constructor with mouse details
+ *
+ * @param source
+ * The source where the click took place
+ * @param details
+ * Details about the mouse click
+ */
+ public ClickEvent(Component source, MouseEventDetails details) {
+ super(source);
+ this.details = details;
+ }
+
+ /**
+ * Gets the Button where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public Button getButton() {
+ return (Button) getSource();
+ }
+
+ /**
+ * Returns the mouse position (x coordinate) when the click took place.
+ * The position is relative to the browser client area.
+ *
+ * @return The mouse cursor x position or -1 if unknown
+ */
+ public int getClientX() {
+ if (null != details) {
+ return details.getClientX();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the mouse position (y coordinate) when the click took place.
+ * The position is relative to the browser client area.
+ *
+ * @return The mouse cursor y position or -1 if unknown
+ */
+ public int getClientY() {
+ if (null != details) {
+ return details.getClientY();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the relative mouse position (x coordinate) when the click
+ * took place. The position is relative to the clicked component.
+ *
+ * @return The mouse cursor x position relative to the clicked layout
+ * component or -1 if no x coordinate available
+ */
+ public int getRelativeX() {
+ if (null != details) {
+ return details.getRelativeX();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the relative mouse position (y coordinate) when the click
+ * took place. The position is relative to the clicked component.
+ *
+ * @return The mouse cursor y position relative to the clicked layout
+ * component or -1 if no y coordinate available
+ */
+ public int getRelativeY() {
+ if (null != details) {
+ return details.getRelativeY();
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Checks if the Alt key was down when the mouse event took place.
+ *
+ * @return true if Alt was down when the event occured, false otherwise
+ * or if unknown
+ */
+ public boolean isAltKey() {
+ if (null != details) {
+ return details.isAltKey();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the Ctrl key was down when the mouse event took place.
+ *
+ * @return true if Ctrl was pressed when the event occured, false
+ * otherwise or if unknown
+ */
+ public boolean isCtrlKey() {
+ if (null != details) {
+ return details.isCtrlKey();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the Meta key was down when the mouse event took place.
+ *
+ * @return true if Meta was pressed when the event occured, false
+ * otherwise or if unknown
+ */
+ public boolean isMetaKey() {
+ if (null != details) {
+ return details.isMetaKey();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if the Shift key was down when the mouse event took place.
+ *
+ * @return true if Shift was pressed when the event occured, false
+ * otherwise or if unknown
+ */
+ public boolean isShiftKey() {
+ if (null != details) {
+ return details.isShiftKey();
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Interface for listening for a {@link ClickEvent} fired by a
+ * {@link Component}.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ClickListener extends Serializable {
+
+ public static final Method BUTTON_CLICK_METHOD = ReflectTools
+ .findMethod(ClickListener.class, "buttonClick",
+ ClickEvent.class);
+
+ /**
+ * Called when a {@link Button} has been clicked. A reference to the
+ * button is given by {@link ClickEvent#getButton()}.
+ *
+ * @param event
+ * An event containing information about the click.
+ */
+ public void buttonClick(ClickEvent event);
+
+ }
+
+ /**
+ * Adds the button click listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(ClickListener listener) {
+ addListener(ClickEvent.class, listener,
+ ClickListener.BUTTON_CLICK_METHOD);
+ }
+
+ /**
+ * Removes the button click listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(ClickListener listener) {
+ removeListener(ClickEvent.class, listener,
+ ClickListener.BUTTON_CLICK_METHOD);
+ }
+
+ /**
+ * Simulates a button click, notifying all server-side listeners.
+ *
+ * No action is taken is the button is disabled.
+ */
+ public void click() {
+ if (isEnabled() && !isReadOnly()) {
+ fireClick();
+ }
+ }
+
+ /**
+ * Fires a click event to all listeners without any event details.
+ *
+ * In subclasses, override {@link #fireClick(MouseEventDetails)} instead of
+ * this method.
+ */
+ protected void fireClick() {
+ fireEvent(new Button.ClickEvent(this));
+ }
+
+ /**
+ * Fires a click event to all listeners.
+ *
+ * @param details
+ * MouseEventDetails from which keyboard modifiers and other
+ * information about the mouse click can be obtained. If the
+ * button was clicked by a keyboard event, some of the fields may
+ * be empty/undefined.
+ */
+ protected void fireClick(MouseEventDetails details) {
+ fireEvent(new Button.ClickEvent(this, details));
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+
+ }
+
+ /*
+ * Actions
+ */
+
+ protected ClickShortcut clickShortcut;
+
+ /**
+ * Makes it possible to invoke a click on this button by pressing the given
+ * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/>
+ * The shortcut is global (bound to the containing Window).
+ *
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut, null for
+ * none
+ */
+ public void setClickShortcut(int keyCode, int... modifiers) {
+ if (clickShortcut != null) {
+ removeShortcutListener(clickShortcut);
+ }
+ clickShortcut = new ClickShortcut(this, keyCode, modifiers);
+ addShortcutListener(clickShortcut);
+ getState().setClickShortcutKeyCode(clickShortcut.getKeyCode());
+ }
+
+ /**
+ * Removes the keyboard shortcut previously set with
+ * {@link #setClickShortcut(int, int...)}.
+ */
+ public void removeClickShortcut() {
+ if (clickShortcut != null) {
+ removeShortcutListener(clickShortcut);
+ clickShortcut = null;
+ getState().setClickShortcutKeyCode(0);
+ }
+ }
+
+ /**
+ * A {@link ShortcutListener} specifically made to define a keyboard
+ * shortcut that invokes a click on the given button.
+ *
+ */
+ public static class ClickShortcut extends ShortcutListener {
+ protected Button button;
+
+ /**
+ * Creates a keyboard shortcut for clicking the given button using the
+ * shorthand notation defined in {@link ShortcutAction}.
+ *
+ * @param button
+ * to be clicked when the shortcut is invoked
+ * @param shorthandCaption
+ * the caption with shortcut keycode and modifiers indicated
+ */
+ public ClickShortcut(Button button, String shorthandCaption) {
+ super(shorthandCaption);
+ this.button = button;
+ }
+
+ /**
+ * Creates a keyboard shortcut for clicking the given button using the
+ * given {@link KeyCode} and {@link ModifierKey}s.
+ *
+ * @param button
+ * to be clicked when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ * @param modifiers
+ * optional modifiers for shortcut
+ */
+ public ClickShortcut(Button button, int keyCode, int... modifiers) {
+ super(null, keyCode, modifiers);
+ this.button = button;
+ }
+
+ /**
+ * Creates a keyboard shortcut for clicking the given button using the
+ * given {@link KeyCode}.
+ *
+ * @param button
+ * to be clicked when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ */
+ public ClickShortcut(Button button, int keyCode) {
+ this(button, keyCode, null);
+ }
+
+ @Override
+ public void handleAction(Object sender, Object target) {
+ button.click();
+ }
+ }
+
+ /**
+ * Determines if a button is automatically disabled when clicked. See
+ * {@link #setDisableOnClick(boolean)} for details.
+ *
+ * @return true if the button is disabled when clicked, false otherwise
+ */
+ public boolean isDisableOnClick() {
+ return getState().isDisableOnClick();
+ }
+
+ /**
+ * Determines if a button is automatically disabled when clicked. If this is
+ * set to true the button will be automatically disabled when clicked,
+ * typically to prevent (accidental) extra clicks on a button.
+ *
+ * @param disableOnClick
+ * true to disable button when it is clicked, false otherwise
+ */
+ public void setDisableOnClick(boolean disableOnClick) {
+ getState().setDisableOnClick(disableOnClick);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#getTabIndex()
+ */
+ @Override
+ public int getTabIndex() {
+ return getState().getTabIndex();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ getState().setTabIndex(tabIndex);
+ requestRepaint();
+ }
+
+ @Override
+ public void focus() {
+ // Overridden only to make public
+ super.focus();
+ }
+
+ @Override
+ public ButtonState getState() {
+ return (ButtonState) super.getState();
+ }
+
+ /**
+ * 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.
+ *
+ * If set to true, the captions are passed to the browser as html and the
+ * developer is responsible for ensuring no harmful html is used. If set to
+ * false, the content is passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * <code>true</code> if caption is rendered as HTML,
+ * <code>false</code> otherwise
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ if (getState().isHtmlContentAllowed() != htmlContentAllowed) {
+ getState().setHtmlContentAllowed(htmlContentAllowed);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Return HTML rendering setting
+ *
+ * @return <code>true</code> if the caption text is to be rendered as HTML,
+ * <code>false</code> otherwise
+ */
+ public boolean isHtmlContentAllowed() {
+ return getState().isHtmlContentAllowed();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/CheckBox.java b/server/src/com/vaadin/ui/CheckBox.java
new file mode 100644
index 0000000000..30ac9b4626
--- /dev/null
+++ b/server/src/com/vaadin/ui/CheckBox.java
@@ -0,0 +1,141 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.data.Property;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc;
+import com.vaadin.shared.ui.checkbox.CheckBoxState;
+
+public class CheckBox extends AbstractField<Boolean> {
+
+ private CheckBoxServerRpc rpc = new CheckBoxServerRpc() {
+
+ @Override
+ public void setChecked(boolean checked,
+ MouseEventDetails mouseEventDetails) {
+ if (isReadOnly()) {
+ return;
+ }
+
+ final Boolean oldValue = getValue();
+ final Boolean newValue = checked;
+
+ if (!newValue.equals(oldValue)) {
+ // The event is only sent if the switch state is changed
+ setValue(newValue);
+ }
+
+ }
+ };
+
+ FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) {
+ @Override
+ protected void fireEvent(Event event) {
+ CheckBox.this.fireEvent(event);
+ }
+ };
+
+ /**
+ * Creates a new checkbox.
+ */
+ public CheckBox() {
+ registerRpc(rpc);
+ registerRpc(focusBlurRpc);
+ setValue(Boolean.FALSE);
+ }
+
+ /**
+ * Creates a new checkbox with a set caption.
+ *
+ * @param caption
+ * the Checkbox caption.
+ */
+ public CheckBox(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new checkbox with a caption and a set initial state.
+ *
+ * @param caption
+ * the caption of the checkbox
+ * @param initialState
+ * the initial state of the checkbox
+ */
+ public CheckBox(String caption, boolean initialState) {
+ this(caption);
+ setValue(initialState);
+ }
+
+ /**
+ * Creates a new checkbox that is connected to a boolean property.
+ *
+ * @param state
+ * the Initial state of the switch-button.
+ * @param dataSource
+ */
+ public CheckBox(String caption, Property<?> dataSource) {
+ this(caption);
+ setPropertyDataSource(dataSource);
+ }
+
+ @Override
+ public Class<Boolean> getType() {
+ return Boolean.class;
+ }
+
+ @Override
+ public CheckBoxState getState() {
+ return (CheckBoxState) super.getState();
+ }
+
+ @Override
+ protected void setInternalValue(Boolean newValue) {
+ super.setInternalValue(newValue);
+ if (newValue == null) {
+ newValue = false;
+ }
+ getState().setChecked(newValue);
+ }
+
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ /**
+ * Get the boolean value of the button state.
+ *
+ * @return True iff the button is pressed down or checked.
+ *
+ * @deprecated Use {@link #getValue()} instead and, if needed, handle null
+ * values.
+ */
+ @Deprecated
+ public boolean booleanValue() {
+ Boolean value = getValue();
+ return (null == value) ? false : value.booleanValue();
+ }
+}
diff --git a/server/src/com/vaadin/ui/ComboBox.java b/server/src/com/vaadin/ui/ComboBox.java
new file mode 100644
index 0000000000..6286dad124
--- /dev/null
+++ b/server/src/com/vaadin/ui/ComboBox.java
@@ -0,0 +1,116 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.gwt.client.ui.combobox.VFilterSelect;
+
+/**
+ * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's
+ * turned of by default to avoid mistakes. Items are filtered based on user
+ * input, and loaded dynamically ("lazy-loading") from the server. You can turn
+ * on newItemsAllowed and change filtering mode (and also turn it off), but you
+ * can not turn on multi-select mode.
+ *
+ */
+@SuppressWarnings("serial")
+public class ComboBox extends Select {
+
+ private String inputPrompt = null;
+
+ /**
+ * If text input is not allowed, the ComboBox behaves like a pretty
+ * NativeSelect - the user can not enter any text and clicking the text
+ * field opens the drop down with options
+ */
+ private boolean textInputAllowed = true;
+
+ public ComboBox() {
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption, Collection<?> options) {
+ super(caption, options);
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption, Container dataSource) {
+ super(caption, dataSource);
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption) {
+ super(caption);
+ setNewItemsAllowed(false);
+ }
+
+ /**
+ * Gets the current input prompt.
+ *
+ * @see #setInputPrompt(String)
+ * @return the current input prompt, or null if not enabled
+ */
+ public String getInputPrompt() {
+ return inputPrompt;
+ }
+
+ /**
+ * Sets the input prompt - a textual prompt that is displayed when the
+ * select would otherwise be empty, to prompt the user for input.
+ *
+ * @param inputPrompt
+ * the desired input prompt, or null to disable
+ */
+ public void setInputPrompt(String inputPrompt) {
+ this.inputPrompt = inputPrompt;
+ requestRepaint();
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (inputPrompt != null) {
+ target.addAttribute("prompt", inputPrompt);
+ }
+ super.paintContent(target);
+
+ if (!textInputAllowed) {
+ target.addAttribute(VFilterSelect.ATTR_NO_TEXT_INPUT, true);
+ }
+ }
+
+ /**
+ * Sets whether it is possible to input text into the field or whether the
+ * field area of the component is just used to show what is selected. By
+ * disabling text input, the comboBox will work in the same way as a
+ * {@link NativeSelect}
+ *
+ * @see #isTextInputAllowed()
+ *
+ * @param textInputAllowed
+ * true to allow entering text, false to just show the current
+ * selection
+ */
+ public void setTextInputAllowed(boolean textInputAllowed) {
+ this.textInputAllowed = textInputAllowed;
+ requestRepaint();
+ }
+
+ /**
+ * Returns true if the user can enter text into the field to either filter
+ * the selections or enter a new value if {@link #isNewItemsAllowed()}
+ * returns true. If text input is disabled, the comboBox will work in the
+ * same way as a {@link NativeSelect}
+ *
+ * @return
+ */
+ public boolean isTextInputAllowed() {
+ return textInputAllowed;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java
new file mode 100644
index 0000000000..a2c257ab68
--- /dev/null
+++ b/server/src/com/vaadin/ui/Component.java
@@ -0,0 +1,1047 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.EventListener;
+import java.util.EventObject;
+import java.util.Locale;
+
+import com.vaadin.Application;
+import com.vaadin.event.FieldEvents;
+import com.vaadin.shared.ComponentState;
+import com.vaadin.terminal.ErrorMessage;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Sizeable;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+
+/**
+ * {@code Component} is the top-level interface that is and must be implemented
+ * by all Vaadin components. {@code Component} is paired with
+ * {@link AbstractComponent}, which provides a default implementation for all
+ * the methods defined in this interface.
+ *
+ * <p>
+ * Components are laid out in the user interface hierarchically. The layout is
+ * managed by layout components, or more generally by components that implement
+ * the {@link ComponentContainer} interface. Such a container is the
+ * <i>parent</i> of the contained components.
+ * </p>
+ *
+ * <p>
+ * The {@link #getParent()} method allows retrieving the parent component of a
+ * component. While there is a {@link #setParent(Component) setParent()}, you
+ * rarely need it as you normally add components with the
+ * {@link ComponentContainer#addComponent(Component) addComponent()} method of
+ * the layout or other {@code ComponentContainer}, which automatically sets the
+ * parent.
+ * </p>
+ *
+ * <p>
+ * A component becomes <i>attached</i> to an application (and the
+ * {@link #attach()} is called) when it or one of its parents is attached to the
+ * main window of the application through its containment hierarchy.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Component extends ClientConnector, Sizeable, Serializable {
+
+ /**
+ * Gets all user-defined CSS style names of a component. If the component
+ * has multiple style names defined, the return string is a space-separated
+ * list of style names. Built-in style names defined in Vaadin or GWT are
+ * not returned.
+ *
+ * <p>
+ * The style names are returned only in the basic form in which they were
+ * added; each user-defined style name shows as two CSS style class names in
+ * the rendered HTML: one as it was given and one prefixed with the
+ * component-specific style name. Only the former is returned.
+ * </p>
+ *
+ * @return the style name or a space-separated list of user-defined style
+ * names of the component
+ * @see #setStyleName(String)
+ * @see #addStyleName(String)
+ * @see #removeStyleName(String)
+ */
+ public String getStyleName();
+
+ /**
+ * Sets one or more user-defined style names of the component, replacing any
+ * previous user-defined styles. Multiple styles can be specified as a
+ * space-separated list of style names. The style names must be valid CSS
+ * class names and should not conflict with any built-in style names in
+ * Vaadin or GWT.
+ *
+ * <pre>
+ * Label label = new Label(&quot;This text has a lot of style&quot;);
+ * label.setStyleName(&quot;myonestyle myotherstyle&quot;);
+ * </pre>
+ *
+ * <p>
+ * Each style name will occur in two versions: one as specified and one that
+ * is prefixed with the style name of the component. For example, if you
+ * have a {@code Button} component and give it "{@code mystyle}" style, the
+ * component will have both "{@code mystyle}" and "{@code v-button-mystyle}"
+ * styles. You could then style the component either with:
+ * </p>
+ *
+ * <pre>
+ * .myonestyle {background: blue;}
+ * </pre>
+ *
+ * <p>
+ * or
+ * </p>
+ *
+ * <pre>
+ * .v-button-myonestyle {background: blue;}
+ * </pre>
+ *
+ * <p>
+ * It is normally a good practice to use {@link #addStyleName(String)
+ * addStyleName()} rather than this setter, as different software
+ * abstraction layers can then add their own styles without accidentally
+ * removing those defined in other layers.
+ * </p>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent}.
+ * </p>
+ *
+ * @param style
+ * the new style or styles of the component as a space-separated
+ * list
+ * @see #getStyleName()
+ * @see #addStyleName(String)
+ * @see #removeStyleName(String)
+ */
+ public void setStyleName(String style);
+
+ /**
+ * Adds a style name to component. The style name will be rendered as a HTML
+ * class name, which can be used in a CSS definition.
+ *
+ * <pre>
+ * Label label = new Label(&quot;This text has style&quot;);
+ * label.addStyleName(&quot;mystyle&quot;);
+ * </pre>
+ *
+ * <p>
+ * Each style name will occur in two versions: one as specified and one that
+ * is prefixed wil the style name of the component. For example, if you have
+ * a {@code Button} component and give it "{@code mystyle}" style, the
+ * component will have both "{@code mystyle}" and "{@code v-button-mystyle}"
+ * styles. You could then style the component either with:
+ * </p>
+ *
+ * <pre>
+ * .mystyle {font-style: italic;}
+ * </pre>
+ *
+ * <p>
+ * or
+ * </p>
+ *
+ * <pre>
+ * .v-button-mystyle {font-style: italic;}
+ * </pre>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent}.
+ * </p>
+ *
+ * @param style
+ * the new style to be added to the component
+ * @see #getStyleName()
+ * @see #setStyleName(String)
+ * @see #removeStyleName(String)
+ */
+ public void addStyleName(String style);
+
+ /**
+ * Removes one or more style names from component. Multiple styles can be
+ * specified as a space-separated list of style names.
+ *
+ * <p>
+ * The parameter must be a valid CSS style name. Only user-defined style
+ * names added with {@link #addStyleName(String) addStyleName()} or
+ * {@link #setStyleName(String) setStyleName()} can be removed; built-in
+ * style names defined in Vaadin or GWT can not be removed.
+ * </p>
+ *
+ * * This method will trigger a {@link RepaintRequestEvent}.
+ *
+ * @param style
+ * the style name or style names to be removed
+ * @see #getStyleName()
+ * @see #setStyleName(String)
+ * @see #addStyleName(String)
+ */
+ public void removeStyleName(String style);
+
+ /**
+ * Tests whether the component is enabled or not. A user can not interact
+ * with disabled components. Disabled components are rendered in a style
+ * that indicates the status, usually in gray color. Children of a disabled
+ * component are also disabled. Components are enabled by default.
+ *
+ * <p>
+ * As a security feature, all updates for disabled components are blocked on
+ * the server-side.
+ * </p>
+ *
+ * <p>
+ * Note that this method only returns the status of the component and does
+ * not take parents into account. Even though this method returns true the
+ * component can be disabled to the user if a parent is disabled.
+ * </p>
+ *
+ * @return <code>true</code> if the component and its parent are enabled,
+ * <code>false</code> otherwise.
+ * @see VariableOwner#isEnabled()
+ */
+ public boolean isEnabled();
+
+ /**
+ * Enables or disables the component. The user can not interact disabled
+ * components, which are shown with a style that indicates the status,
+ * usually shaded in light gray color. Components are enabled by default.
+ *
+ * <pre>
+ * Button enabled = new Button(&quot;Enabled&quot;);
+ * enabled.setEnabled(true); // The default
+ * layout.addComponent(enabled);
+ *
+ * Button disabled = new Button(&quot;Disabled&quot;);
+ * disabled.setEnabled(false);
+ * layout.addComponent(disabled);
+ * </pre>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent} for the component
+ * and, if it is a {@link ComponentContainer}, for all its children
+ * recursively.
+ * </p>
+ *
+ * @param enabled
+ * a boolean value specifying if the component should be enabled
+ * or not
+ */
+ public void setEnabled(boolean enabled);
+
+ /**
+ * Tests the <i>visibility</i> property of the component.
+ *
+ * <p>
+ * Visible components are drawn in the user interface, while invisible ones
+ * are not. The effect is not merely a cosmetic CSS change - no information
+ * about an invisible component will be sent to the client. The effect is
+ * thus the same as removing the component from its parent. Making a
+ * component invisible through this property can alter the positioning of
+ * other components.
+ * </p>
+ *
+ * <p>
+ * A component is visible only if all its parents are also visible. This is
+ * not checked by this method though, so even if this method returns true,
+ * the component can be hidden from the user because a parent is set to
+ * invisible.
+ * </p>
+ *
+ * @return <code>true</code> if the component has been set to be visible in
+ * the user interface, <code>false</code> if not
+ * @see #setVisible(boolean)
+ * @see #attach()
+ */
+ public boolean isVisible();
+
+ /**
+ * Sets the visibility of the component.
+ *
+ * <p>
+ * Visible components are drawn in the user interface, while invisible ones
+ * are not. The effect is not merely a cosmetic CSS change - no information
+ * about an invisible component will be sent to the client. The effect is
+ * thus the same as removing the component from its parent.
+ * </p>
+ *
+ * <pre>
+ * TextField readonly = new TextField(&quot;Read-Only&quot;);
+ * readonly.setValue(&quot;You can't see this!&quot;);
+ * readonly.setVisible(false);
+ * layout.addComponent(readonly);
+ * </pre>
+ *
+ * <p>
+ * A component is visible only if all of its parents are also visible. If a
+ * component is explicitly set to be invisible, changes in the visibility of
+ * its parents will not change the visibility of the component.
+ * </p>
+ *
+ * @param visible
+ * the boolean value specifying if the component should be
+ * visible after the call or not.
+ * @see #isVisible()
+ */
+ public void setVisible(boolean visible);
+
+ /**
+ * Gets the parent component of the component.
+ *
+ * <p>
+ * Components can be nested but a component can have only one parent. A
+ * component that contains other components, that is, can be a parent,
+ * should usually inherit the {@link ComponentContainer} interface.
+ * </p>
+ *
+ * @return the parent component
+ */
+ @Override
+ public HasComponents getParent();
+
+ /**
+ * Tests whether the component is in the read-only mode. The user can not
+ * change the value of a read-only component. As only {@link Field}
+ * components normally have a value that can be input or changed by the
+ * user, this is mostly relevant only to field components, though not
+ * restricted to them.
+ *
+ * <p>
+ * Notice that the read-only mode only affects whether the user can change
+ * the <i>value</i> of the component; it is possible to, for example, scroll
+ * a read-only table.
+ * </p>
+ *
+ * <p>
+ * The method will return {@code true} if the component or any of its
+ * parents is in the read-only mode.
+ * </p>
+ *
+ * @return <code>true</code> if the component or any of its parents is in
+ * read-only mode, <code>false</code> if not.
+ * @see #setReadOnly(boolean)
+ */
+ public boolean isReadOnly();
+
+ /**
+ * Sets the read-only mode of the component to the specified mode. The user
+ * can not change the value of a read-only component.
+ *
+ * <p>
+ * As only {@link Field} components normally have a value that can be input
+ * or changed by the user, this is mostly relevant only to field components,
+ * though not restricted to them.
+ * </p>
+ *
+ * <p>
+ * Notice that the read-only mode only affects whether the user can change
+ * the <i>value</i> of the component; it is possible to, for example, scroll
+ * a read-only table.
+ * </p>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent}.
+ * </p>
+ *
+ * @param readOnly
+ * a boolean value specifying whether the component is put
+ * read-only mode or not
+ */
+ public void setReadOnly(boolean readOnly);
+
+ /**
+ * Gets the caption of the component.
+ *
+ * <p>
+ * See {@link #setCaption(String)} for a detailed description of the
+ * caption.
+ * </p>
+ *
+ * @return the caption of the component or {@code null} if the caption is
+ * not set.
+ * @see #setCaption(String)
+ */
+ public String getCaption();
+
+ /**
+ * Sets the caption of the component.
+ *
+ * <p>
+ * A <i>caption</i> is an explanatory textual label accompanying a user
+ * interface component, usually shown above, left of, or inside the
+ * component. <i>Icon</i> (see {@link #setIcon(Resource) setIcon()} is
+ * closely related to caption and is usually displayed horizontally before
+ * or after it, depending on the component and the containing layout.
+ * </p>
+ *
+ * <p>
+ * The caption can usually also be given as the first parameter to a
+ * constructor, though some components do not support it.
+ * </p>
+ *
+ * <pre>
+ * RichTextArea area = new RichTextArea();
+ * area.setCaption(&quot;You can edit stuff here&quot;);
+ * area.setValue(&quot;&lt;h1&gt;Helpful Heading&lt;/h1&gt;&quot;
+ * + &quot;&lt;p&gt;All this is for you to edit.&lt;/p&gt;&quot;);
+ * </pre>
+ *
+ * <p>
+ * The contents of a caption are automatically quoted, so no raw XHTML can
+ * be rendered in a caption. The validity of the used character encoding,
+ * usually UTF-8, is not checked.
+ * </p>
+ *
+ * <p>
+ * The caption of a component is, by default, managed and displayed by the
+ * layout component or component container in which the component is placed.
+ * For example, the {@link VerticalLayout} component shows the captions
+ * left-aligned above the contained components, while the {@link FormLayout}
+ * component shows the captions on the left side of the vertically laid
+ * components, with the captions and their associated components
+ * left-aligned in their own columns. The {@link CustomComponent} does not
+ * manage the caption of its composition root, so if the root component has
+ * a caption, it will not be rendered. Some components, such as
+ * {@link Button} and {@link Panel}, manage the caption themselves and
+ * display it inside the component.
+ * </p>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent}. A
+ * reimplementation should call the superclass implementation.
+ * </p>
+ *
+ * @param caption
+ * the new caption for the component. If the caption is
+ * {@code null}, no caption is shown and it does not normally
+ * take any space
+ */
+ public void setCaption(String caption);
+
+ /**
+ * Gets the icon resource of the component.
+ *
+ * <p>
+ * See {@link #setIcon(Resource)} for a detailed description of the icon.
+ * </p>
+ *
+ * @return the icon resource of the component or {@code null} if the
+ * component has no icon
+ * @see #setIcon(Resource)
+ */
+ public Resource getIcon();
+
+ /**
+ * Sets the icon of the component.
+ *
+ * <p>
+ * An icon is an explanatory graphical label accompanying a user interface
+ * component, usually shown above, left of, or inside the component. Icon is
+ * closely related to caption (see {@link #setCaption(String) setCaption()})
+ * and is usually displayed horizontally before or after it, depending on
+ * the component and the containing layout.
+ * </p>
+ *
+ * <p>
+ * The image is loaded by the browser from a resource, typically a
+ * {@link com.vaadin.terminal.ThemeResource}.
+ * </p>
+ *
+ * <pre>
+ * // Component with an icon from a custom theme
+ * TextField name = new TextField(&quot;Name&quot;);
+ * name.setIcon(new ThemeResource(&quot;icons/user.png&quot;));
+ * layout.addComponent(name);
+ *
+ * // Component with an icon from another theme ('runo')
+ * Button ok = new Button(&quot;OK&quot;);
+ * ok.setIcon(new ThemeResource(&quot;../runo/icons/16/ok.png&quot;));
+ * layout.addComponent(ok);
+ * </pre>
+ *
+ * <p>
+ * The icon of a component is, by default, managed and displayed by the
+ * layout component or component container in which the component is placed.
+ * For example, the {@link VerticalLayout} component shows the icons
+ * left-aligned above the contained components, while the {@link FormLayout}
+ * component shows the icons on the left side of the vertically laid
+ * components, with the icons and their associated components left-aligned
+ * in their own columns. The {@link CustomComponent} does not manage the
+ * icon of its composition root, so if the root component has an icon, it
+ * will not be rendered.
+ * </p>
+ *
+ * <p>
+ * An icon will be rendered inside an HTML element that has the
+ * {@code v-icon} CSS style class. The containing layout may enclose an icon
+ * and a caption inside elements related to the caption, such as
+ * {@code v-caption} .
+ * </p>
+ *
+ * This method will trigger a {@link RepaintRequestEvent}.
+ *
+ * @param icon
+ * the icon of the component. If null, no icon is shown and it
+ * does not normally take any space.
+ * @see #getIcon()
+ * @see #setCaption(String)
+ */
+ public void setIcon(Resource icon);
+
+ /**
+ * Gets the Root the component is attached to.
+ *
+ * <p>
+ * If the component is not attached to a Root through a component
+ * containment hierarchy, <code>null</code> is returned.
+ * </p>
+ *
+ * @return the Root of the component or <code>null</code> if it is not
+ * attached to a Root
+ */
+ @Override
+ public Root getRoot();
+
+ /**
+ * Gets the application object to which the component is attached.
+ *
+ * <p>
+ * The method will return {@code null} if the component is not currently
+ * attached to an application.
+ * </p>
+ *
+ * <p>
+ * Getting a null value is often a problem in constructors of regular
+ * components and in the initializers of custom composite components. A
+ * standard workaround is to use {@link Application#getCurrent()} to
+ * retrieve the application instance that the current request relates to.
+ * Another way is to move the problematic initialization to
+ * {@link #attach()}, as described in the documentation of the method.
+ * </p>
+ *
+ * @return the parent application of the component or <code>null</code>.
+ * @see #attach()
+ */
+ public Application getApplication();
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>
+ * Reimplementing the {@code attach()} method is useful for tasks that need
+ * to get a reference to the parent, window, or application object with the
+ * {@link #getParent()}, {@link #getRoot()}, and {@link #getApplication()}
+ * methods. A component does not yet know these objects in the constructor,
+ * so in such case, the methods will return {@code null}. For example, the
+ * following is invalid:
+ * </p>
+ *
+ * <pre>
+ * public class AttachExample extends CustomComponent {
+ * public AttachExample() {
+ * // ERROR: We can't access the application object yet.
+ * ClassResource r = new ClassResource(&quot;smiley.jpg&quot;, getApplication());
+ * Embedded image = new Embedded(&quot;Image:&quot;, r);
+ * setCompositionRoot(image);
+ * }
+ * }
+ * </pre>
+ *
+ * <p>
+ * Adding a component to an application triggers calling the
+ * {@link #attach()} method for the component. Correspondingly, removing a
+ * component from a container triggers calling the {@link #detach()} method.
+ * If the parent of an added component is already connected to the
+ * application, the {@code attach()} is called immediately from
+ * {@link #setParent(Component)}.
+ * </p>
+ * <p>
+ * This method must call {@link Root#componentAttached(Component)} to let
+ * the Root know that a new Component has been attached.
+ * </p>
+ *
+ *
+ * <pre>
+ * public class AttachExample extends CustomComponent {
+ * public AttachExample() {
+ * }
+ *
+ * &#064;Override
+ * public void attach() {
+ * super.attach(); // Must call.
+ *
+ * // Now we know who ultimately owns us.
+ * ClassResource r = new ClassResource(&quot;smiley.jpg&quot;, getApplication());
+ * Embedded image = new Embedded(&quot;Image:&quot;, r);
+ * setCompositionRoot(image);
+ * }
+ * }
+ * </pre>
+ */
+ @Override
+ public void attach();
+
+ /**
+ * Gets the locale of the component.
+ *
+ * <p>
+ * If a component does not have a locale set, the locale of its parent is
+ * returned, and so on. Eventually, if no parent has locale set, the locale
+ * of the application is returned. If the application does not have a locale
+ * set, it is determined by <code>Locale.getDefault()</code>.
+ * </p>
+ *
+ * <p>
+ * As the component must be attached before its locale can be acquired,
+ * using this method in the internationalization of component captions, etc.
+ * is generally not feasible. For such use case, we recommend using an
+ * otherwise acquired reference to the application locale.
+ * </p>
+ *
+ * @return Locale of this component or {@code null} if the component and
+ * none of its parents has a locale set and the component is not yet
+ * attached to an application.
+ */
+ public Locale getLocale();
+
+ /**
+ * Returns the current shared state bean for the component. The state (or
+ * changes to it) is communicated from the server to the client.
+ *
+ * Subclasses can use a more specific return type for this method.
+ *
+ * @return The state object for the component
+ *
+ * @since 7.0
+ */
+ @Override
+ public ComponentState getState();
+
+ /**
+ * Called before the shared state is sent to the client. Gives the component
+ * an opportunity to set computed/dynamic state values e.g. state values
+ * that depend on other component features.
+ * <p>
+ * This method must not alter the component hierarchy in any way.
+ * </p>
+ *
+ * @since 7.0
+ */
+ public void updateState();
+
+ /**
+ * Adds an unique id for component that get's transferred to terminal for
+ * testing purposes. Keeping identifiers unique is the responsibility of the
+ * programmer.
+ *
+ * @param id
+ * An alphanumeric id
+ */
+ public void setDebugId(String id);
+
+ /**
+ * Get's currently set debug identifier
+ *
+ * @return current debug id, null if not set
+ */
+ public String getDebugId();
+
+ /* Component event framework */
+
+ /**
+ * Superclass of all component originated events.
+ *
+ * <p>
+ * Events are the basis of all user interaction handling in Vaadin. To
+ * handle events, you provide a listener object that receives the events of
+ * the particular event type.
+ * </p>
+ *
+ * <pre>
+ * Button button = new Button(&quot;Click Me!&quot;);
+ * button.addListener(new Button.ClickListener() {
+ * public void buttonClick(ClickEvent event) {
+ * getWindow().showNotification(&quot;Thank You!&quot;);
+ * }
+ * });
+ * layout.addComponent(button);
+ * </pre>
+ *
+ * <p>
+ * Notice that while each of the event types have their corresponding
+ * listener types; the listener interfaces are not required to inherit the
+ * {@code Component.Listener} interface.
+ * </p>
+ *
+ * @see Component.Listener
+ */
+ @SuppressWarnings("serial")
+ public class Event extends EventObject {
+
+ /**
+ * Constructs a new event with the specified source component.
+ *
+ * @param source
+ * the source component of the event
+ */
+ public Event(Component source) {
+ super(source);
+ }
+
+ /**
+ * Gets the component where the event occurred.
+ *
+ * @return the source component of the event
+ */
+ public Component getComponent() {
+ return (Component) getSource();
+ }
+ }
+
+ /**
+ * Listener interface for receiving <code>Component.Event</code>s.
+ *
+ * <p>
+ * Listener interfaces are the basis of all user interaction handling in
+ * Vaadin. You have or create a listener object that receives the events.
+ * All event types have their corresponding listener types; they are not,
+ * however, required to inherit the {@code Component.Listener} interface,
+ * and they rarely do so.
+ * </p>
+ *
+ * <p>
+ * This generic listener interface is useful typically when you wish to
+ * handle events from different component types in a single listener method
+ * ({@code componentEvent()}. If you handle component events in an anonymous
+ * listener class, you normally use the component specific listener class,
+ * such as {@link com.vaadin.ui.Button.ClickEvent}.
+ * </p>
+ *
+ * <pre>
+ * class Listening extends CustomComponent implements Listener {
+ * Button ok; // Stored for determining the source of an event
+ *
+ * Label status; // For displaying info about the event
+ *
+ * public Listening() {
+ * VerticalLayout layout = new VerticalLayout();
+ *
+ * // Some miscellaneous component
+ * TextField name = new TextField(&quot;Say it all here&quot;);
+ * name.addListener(this);
+ * name.setImmediate(true);
+ * layout.addComponent(name);
+ *
+ * // Handle button clicks as generic events instead
+ * // of Button.ClickEvent events
+ * ok = new Button(&quot;OK&quot;);
+ * ok.addListener(this);
+ * layout.addComponent(ok);
+ *
+ * // For displaying information about an event
+ * status = new Label(&quot;&quot;);
+ * layout.addComponent(status);
+ *
+ * setCompositionRoot(layout);
+ * }
+ *
+ * public void componentEvent(Event event) {
+ * // Act according to the source of the event
+ * if (event.getSource() == ok
+ * &amp;&amp; event.getClass() == Button.ClickEvent.class)
+ * getWindow().showNotification(&quot;Click!&quot;);
+ *
+ * // Display source component and event class names
+ * status.setValue(&quot;Event from &quot; + event.getSource().getClass().getName()
+ * + &quot;: &quot; + event.getClass().getName());
+ * }
+ * }
+ *
+ * Listening listening = new Listening();
+ * layout.addComponent(listening);
+ * </pre>
+ *
+ * @see Component#addListener(Listener)
+ */
+ public interface Listener extends EventListener, Serializable {
+
+ /**
+ * Notifies the listener of a component event.
+ *
+ * <p>
+ * As the event can typically come from one of many source components,
+ * you may need to differentiate between the event source by component
+ * reference, class, etc.
+ * </p>
+ *
+ * <pre>
+ * public void componentEvent(Event event) {
+ * // Act according to the source of the event
+ * if (event.getSource() == ok &amp;&amp; event.getClass() == Button.ClickEvent.class)
+ * getWindow().showNotification(&quot;Click!&quot;);
+ *
+ * // Display source component and event class names
+ * status.setValue(&quot;Event from &quot; + event.getSource().getClass().getName()
+ * + &quot;: &quot; + event.getClass().getName());
+ * }
+ * </pre>
+ *
+ * @param event
+ * the event that has occured.
+ */
+ public void componentEvent(Component.Event event);
+ }
+
+ /**
+ * Registers a new (generic) component event listener for the component.
+ *
+ * <pre>
+ * class Listening extends CustomComponent implements Listener {
+ * // Stored for determining the source of an event
+ * Button ok;
+ *
+ * Label status; // For displaying info about the event
+ *
+ * public Listening() {
+ * VerticalLayout layout = new VerticalLayout();
+ *
+ * // Some miscellaneous component
+ * TextField name = new TextField(&quot;Say it all here&quot;);
+ * name.addListener(this);
+ * name.setImmediate(true);
+ * layout.addComponent(name);
+ *
+ * // Handle button clicks as generic events instead
+ * // of Button.ClickEvent events
+ * ok = new Button(&quot;OK&quot;);
+ * ok.addListener(this);
+ * layout.addComponent(ok);
+ *
+ * // For displaying information about an event
+ * status = new Label(&quot;&quot;);
+ * layout.addComponent(status);
+ *
+ * setCompositionRoot(layout);
+ * }
+ *
+ * public void componentEvent(Event event) {
+ * // Act according to the source of the event
+ * if (event.getSource() == ok)
+ * getWindow().showNotification(&quot;Click!&quot;);
+ *
+ * status.setValue(&quot;Event from &quot; + event.getSource().getClass().getName()
+ * + &quot;: &quot; + event.getClass().getName());
+ * }
+ * }
+ *
+ * Listening listening = new Listening();
+ * layout.addComponent(listening);
+ * </pre>
+ *
+ * @param listener
+ * the new Listener to be registered.
+ * @see Component.Event
+ * @see #removeListener(Listener)
+ */
+ public void addListener(Component.Listener listener);
+
+ /**
+ * Removes a previously registered component event listener from this
+ * component.
+ *
+ * @param listener
+ * the listener to be removed.
+ * @see #addListener(Listener)
+ */
+ public void removeListener(Component.Listener listener);
+
+ /**
+ * Class of all component originated error events.
+ *
+ * <p>
+ * The component error event is normally fired by
+ * {@link AbstractComponent#setComponentError(ErrorMessage)}. The component
+ * errors are set by the framework in some situations and can be set by user
+ * code. They are indicated in a component with an error indicator.
+ * </p>
+ */
+ @SuppressWarnings("serial")
+ public class ErrorEvent extends Event {
+
+ private final ErrorMessage message;
+
+ /**
+ * Constructs a new event with a specified source component.
+ *
+ * @param message
+ * the error message.
+ * @param component
+ * the source component.
+ */
+ public ErrorEvent(ErrorMessage message, Component component) {
+ super(component);
+ this.message = message;
+ }
+
+ /**
+ * Gets the error message.
+ *
+ * @return the error message.
+ */
+ public ErrorMessage getErrorMessage() {
+ return message;
+ }
+ }
+
+ /**
+ * Listener interface for receiving <code>Component.Errors</code>s.
+ */
+ public interface ErrorListener extends EventListener, Serializable {
+
+ /**
+ * Notifies the listener of a component error.
+ *
+ * @param event
+ * the event that has occured.
+ */
+ public void componentError(Component.ErrorEvent event);
+ }
+
+ /**
+ * A sub-interface implemented by components that can obtain input focus.
+ * This includes all {@link Field} components as well as some other
+ * components, such as {@link Upload}.
+ *
+ * <p>
+ * Focus can be set with {@link #focus()}. This interface does not provide
+ * an accessor that would allow finding out the currently focused component;
+ * focus information can be acquired for some (but not all) {@link Field}
+ * components through the {@link com.vaadin.event.FieldEvents.FocusListener}
+ * and {@link com.vaadin.event.FieldEvents.BlurListener} interfaces.
+ * </p>
+ *
+ * @see FieldEvents
+ */
+ public interface Focusable extends Component {
+
+ /**
+ * Sets the focus to this component.
+ *
+ * <pre>
+ * Form loginBox = new Form();
+ * loginBox.setCaption(&quot;Login&quot;);
+ * layout.addComponent(loginBox);
+ *
+ * // Create the first field which will be focused
+ * TextField username = new TextField(&quot;User name&quot;);
+ * loginBox.addField(&quot;username&quot;, username);
+ *
+ * // Set focus to the user name
+ * username.focus();
+ *
+ * TextField password = new TextField(&quot;Password&quot;);
+ * loginBox.addField(&quot;password&quot;, password);
+ *
+ * Button login = new Button(&quot;Login&quot;);
+ * loginBox.getFooter().addComponent(login);
+ * </pre>
+ *
+ * <p>
+ * Notice that this interface does not provide an accessor that would
+ * allow finding out the currently focused component. Focus information
+ * can be acquired for some (but not all) {@link Field} components
+ * through the {@link com.vaadin.event.FieldEvents.FocusListener} and
+ * {@link com.vaadin.event.FieldEvents.BlurListener} interfaces.
+ * </p>
+ *
+ * @see com.vaadin.event.FieldEvents
+ * @see com.vaadin.event.FieldEvents.FocusEvent
+ * @see com.vaadin.event.FieldEvents.FocusListener
+ * @see com.vaadin.event.FieldEvents.BlurEvent
+ * @see com.vaadin.event.FieldEvents.BlurListener
+ */
+ public void focus();
+
+ /**
+ * Gets the <i>tabulator index</i> of the {@code Focusable} component.
+ *
+ * @return tab index set for the {@code Focusable} component
+ * @see #setTabIndex(int)
+ */
+ public int getTabIndex();
+
+ /**
+ * Sets the <i>tabulator index</i> of the {@code Focusable} component.
+ * The tab index property is used to specify the order in which the
+ * fields are focused when the user presses the Tab key. Components with
+ * a defined tab index are focused sequentially first, and then the
+ * components with no tab index.
+ *
+ * <pre>
+ * Form loginBox = new Form();
+ * loginBox.setCaption(&quot;Login&quot;);
+ * layout.addComponent(loginBox);
+ *
+ * // Create the first field which will be focused
+ * TextField username = new TextField(&quot;User name&quot;);
+ * loginBox.addField(&quot;username&quot;, username);
+ *
+ * // Set focus to the user name
+ * username.focus();
+ *
+ * TextField password = new TextField(&quot;Password&quot;);
+ * loginBox.addField(&quot;password&quot;, password);
+ *
+ * Button login = new Button(&quot;Login&quot;);
+ * loginBox.getFooter().addComponent(login);
+ *
+ * // An additional component which natural focus order would
+ * // be after the button.
+ * CheckBox remember = new CheckBox(&quot;Remember me&quot;);
+ * loginBox.getFooter().addComponent(remember);
+ *
+ * username.setTabIndex(1);
+ * password.setTabIndex(2);
+ * remember.setTabIndex(3); // Different than natural place
+ * login.setTabIndex(4);
+ * </pre>
+ *
+ * <p>
+ * After all focusable user interface components are done, the browser
+ * can begin again from the component with the smallest tab index, or it
+ * can take the focus out of the page, for example, to the location bar.
+ * </p>
+ *
+ * <p>
+ * If the tab index is not set (is set to zero), the default tab order
+ * is used. The order is somewhat browser-dependent, but generally
+ * follows the HTML structure of the page.
+ * </p>
+ *
+ * <p>
+ * A negative value means that the component is completely removed from
+ * the tabulation order and can not be reached by pressing the Tab key
+ * at all.
+ * </p>
+ *
+ * @param tabIndex
+ * the tab order of this component. Indexes usually start
+ * from 1. Zero means that default tab order should be used.
+ * A negative value means that the field should not be
+ * included in the tabbing sequence.
+ * @see #getTabIndex()
+ */
+ public void setTabIndex(int tabIndex);
+
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/ComponentContainer.java b/server/src/com/vaadin/ui/ComponentContainer.java
new file mode 100644
index 0000000000..8182d54b56
--- /dev/null
+++ b/server/src/com/vaadin/ui/ComponentContainer.java
@@ -0,0 +1,222 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+/**
+ * Extension to the {@link Component} interface which adds to it the capacity to
+ * contain other components. All UI elements that can have child elements
+ * implement this interface.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ComponentContainer extends HasComponents {
+
+ /**
+ * Adds the component into this container.
+ *
+ * @param c
+ * the component to be added.
+ */
+ public void addComponent(Component c);
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+ public void removeComponent(Component c);
+
+ /**
+ * Removes all components from this container.
+ */
+ public void removeAllComponents();
+
+ /**
+ * Replaces the component in the container with another one without changing
+ * position.
+ *
+ * <p>
+ * This method replaces component with another one is such way that the new
+ * component overtakes the position of the old component. If the old
+ * component is not in the container, the new component is added to the
+ * container. If the both component are already in the container, their
+ * positions are swapped. Component attach and detach events should be taken
+ * care as with add and remove.
+ * </p>
+ *
+ * @param oldComponent
+ * the old component that will be replaced.
+ * @param newComponent
+ * the new component to be replaced.
+ */
+ public void replaceComponent(Component oldComponent, Component newComponent);
+
+ /**
+ * Gets the number of children this {@link ComponentContainer} has. This
+ * must be symmetric with what {@link #getComponentIterator()} returns.
+ *
+ * @return The number of child components this container has.
+ * @since 7.0.0
+ */
+ public int getComponentCount();
+
+ /**
+ * Moves all components from an another container into this container. The
+ * components are removed from <code>source</code>.
+ *
+ * @param source
+ * the container which contains the components that are to be
+ * moved to this container.
+ */
+ public void moveComponentsFrom(ComponentContainer source);
+
+ /**
+ * Listens the component attach events.
+ *
+ * @param listener
+ * the listener to add.
+ */
+ public void addListener(ComponentAttachListener listener);
+
+ /**
+ * Stops the listening component attach events.
+ *
+ * @param listener
+ * the listener to removed.
+ */
+ public void removeListener(ComponentAttachListener listener);
+
+ /**
+ * Listens the component detach events.
+ */
+ public void addListener(ComponentDetachListener listener);
+
+ /**
+ * Stops the listening component detach events.
+ */
+ public void removeListener(ComponentDetachListener listener);
+
+ /**
+ * Component attach listener interface.
+ */
+ public interface ComponentAttachListener extends Serializable {
+
+ /**
+ * A new component is attached to container.
+ *
+ * @param event
+ * the component attach event.
+ */
+ public void componentAttachedToContainer(ComponentAttachEvent event);
+ }
+
+ /**
+ * Component detach listener interface.
+ */
+ public interface ComponentDetachListener extends Serializable {
+
+ /**
+ * A component has been detached from container.
+ *
+ * @param event
+ * the component detach event.
+ */
+ public void componentDetachedFromContainer(ComponentDetachEvent event);
+ }
+
+ /**
+ * Component attach event sent when a component is attached to container.
+ */
+ @SuppressWarnings("serial")
+ public class ComponentAttachEvent extends Component.Event {
+
+ private final Component component;
+
+ /**
+ * Creates a new attach event.
+ *
+ * @param container
+ * the component container the component has been detached
+ * to.
+ * @param attachedComponent
+ * the component that has been attached.
+ */
+ public ComponentAttachEvent(ComponentContainer container,
+ Component attachedComponent) {
+ super(container);
+ component = attachedComponent;
+ }
+
+ /**
+ * Gets the component container.
+ *
+ * @param the
+ * component container.
+ */
+ public ComponentContainer getContainer() {
+ return (ComponentContainer) getSource();
+ }
+
+ /**
+ * Gets the attached component.
+ *
+ * @param the
+ * attach component.
+ */
+ public Component getAttachedComponent() {
+ return component;
+ }
+ }
+
+ /**
+ * Component detach event sent when a component is detached from container.
+ */
+ @SuppressWarnings("serial")
+ public class ComponentDetachEvent extends Component.Event {
+
+ private final Component component;
+
+ /**
+ * Creates a new detach event.
+ *
+ * @param container
+ * the component container the component has been detached
+ * from.
+ * @param detachedComponent
+ * the component that has been detached.
+ */
+ public ComponentDetachEvent(ComponentContainer container,
+ Component detachedComponent) {
+ super(container);
+ component = detachedComponent;
+ }
+
+ /**
+ * Gets the component container.
+ *
+ * @param the
+ * component container.
+ */
+ public ComponentContainer getContainer() {
+ return (ComponentContainer) getSource();
+ }
+
+ /**
+ * Gets the detached component.
+ *
+ * @return the detached component.
+ */
+ public Component getDetachedComponent() {
+ return component;
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java
new file mode 100644
index 0000000000..e3d1bf86db
--- /dev/null
+++ b/server/src/com/vaadin/ui/ConnectorTracker.java
@@ -0,0 +1,320 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.terminal.AbstractClientConnector;
+import com.vaadin.terminal.gwt.client.ServerConnector;
+import com.vaadin.terminal.gwt.server.ClientConnector;
+
+/**
+ * A class which takes care of book keeping of {@link ClientConnector}s for a
+ * Root.
+ * <p>
+ * Provides {@link #getConnector(String)} which can be used to lookup a
+ * connector from its id. This is for framework use only and should not be
+ * needed in applications.
+ * </p>
+ * <p>
+ * Tracks which {@link ClientConnector}s are dirty so they can be updated to the
+ * client when the following response is sent. A connector is dirty when an
+ * operation has been performed on it on the server and as a result of this
+ * operation new information needs to be sent to its {@link ServerConnector}.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public class ConnectorTracker implements Serializable {
+
+ private final HashMap<String, ClientConnector> connectorIdToConnector = new HashMap<String, ClientConnector>();
+ private Set<ClientConnector> dirtyConnectors = new HashSet<ClientConnector>();
+
+ private Root root;
+
+ /**
+ * Gets a logger for this class
+ *
+ * @return A logger instance for logging within this class
+ *
+ */
+ public static Logger getLogger() {
+ return Logger.getLogger(ConnectorTracker.class.getName());
+ }
+
+ /**
+ * Creates a new ConnectorTracker for the given root. A tracker is always
+ * attached to a root and the root cannot be changed during the lifetime of
+ * a {@link ConnectorTracker}.
+ *
+ * @param root
+ * The root to attach to. Cannot be null.
+ */
+ public ConnectorTracker(Root root) {
+ this.root = root;
+ }
+
+ /**
+ * Register the given connector.
+ * <p>
+ * The lookup method {@link #getConnector(String)} only returns registered
+ * connectors.
+ * </p>
+ *
+ * @param connector
+ * The connector to register.
+ */
+ public void registerConnector(ClientConnector connector) {
+ String connectorId = connector.getConnectorId();
+ ClientConnector previouslyRegistered = connectorIdToConnector
+ .get(connectorId);
+ if (previouslyRegistered == null) {
+ connectorIdToConnector.put(connectorId, connector);
+ getLogger().fine(
+ "Registered " + connector.getClass().getSimpleName() + " ("
+ + connectorId + ")");
+ } else if (previouslyRegistered != connector) {
+ throw new RuntimeException("A connector with id " + connectorId
+ + " is already registered!");
+ } else {
+ getLogger().warning(
+ "An already registered connector was registered again: "
+ + connector.getClass().getSimpleName() + " ("
+ + connectorId + ")");
+ }
+
+ }
+
+ /**
+ * Unregister the given connector.
+ *
+ * <p>
+ * The lookup method {@link #getConnector(String)} only returns registered
+ * connectors.
+ * </p>
+ *
+ * @param connector
+ * The connector to unregister
+ */
+ public void unregisterConnector(ClientConnector connector) {
+ String connectorId = connector.getConnectorId();
+ if (!connectorIdToConnector.containsKey(connectorId)) {
+ getLogger().warning(
+ "Tried to unregister "
+ + connector.getClass().getSimpleName() + " ("
+ + connectorId + ") which is not registered");
+ return;
+ }
+ if (connectorIdToConnector.get(connectorId) != connector) {
+ throw new RuntimeException("The given connector with id "
+ + connectorId
+ + " is not the one that was registered for that id");
+ }
+
+ getLogger().fine(
+ "Unregistered " + connector.getClass().getSimpleName() + " ("
+ + connectorId + ")");
+ connectorIdToConnector.remove(connectorId);
+ }
+
+ /**
+ * Gets a connector by its id.
+ *
+ * @param connectorId
+ * The connector id to look for
+ * @return The connector with the given id or null if no connector has the
+ * given id
+ */
+ public ClientConnector getConnector(String connectorId) {
+ return connectorIdToConnector.get(connectorId);
+ }
+
+ /**
+ * Cleans the connector map from all connectors that are no longer attached
+ * to the application. This should only be called by the framework.
+ */
+ public void cleanConnectorMap() {
+ // remove detached components from paintableIdMap so they
+ // can be GC'ed
+ Iterator<String> iterator = connectorIdToConnector.keySet().iterator();
+
+ while (iterator.hasNext()) {
+ String connectorId = iterator.next();
+ ClientConnector connector = connectorIdToConnector.get(connectorId);
+ if (getRootForConnector(connector) != root) {
+ // If connector is no longer part of this root,
+ // remove it from the map. If it is re-attached to the
+ // application at some point it will be re-added through
+ // registerConnector(connector)
+
+ // This code should never be called as cleanup should take place
+ // in detach()
+ getLogger()
+ .warning(
+ "cleanConnectorMap unregistered connector "
+ + getConnectorAndParentInfo(connector)
+ + "). This should have been done when the connector was detached.");
+ iterator.remove();
+ }
+ }
+
+ }
+
+ /**
+ * Finds the root that the connector is attached to.
+ *
+ * @param connector
+ * The connector to lookup
+ * @return The root the connector is attached to or null if it is not
+ * attached to any root.
+ */
+ private Root getRootForConnector(ClientConnector connector) {
+ if (connector == null) {
+ return null;
+ }
+ if (connector instanceof Component) {
+ return ((Component) connector).getRoot();
+ }
+
+ return getRootForConnector(connector.getParent());
+ }
+
+ /**
+ * Mark the connector as dirty.
+ *
+ * @see #getDirtyConnectors()
+ *
+ * @param connector
+ * The connector that should be marked clean.
+ */
+ public void markDirty(ClientConnector connector) {
+ if (getLogger().isLoggable(Level.FINE)) {
+ if (!dirtyConnectors.contains(connector)) {
+ getLogger().fine(
+ getConnectorAndParentInfo(connector) + " "
+ + "is now dirty");
+ }
+ }
+
+ dirtyConnectors.add(connector);
+ }
+
+ /**
+ * Mark the connector as clean.
+ *
+ * @param connector
+ * The connector that should be marked clean.
+ */
+ public void markClean(ClientConnector connector) {
+ if (getLogger().isLoggable(Level.FINE)) {
+ if (dirtyConnectors.contains(connector)) {
+ getLogger().fine(
+ getConnectorAndParentInfo(connector) + " "
+ + "is no longer dirty");
+ }
+ }
+
+ dirtyConnectors.remove(connector);
+ }
+
+ /**
+ * Returns {@link #getConnectorString(ClientConnector)} for the connector
+ * and its parent (if it has a parent).
+ *
+ * @param connector
+ * The connector
+ * @return A string describing the connector and its parent
+ */
+ private String getConnectorAndParentInfo(ClientConnector connector) {
+ String message = getConnectorString(connector);
+ if (connector.getParent() != null) {
+ message += " (parent: " + getConnectorString(connector.getParent())
+ + ")";
+ }
+ return message;
+ }
+
+ /**
+ * Returns a string with the connector name and id. Useful mostly for
+ * debugging and logging.
+ *
+ * @param connector
+ * The connector
+ * @return A string that describes the connector
+ */
+ private String getConnectorString(ClientConnector connector) {
+ if (connector == null) {
+ return "(null)";
+ }
+
+ String connectorId;
+ try {
+ connectorId = connector.getConnectorId();
+ } catch (RuntimeException e) {
+ // This happens if the connector is not attached to the application.
+ // SHOULD not happen in this case but theoretically can.
+ connectorId = "@" + Integer.toHexString(connector.hashCode());
+ }
+ return connector.getClass().getName() + "(" + connectorId + ")";
+ }
+
+ /**
+ * Mark all connectors in this root as dirty.
+ */
+ public void markAllConnectorsDirty() {
+ markConnectorsDirtyRecursively(root);
+ getLogger().fine("All connectors are now dirty");
+ }
+
+ /**
+ * Mark all connectors in this root as clean.
+ */
+ public void markAllConnectorsClean() {
+ dirtyConnectors.clear();
+ getLogger().fine("All connectors are now clean");
+ }
+
+ /**
+ * Marks all visible connectors dirty, starting from the given connector and
+ * going downwards in the hierarchy.
+ *
+ * @param c
+ * The component to start iterating downwards from
+ */
+ private void markConnectorsDirtyRecursively(ClientConnector c) {
+ if (c instanceof Component && !((Component) c).isVisible()) {
+ return;
+ }
+ markDirty(c);
+ for (ClientConnector child : AbstractClientConnector
+ .getAllChildrenIterable(c)) {
+ markConnectorsDirtyRecursively(child);
+ }
+ }
+
+ /**
+ * Returns a collection of all connectors which have been marked as dirty.
+ * <p>
+ * The state and pending RPC calls for dirty connectors are sent to the
+ * client in the following request.
+ * </p>
+ *
+ * @return A collection of all dirty connectors for this root. This list may
+ * contain invisible connectors.
+ */
+ public Collection<ClientConnector> getDirtyConnectors() {
+ return dirtyConnectors;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/CssLayout.java b/server/src/com/vaadin/ui/CssLayout.java
new file mode 100644
index 0000000000..356f0a3843
--- /dev/null
+++ b/server/src/com/vaadin/ui/CssLayout.java
@@ -0,0 +1,308 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc;
+import com.vaadin.shared.ui.csslayout.CssLayoutState;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+
+/**
+ * CssLayout is a layout component that can be used in browser environment only.
+ * It simply renders components and their captions into a same div element.
+ * Component layout can then be adjusted with css.
+ * <p>
+ * In comparison to {@link HorizontalLayout} and {@link VerticalLayout}
+ * <ul>
+ * <li>rather similar server side api
+ * <li>no spacing, alignment or expand ratios
+ * <li>much simpler DOM that can be styled by skilled web developer
+ * <li>no abstraction of browser differences (developer must ensure that the
+ * result works properly on each browser)
+ * <li>different kind of handling for relative sizes (that are set from server
+ * side) (*)
+ * <li>noticeably faster rendering time in some situations as we rely more on
+ * the browser's rendering engine.
+ * </ul>
+ * <p>
+ * With {@link CustomLayout} one can often achieve similar results (good looking
+ * layouts with web technologies), but with CustomLayout developer needs to work
+ * with fixed templates.
+ * <p>
+ * By extending CssLayout one can also inject some css rules straight to child
+ * components using {@link #getCss(Component)}.
+ *
+ * <p>
+ * (*) Relative sizes (set from server side) are treated bit differently than in
+ * other layouts in Vaadin. In cssLayout the size is calculated relatively to
+ * CSS layouts content area which is pretty much as in html and css. In other
+ * layouts the size of component is calculated relatively to the "slot" given by
+ * layout.
+ * <p>
+ * Also note that client side framework in Vaadin modifies inline style
+ * properties width and height. This happens on each update to component. If one
+ * wants to set component sizes with CSS, component must have undefined size on
+ * server side (which is not the default for all components) and the size must
+ * be defined with class styles - not by directly injecting width and height.
+ *
+ * @since 6.1 brought in from "FastLayouts" incubator project
+ *
+ */
+public class CssLayout extends AbstractLayout implements LayoutClickNotifier {
+
+ private CssLayoutServerRpc rpc = new CssLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(CssLayout.this,
+ mouseDetails, clickedConnector));
+ }
+ };
+ /**
+ * Custom layout slots containing the components.
+ */
+ protected LinkedList<Component> components = new LinkedList<Component>();
+
+ public CssLayout() {
+ registerRpc(rpc);
+ }
+
+ /**
+ * Add a component into this container. The component is added to the right
+ * or under the previous component.
+ *
+ * @param c
+ * the component to be added.
+ */
+ @Override
+ public void addComponent(Component c) {
+ // Add to components before calling super.addComponent
+ // so that it is available to AttachListeners
+ components.add(c);
+ try {
+ super.addComponent(c);
+ requestRepaint();
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ }
+
+ /**
+ * Adds a component into this container. The component is added to the left
+ * or on top of the other components.
+ *
+ * @param c
+ * the component to be added.
+ */
+ public void addComponentAsFirst(Component c) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (c.getParent() == this) {
+ removeComponent(c);
+ }
+ components.addFirst(c);
+ try {
+ super.addComponent(c);
+ requestRepaint();
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ }
+
+ /**
+ * Adds a component into indexed position in this container.
+ *
+ * @param c
+ * the component to be added.
+ * @param index
+ * the index of the component position. The components currently
+ * in and after the position are shifted forwards.
+ */
+ public void addComponent(Component c, int index) {
+ // If c is already in this, we must remove it before proceeding
+ // see ticket #7668
+ if (c.getParent() == this) {
+ // When c is removed, all components after it are shifted down
+ if (index > getComponentIndex(c)) {
+ index--;
+ }
+ removeComponent(c);
+ }
+ components.add(index, c);
+ try {
+ super.addComponent(c);
+ requestRepaint();
+ } catch (IllegalArgumentException e) {
+ components.remove(c);
+ throw e;
+ }
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component c) {
+ components.remove(c);
+ super.removeComponent(c);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the component container iterator for going trough all the components
+ * in the container.
+ *
+ * @return the Iterator of the components inside the container.
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return components.iterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ @Override
+ public void updateState() {
+ super.updateState();
+ getState().getChildCss().clear();
+ for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) {
+ Component child = ci.next();
+ String componentCssString = getCss(child);
+ if (componentCssString != null) {
+ getState().getChildCss().put(child, componentCssString);
+ }
+
+ }
+ }
+
+ @Override
+ public CssLayoutState getState() {
+ return (CssLayoutState) super.getState();
+ }
+
+ /**
+ * Returns styles to be applied to given component. Override this method to
+ * inject custom style rules to components.
+ *
+ * <p>
+ * Note that styles are injected over previous styles before actual child
+ * rendering. Previous styles are not cleared, but overridden.
+ *
+ * <p>
+ * Note that one most often achieves better code style, by separating
+ * styling to theme (with custom theme and {@link #addStyleName(String)}.
+ * With own custom styles it is also very easy to break browser
+ * compatibility.
+ *
+ * @param c
+ * the component
+ * @return css rules to be applied to component
+ */
+ protected String getCss(Component c) {
+ return null;
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ int oldLocation = -1;
+ int newLocation = -1;
+ int location = 0;
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+
+ location++;
+ }
+
+ if (oldLocation == -1) {
+ addComponent(newComponent);
+ } else if (newLocation == -1) {
+ removeComponent(oldComponent);
+ addComponent(newComponent, oldLocation);
+ } else {
+ if (oldLocation > newLocation) {
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ } else {
+ components.remove(newComponent);
+ components.add(oldLocation, newComponent);
+ components.remove(oldComponent);
+ components.add(newLocation, oldComponent);
+ }
+
+ requestRepaint();
+ }
+ }
+
+ @Override
+ public void addListener(LayoutClickListener listener) {
+ addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ @Override
+ public void removeListener(LayoutClickListener listener) {
+ removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+ /**
+ * Returns the index of the given component.
+ *
+ * @param component
+ * The component to look up.
+ * @return The index of the component or -1 if the component is not a child.
+ */
+ public int getComponentIndex(Component component) {
+ return components.indexOf(component);
+ }
+
+ /**
+ * Returns the component at the given position.
+ *
+ * @param index
+ * The position of the component.
+ * @return The component at the given index.
+ * @throws IndexOutOfBoundsException
+ * If the index is out of range.
+ */
+ public Component getComponent(int index) throws IndexOutOfBoundsException {
+ return components.get(index);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/CustomComponent.java b/server/src/com/vaadin/ui/CustomComponent.java
new file mode 100644
index 0000000000..40b5dcd636
--- /dev/null
+++ b/server/src/com/vaadin/ui/CustomComponent.java
@@ -0,0 +1,189 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.Iterator;
+
+/**
+ * Custom component provides simple implementation of Component interface for
+ * creation of new UI components by composition of existing components.
+ * <p>
+ * The component is used by inheriting the CustomComponent class and setting
+ * composite root inside the Custom component. The composite root itself can
+ * contain more components, but their interfaces are hidden from the users.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class CustomComponent extends AbstractComponentContainer {
+
+ /**
+ * The root component implementing the custom component.
+ */
+ private Component root = null;
+
+ /**
+ * Constructs a new custom component.
+ *
+ * <p>
+ * The component is implemented by wrapping the methods of the composition
+ * root component given as parameter. The composition root must be set
+ * before the component can be used.
+ * </p>
+ */
+ public CustomComponent() {
+ // expand horizontally by default
+ setWidth(100, UNITS_PERCENTAGE);
+ }
+
+ /**
+ * Constructs a new custom component.
+ *
+ * <p>
+ * The component is implemented by wrapping the methods of the composition
+ * root component given as parameter. The composition root must not be null
+ * and can not be changed after the composition.
+ * </p>
+ *
+ * @param compositionRoot
+ * the root of the composition component tree.
+ */
+ public CustomComponent(Component compositionRoot) {
+ this();
+ setCompositionRoot(compositionRoot);
+ }
+
+ /**
+ * Returns the composition root.
+ *
+ * @return the Component Composition root.
+ */
+ protected Component getCompositionRoot() {
+ return root;
+ }
+
+ /**
+ * Sets the compositions root.
+ * <p>
+ * The composition root must be set to non-null value before the component
+ * can be used. The composition root can only be set once.
+ * </p>
+ *
+ * @param compositionRoot
+ * the root of the composition component tree.
+ */
+ protected void setCompositionRoot(Component compositionRoot) {
+ if (compositionRoot != root) {
+ if (root != null) {
+ // remove old component
+ super.removeComponent(root);
+ }
+ if (compositionRoot != null) {
+ // set new component
+ super.addComponent(compositionRoot);
+ }
+ root = compositionRoot;
+ requestRepaint();
+ }
+ }
+
+ /* Basic component features ------------------------------------------ */
+
+ private class ComponentIterator implements Iterator<Component>,
+ Serializable {
+ boolean first = getCompositionRoot() != null;
+
+ @Override
+ public boolean hasNext() {
+ return first;
+ }
+
+ @Override
+ public Component next() {
+ first = false;
+ return root;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return new ComponentIterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components (zero or one)
+ */
+ @Override
+ public int getComponentCount() {
+ return (root != null ? 1 : 0);
+ }
+
+ /**
+ * This method is not supported by CustomComponent.
+ *
+ * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component,
+ * com.vaadin.ui.Component)
+ */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This method is not supported by CustomComponent. Use
+ * {@link CustomComponent#setCompositionRoot(Component)} to set
+ * CustomComponents "child".
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component)
+ */
+ @Override
+ public void addComponent(Component c) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This method is not supported by CustomComponent.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer)
+ */
+ @Override
+ public void moveComponentsFrom(ComponentContainer source) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This method is not supported by CustomComponent.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents()
+ */
+ @Override
+ public void removeAllComponents() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * This method is not supported by CustomComponent.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component)
+ */
+ @Override
+ public void removeComponent(Component c) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/CustomField.java b/server/src/com/vaadin/ui/CustomField.java
new file mode 100644
index 0000000000..ab3797a58c
--- /dev/null
+++ b/server/src/com/vaadin/ui/CustomField.java
@@ -0,0 +1,237 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+
+import com.vaadin.data.Property;
+
+/**
+ * A {@link Field} whose UI content can be constructed by the user, enabling the
+ * creation of e.g. form fields by composing Vaadin components. Customization of
+ * both the visual presentation and the logic of the field is possible.
+ *
+ * Subclasses must implement {@link #getType()} and {@link #initContent()}.
+ *
+ * Most custom fields can simply compose a user interface that calls the methods
+ * {@link #setInternalValue(Object)} and {@link #getInternalValue()} when
+ * necessary.
+ *
+ * It is also possible to override {@link #validate()},
+ * {@link #setInternalValue(Object)}, {@link #commit()},
+ * {@link #setPropertyDataSource(Property)}, {@link #isEmpty()} and other logic
+ * of the field. Methods overriding {@link #setInternalValue(Object)} should
+ * also call the corresponding superclass method.
+ *
+ * @param <T>
+ * field value type
+ *
+ * @since 7.0
+ */
+public abstract class CustomField<T> extends AbstractField<T> implements
+ ComponentContainer {
+
+ /**
+ * The root component implementing the custom component.
+ */
+ private Component root = null;
+
+ /**
+ * Constructs a new custom field.
+ *
+ * <p>
+ * The component is implemented by wrapping the methods of the composition
+ * root component given as parameter. The composition root must be set
+ * before the component can be used.
+ * </p>
+ */
+ public CustomField() {
+ // expand horizontally by default
+ setWidth(100, Unit.PERCENTAGE);
+ }
+
+ /**
+ * Constructs the content and notifies it that the {@link CustomField} is
+ * attached to a window.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+ @Override
+ public void attach() {
+ // First call super attach to notify all children (none if content has
+ // not yet been created)
+ super.attach();
+
+ // If the content has not yet been created, we create and attach it at
+ // this point.
+ if (root == null) {
+ // Ensure content is created and its parent is set.
+ // The getContent() call creates the content and attaches the
+ // content
+ fireComponentAttachEvent(getContent());
+ }
+ }
+
+ /**
+ * Returns the content (UI) of the custom component.
+ *
+ * @return Component
+ */
+ protected Component getContent() {
+ if (null == root) {
+ root = initContent();
+ root.setParent(this);
+ }
+ return root;
+ }
+
+ /**
+ * Create the content component or layout for the field. Subclasses of
+ * {@link CustomField} should implement this method.
+ *
+ * Note that this method is called when the CustomField is attached to a
+ * layout or when {@link #getContent()} is called explicitly for the first
+ * time. It is only called once for a {@link CustomField}.
+ *
+ * @return {@link Component} representing the UI of the CustomField
+ */
+ protected abstract Component initContent();
+
+ // Size related methods
+ // TODO might not be necessary to override but following the pattern from
+ // AbstractComponentContainer
+
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ requestRepaintAll();
+ }
+
+ @Override
+ public void setWidth(float height, Unit unit) {
+ super.setWidth(height, unit);
+ requestRepaintAll();
+ }
+
+ // ComponentContainer methods
+
+ private class ComponentIterator implements Iterator<Component>,
+ Serializable {
+ boolean first = (root != null);
+
+ @Override
+ public boolean hasNext() {
+ return first;
+ }
+
+ @Override
+ public Component next() {
+ first = false;
+ return getContent();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return new ComponentIterator();
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ return getComponentIterator();
+ }
+
+ @Override
+ public int getComponentCount() {
+ return (null != getContent()) ? 1 : 0;
+ }
+
+ /**
+ * Fires the component attached event. This should be called by the
+ * addComponent methods after the component have been added to this
+ * container.
+ *
+ * @param component
+ * the component that has been added to this container.
+ */
+ protected void fireComponentAttachEvent(Component component) {
+ fireEvent(new ComponentAttachEvent(this, component));
+ }
+
+ // TODO remove these methods when ComponentContainer interface is cleaned up
+
+ @Override
+ public void addComponent(Component c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeComponent(Component c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeAllComponents() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void moveComponentsFrom(ComponentContainer source) {
+ throw new UnsupportedOperationException();
+ }
+
+ private static final Method COMPONENT_ATTACHED_METHOD;
+
+ static {
+ try {
+ COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class
+ .getDeclaredMethod("componentAttachedToContainer",
+ new Class[] { ComponentAttachEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in CustomField");
+ }
+ }
+
+ @Override
+ public void addListener(ComponentAttachListener listener) {
+ addListener(ComponentContainer.ComponentAttachEvent.class, listener,
+ COMPONENT_ATTACHED_METHOD);
+ }
+
+ @Override
+ public void removeListener(ComponentAttachListener listener) {
+ removeListener(ComponentContainer.ComponentAttachEvent.class, listener,
+ COMPONENT_ATTACHED_METHOD);
+ }
+
+ @Override
+ public void addListener(ComponentDetachListener listener) {
+ // content never detached
+ }
+
+ @Override
+ public void removeListener(ComponentDetachListener listener) {
+ // content never detached
+ }
+
+ @Override
+ public boolean isComponentVisible(Component childComponent) {
+ return true;
+ }
+}
diff --git a/server/src/com/vaadin/ui/CustomLayout.java b/server/src/com/vaadin/ui/CustomLayout.java
new file mode 100644
index 0000000000..d7830603f0
--- /dev/null
+++ b/server/src/com/vaadin/ui/CustomLayout.java
@@ -0,0 +1,329 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.shared.ui.customlayout.CustomLayoutState;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.server.JsonPaintTarget;
+
+/**
+ * <p>
+ * A container component with freely designed layout and style. The layout
+ * consists of items with textually represented locations. Each item contains
+ * one sub-component, which can be any Vaadin component, such as a layout. The
+ * adapter and theme are responsible for rendering the layout with a given style
+ * by placing the items in the defined locations.
+ * </p>
+ *
+ * <p>
+ * The placement of the locations is not fixed - different themes can define the
+ * locations in a way that is suitable for them. One typical example would be to
+ * create visual design for a web site as a custom layout: the visual design
+ * would define locations for "menu", "body", and "title", for example. The
+ * layout would then be implemented as an XHTML template for each theme.
+ * </p>
+ *
+ * <p>
+ * The default theme handles the styles that are not defined by drawing the
+ * subcomponents just as in OrderedLayout.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @author Duy B. Vo (<a
+ * href="mailto:devduy@gmail.com?subject=Vaadin">devduy@gmail.com</a>)
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class CustomLayout extends AbstractLayout implements Vaadin6Component {
+
+ private static final int BUFFER_SIZE = 10000;
+
+ /**
+ * Custom layout slots containing the components.
+ */
+ private final HashMap<String, Component> slots = new HashMap<String, Component>();
+
+ /**
+ * Default constructor only used by subclasses. Subclasses are responsible
+ * for setting the appropriate fields. Either
+ * {@link #setTemplateName(String)}, that makes layout fetch the template
+ * from theme, or {@link #setTemplateContents(String)}.
+ */
+ protected CustomLayout() {
+ setWidth(100, UNITS_PERCENTAGE);
+ }
+
+ /**
+ * Constructs a custom layout with the template given in the stream.
+ *
+ * @param templateStream
+ * Stream containing template data. Must be using UTF-8 encoding.
+ * To use a String as a template use for instance new
+ * ByteArrayInputStream("<template>".getBytes()).
+ * @param streamLength
+ * Length of the templateStream
+ * @throws IOException
+ */
+ public CustomLayout(InputStream templateStream) throws IOException {
+ this();
+ initTemplateContentsFromInputStream(templateStream);
+ }
+
+ /**
+ * Constructor for custom layout with given template name. Template file is
+ * fetched from "<theme>/layout/<templateName>".
+ */
+ public CustomLayout(String template) {
+ this();
+ setTemplateName(template);
+ }
+
+ protected void initTemplateContentsFromInputStream(
+ InputStream templateStream) throws IOException {
+ InputStreamReader reader = new InputStreamReader(templateStream,
+ "UTF-8");
+ StringBuilder b = new StringBuilder(BUFFER_SIZE);
+
+ char[] cbuf = new char[BUFFER_SIZE];
+ int offset = 0;
+
+ while (true) {
+ int nrRead = reader.read(cbuf, offset, BUFFER_SIZE);
+ b.append(cbuf, 0, nrRead);
+ if (nrRead < BUFFER_SIZE) {
+ break;
+ }
+ }
+
+ setTemplateContents(b.toString());
+ }
+
+ @Override
+ public CustomLayoutState getState() {
+ return (CustomLayoutState) super.getState();
+ }
+
+ /**
+ * Adds the component into this container to given location. If the location
+ * is already populated, the old component is removed.
+ *
+ * @param c
+ * the component to be added.
+ * @param location
+ * the location of the component.
+ */
+ public void addComponent(Component c, String location) {
+ final Component old = slots.get(location);
+ if (old != null) {
+ removeComponent(old);
+ }
+ slots.put(location, c);
+ getState().getChildLocations().put(c, location);
+ c.setParent(this);
+ fireComponentAttachEvent(c);
+ requestRepaint();
+ }
+
+ /**
+ * Adds the component into this container. The component is added without
+ * specifying the location (empty string is then used as location). Only one
+ * component can be added to the default "" location and adding more
+ * components into that location overwrites the old components.
+ *
+ * @param c
+ * the component to be added.
+ */
+ @Override
+ public void addComponent(Component c) {
+ this.addComponent(c, "");
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component c) {
+ if (c == null) {
+ return;
+ }
+ slots.values().remove(c);
+ getState().getChildLocations().remove(c);
+ super.removeComponent(c);
+ requestRepaint();
+ }
+
+ /**
+ * Removes the component from this container from given location.
+ *
+ * @param location
+ * the Location identifier of the component.
+ */
+ public void removeComponent(String location) {
+ this.removeComponent(slots.get(location));
+ }
+
+ /**
+ * Gets the component container iterator for going trough all the components
+ * in the container.
+ *
+ * @return the Iterator of the components inside the container.
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return slots.values().iterator();
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return slots.values().size();
+ }
+
+ /**
+ * Gets the child-component by its location.
+ *
+ * @param location
+ * the name of the location where the requested component
+ * resides.
+ * @return the Component in the given location or null if not found.
+ */
+ public Component getComponent(String location) {
+ return slots.get(location);
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ String oldLocation = null;
+ String newLocation = null;
+ for (final Iterator<String> i = slots.keySet().iterator(); i.hasNext();) {
+ final String location = i.next();
+ final Component component = slots.get(location);
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+ }
+
+ if (oldLocation == null) {
+ addComponent(newComponent);
+ } else if (newLocation == null) {
+ removeComponent(oldLocation);
+ addComponent(newComponent, oldLocation);
+ } else {
+ slots.put(newLocation, oldComponent);
+ slots.put(oldLocation, newComponent);
+ getState().getChildLocations().put(newComponent, oldLocation);
+ getState().getChildLocations().put(oldComponent, newLocation);
+ requestRepaint();
+ }
+ }
+
+ /** Get the name of the template */
+ public String getTemplateName() {
+ return getState().getTemplateName();
+ }
+
+ /** Get the contents of the template */
+ public String getTemplateContents() {
+ return getState().getTemplateContents();
+ }
+
+ /**
+ * Set the name of the template used to draw custom layout.
+ *
+ * With GWT-adapter, the template with name 'templatename' is loaded from
+ * VAADIN/themes/themename/layouts/templatename.html. If the theme has not
+ * been set (with Application.setTheme()), themename is 'default'.
+ *
+ * @param templateName
+ */
+ public void setTemplateName(String templateName) {
+ getState().setTemplateName(templateName);
+ getState().setTemplateContents(null);
+ requestRepaint();
+ }
+
+ /**
+ * Set the contents of the template used to draw the custom layout.
+ *
+ * @param templateContents
+ */
+ public void setTemplateContents(String templateContents) {
+ getState().setTemplateContents(templateContents);
+ getState().setTemplateName(null);
+ requestRepaint();
+ }
+
+ /**
+ * Although most layouts support margins, CustomLayout does not. The
+ * behaviour of this layout is determined almost completely by the actual
+ * template.
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void setMargin(boolean enabled) {
+ throw new UnsupportedOperationException(
+ "CustomLayout does not support margins.");
+ }
+
+ /**
+ * Although most layouts support margins, CustomLayout does not. The
+ * behaviour of this layout is determined almost completely by the actual
+ * template.
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void setMargin(boolean topEnabled, boolean rightEnabled,
+ boolean bottomEnabled, boolean leftEnabled) {
+ throw new UnsupportedOperationException(
+ "CustomLayout does not support margins.");
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Nothing to see here
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ // Workaround to make the CommunicationManager read the template file
+ // and send it to the client
+ String templateName = getState().getTemplateName();
+ if (templateName != null && templateName.length() != 0) {
+ Set<Object> usedResources = ((JsonPaintTarget) target)
+ .getUsedResources();
+ String resourceName = "layouts/" + templateName + ".html";
+ usedResources.add(resourceName);
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/DateField.java b/server/src/com/vaadin/ui/DateField.java
new file mode 100644
index 0000000000..d0a22f3c29
--- /dev/null
+++ b/server/src/com/vaadin/ui/DateField.java
@@ -0,0 +1,869 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+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.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.datefield.VDateField;
+
+/**
+ * <p>
+ * A date editor component that can be bound to any {@link Property} that is
+ * compatible with <code>java.util.Date</code>.
+ * </p>
+ * <p>
+ * Since <code>DateField</code> extends <code>AbstractField</code> it implements
+ * the {@link com.vaadin.data.Buffered}interface.
+ * </p>
+ * <p>
+ * A <code>DateField</code> is in write-through mode by default, so
+ * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)}must be called to
+ * enable buffering.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class DateField extends AbstractField<Date> implements
+ FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Vaadin6Component {
+
+ /**
+ * Resolutions for DateFields
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 7.0
+ */
+ public enum Resolution {
+ SECOND(Calendar.SECOND), MINUTE(Calendar.MINUTE), HOUR(
+ Calendar.HOUR_OF_DAY), DAY(Calendar.DAY_OF_MONTH), MONTH(
+ Calendar.MONTH), YEAR(Calendar.YEAR);
+
+ private int calendarField;
+
+ private Resolution(int calendarField) {
+ this.calendarField = calendarField;
+ }
+
+ /**
+ * Returns the field in {@link Calendar} that corresponds to this
+ * resolution.
+ *
+ * @return one of the field numbers used by Calendar
+ */
+ public int getCalendarField() {
+ return calendarField;
+ }
+
+ /**
+ * Returns the resolutions that are higher or equal to the given
+ * resolution, starting from the given resolution. In other words
+ * passing DAY to this methods returns DAY,MONTH,YEAR
+ *
+ * @param r
+ * The resolution to start from
+ * @return An iterable for the resolutions higher or equal to r
+ */
+ public static Iterable<Resolution> getResolutionsHigherOrEqualTo(
+ Resolution r) {
+ List<Resolution> resolutions = new ArrayList<DateField.Resolution>();
+ Resolution[] values = Resolution.values();
+ for (int i = r.ordinal(); i < values.length; i++) {
+ resolutions.add(values[i]);
+ }
+ return resolutions;
+ }
+
+ /**
+ * Returns the resolutions that are lower than the given resolution,
+ * starting from the given resolution. In other words passing DAY to
+ * this methods returns HOUR,MINUTE,SECOND.
+ *
+ * @param r
+ * The resolution to start from
+ * @return An iterable for the resolutions lower than r
+ */
+ public static List<Resolution> getResolutionsLowerThan(Resolution r) {
+ List<Resolution> resolutions = new ArrayList<DateField.Resolution>();
+ Resolution[] values = Resolution.values();
+ for (int i = r.ordinal() - 1; i >= 0; i--) {
+ resolutions.add(values[i]);
+ }
+ return resolutions;
+ }
+ };
+
+ /**
+ * Resolution identifier: seconds.
+ *
+ * @deprecated Use {@link Resolution#SECOND}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_SEC = Resolution.SECOND;
+
+ /**
+ * Resolution identifier: minutes.
+ *
+ * @deprecated Use {@link Resolution#MINUTE}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_MIN = Resolution.MINUTE;
+
+ /**
+ * Resolution identifier: hours.
+ *
+ * @deprecated Use {@link Resolution#HOUR}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_HOUR = Resolution.HOUR;
+
+ /**
+ * Resolution identifier: days.
+ *
+ * @deprecated Use {@link Resolution#DAY}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_DAY = Resolution.DAY;
+
+ /**
+ * Resolution identifier: months.
+ *
+ * @deprecated Use {@link Resolution#MONTH}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_MONTH = Resolution.MONTH;
+
+ /**
+ * Resolution identifier: years.
+ *
+ * @deprecated Use {@link Resolution#YEAR}
+ */
+ @Deprecated
+ public static final Resolution RESOLUTION_YEAR = Resolution.YEAR;
+
+ /**
+ * Specified smallest modifiable unit for the date field.
+ */
+ private Resolution resolution = Resolution.DAY;
+
+ /**
+ * The internal calendar to be used in java.utl.Date conversions.
+ */
+ private transient Calendar calendar;
+
+ /**
+ * Overridden format string
+ */
+ private String dateFormat;
+
+ private boolean lenient = false;
+
+ private String dateString = null;
+
+ /**
+ * Was the last entered string parsable? If this flag is false, datefields
+ * internal validator does not pass.
+ */
+ private boolean uiHasValidDateString = true;
+
+ /**
+ * Determines if week numbers are shown in the date selector.
+ */
+ private boolean showISOWeekNumbers = false;
+
+ private String currentParseErrorMessage;
+
+ private String defaultParseErrorMessage = "Date format not recognized";
+
+ private TimeZone timeZone = null;
+
+ private static Map<Resolution, String> variableNameForResolution = new HashMap<DateField.Resolution, String>();
+ {
+ variableNameForResolution.put(Resolution.SECOND, "sec");
+ variableNameForResolution.put(Resolution.MINUTE, "min");
+ variableNameForResolution.put(Resolution.HOUR, "hour");
+ variableNameForResolution.put(Resolution.DAY, "day");
+ variableNameForResolution.put(Resolution.MONTH, "month");
+ variableNameForResolution.put(Resolution.YEAR, "year");
+ }
+
+ /* Constructors */
+
+ /**
+ * Constructs an empty <code>DateField</code> with no caption.
+ */
+ public DateField() {
+ }
+
+ /**
+ * Constructs an empty <code>DateField</code> with caption.
+ *
+ * @param caption
+ * the caption of the datefield.
+ */
+ public DateField(String caption) {
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>DateField</code> that's bound to the specified
+ * <code>Property</code> and has the given caption <code>String</code>.
+ *
+ * @param caption
+ * the caption <code>String</code> for the editor.
+ * @param dataSource
+ * the Property to be edited with this editor.
+ */
+ public DateField(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>DateField</code> that's bound to the specified
+ * <code>Property</code> and has no caption.
+ *
+ * @param dataSource
+ * the Property to be edited with this editor.
+ */
+ public DateField(Property dataSource) throws IllegalArgumentException {
+ if (!Date.class.isAssignableFrom(dataSource.getType())) {
+ throw new IllegalArgumentException("Can't use "
+ + dataSource.getType().getName()
+ + " typed property as datasource");
+ }
+
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a new <code>DateField</code> with the given caption and
+ * initial text contents. The editor constructed this way will not be bound
+ * to a Property unless
+ * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)}
+ * is called to bind it.
+ *
+ * @param caption
+ * the caption <code>String</code> for the editor.
+ * @param value
+ * the Date value.
+ */
+ public DateField(String caption, Date value) {
+ setValue(value);
+ setCaption(caption);
+ }
+
+ /* Component basic features */
+
+ /*
+ * Paints this component. Don't add a JavaDoc comment here, we use the
+ * default documentation from implemented interface.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ // Adds the locale as attribute
+ final Locale l = getLocale();
+ if (l != null) {
+ target.addAttribute("locale", l.toString());
+ }
+
+ if (getDateFormat() != null) {
+ target.addAttribute("format", dateFormat);
+ }
+
+ if (!isLenient()) {
+ target.addAttribute("strict", true);
+ }
+
+ target.addAttribute(VDateField.WEEK_NUMBERS, isShowISOWeekNumbers());
+ target.addAttribute("parsable", uiHasValidDateString);
+ /*
+ * TODO communicate back the invalid date string? E.g. returning back to
+ * app or refresh.
+ */
+
+ // Gets the calendar
+ final Calendar calendar = getCalendar();
+ final Date currentDate = getValue();
+
+ // Only paint variables for the resolution and up, e.g. Resolution DAY
+ // paints DAY,MONTH,YEAR
+ for (Resolution res : Resolution
+ .getResolutionsHigherOrEqualTo(resolution)) {
+ int value = -1;
+ if (currentDate != null) {
+ value = calendar.get(res.getCalendarField());
+ if (res == Resolution.MONTH) {
+ // Calendar month is zero based
+ value++;
+ }
+ }
+ target.addVariable(this, variableNameForResolution.get(res), value);
+ }
+ }
+
+ @Override
+ protected boolean shouldHideErrors() {
+ return super.shouldHideErrors() && uiHasValidDateString;
+ }
+
+ /*
+ * Invoked when a variable of the component changes. Don't add a JavaDoc
+ * comment here, we use the default documentation from implemented
+ * interface.
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ if (!isReadOnly()
+ && (variables.containsKey("year")
+ || variables.containsKey("month")
+ || variables.containsKey("day")
+ || variables.containsKey("hour")
+ || variables.containsKey("min")
+ || variables.containsKey("sec")
+ || variables.containsKey("msec") || variables
+ .containsKey("dateString"))) {
+
+ // Old and new dates
+ final Date oldDate = getValue();
+ Date newDate = null;
+
+ // this enables analyzing invalid input on the server
+ final String newDateString = (String) variables.get("dateString");
+ dateString = newDateString;
+
+ // Gets the new date in parts
+ boolean hasChanges = false;
+ Map<Resolution, Integer> calendarFieldChanges = new HashMap<DateField.Resolution, Integer>();
+
+ for (Resolution r : Resolution
+ .getResolutionsHigherOrEqualTo(resolution)) {
+ // Only handle what the client is allowed to send. The same
+ // resolutions that are painted
+ String variableName = variableNameForResolution.get(r);
+
+ if (variables.containsKey(variableName)) {
+ Integer value = (Integer) variables.get(variableName);
+ if (r == Resolution.MONTH) {
+ // Calendar MONTH is zero based
+ value--;
+ }
+ if (value >= 0) {
+ hasChanges = true;
+ calendarFieldChanges.put(r, value);
+ }
+ }
+ }
+
+ // If no new variable values were received, use the previous value
+ if (!hasChanges) {
+ newDate = null;
+ } else {
+ // Clone the calendar for date operation
+ final Calendar cal = getCalendar();
+
+ // Update the value based on the received info
+ // Must set in this order to avoid invalid dates (or wrong
+ // dates if lenient is true) in calendar
+ for (int r = Resolution.YEAR.ordinal(); r >= 0; r--) {
+ Resolution res = Resolution.values()[r];
+ if (calendarFieldChanges.containsKey(res)) {
+
+ // Field resolution should be included. Others are
+ // skipped so that client can not make unexpected
+ // changes (e.g. day change even though resolution is
+ // year).
+ Integer newValue = calendarFieldChanges.get(res);
+ cal.set(res.getCalendarField(), newValue);
+ }
+ }
+ newDate = cal.getTime();
+ }
+
+ if (newDate == null && dateString != null && !"".equals(dateString)) {
+ try {
+ Date parsedDate = handleUnparsableDateString(dateString);
+ setValue(parsedDate, true);
+
+ /*
+ * Ensure the value is sent to the client if the value is
+ * set to the same as the previous (#4304). Does not repaint
+ * if handleUnparsableDateString throws an exception. In
+ * this case the invalid text remains in the DateField.
+ */
+ requestRepaint();
+ } catch (Converter.ConversionException e) {
+
+ /*
+ * Datefield now contains some text that could't be parsed
+ * into date.
+ */
+ if (oldDate != null) {
+ /*
+ * Set the logic value to null.
+ */
+ setValue(null);
+ /*
+ * Reset the dateString (overridden to null by setValue)
+ */
+ dateString = newDateString;
+ }
+
+ /*
+ * Saves the localized message of parse error. This can be
+ * overridden in handleUnparsableDateString. The message
+ * will later be used to show a validation error.
+ */
+ currentParseErrorMessage = e.getLocalizedMessage();
+
+ /*
+ * The value of the DateField should be null if an invalid
+ * value has been given. Not using setValue() since we do
+ * not want to cause the client side value to change.
+ */
+ uiHasValidDateString = false;
+
+ /*
+ * Because of our custom implementation of isValid(), that
+ * also checks the parsingSucceeded flag, we must also
+ * notify the form (if this is used in one) that the
+ * validity of this field has changed.
+ *
+ * Normally fields validity doesn't change without value
+ * change and form depends on this implementation detail.
+ */
+ notifyFormOfValidityChange();
+ requestRepaint();
+ }
+ } else if (newDate != oldDate
+ && (newDate == null || !newDate.equals(oldDate))) {
+ setValue(newDate, true); // Don't require a repaint, client
+ // updates itself
+ } else if (!uiHasValidDateString) { // oldDate ==
+ // newDate == null
+ // Empty value set, previously contained unparsable date string,
+ // clear related internal fields
+ setValue(null);
+ }
+ }
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+ }
+
+ /**
+ * This method is called to handle a non-empty date string from the client
+ * if the client could not parse it as a Date.
+ *
+ * By default, a Converter.ConversionException is thrown, and the current
+ * value is not modified.
+ *
+ * This can be overridden to handle conversions, to return null (equivalent
+ * to empty input), to throw an exception or to fire an event.
+ *
+ * @param dateString
+ * @return parsed Date
+ * @throws Converter.ConversionException
+ * to keep the old value and indicate an error
+ */
+ protected Date handleUnparsableDateString(String dateString)
+ throws Converter.ConversionException {
+ currentParseErrorMessage = null;
+ throw new Converter.ConversionException(getParseErrorMessage());
+ }
+
+ /* Property features */
+
+ /*
+ * Gets the edited property's type. Don't add a JavaDoc comment here, we use
+ * the default documentation from implemented interface.
+ */
+ @Override
+ public Class<Date> getType() {
+ return Date.class;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, boolean)
+ */
+ @Override
+ protected void setValue(Date newValue, boolean repaintIsNotNeeded)
+ throws Property.ReadOnlyException {
+
+ /*
+ * First handle special case when the client side component have a date
+ * string but value is null (e.g. unparsable date string typed in by the
+ * user). No value changes should happen, but we need to do some
+ * internal housekeeping.
+ */
+ if (newValue == null && !uiHasValidDateString) {
+ /*
+ * Side-effects of setInternalValue clears possible previous strings
+ * and flags about invalid input.
+ */
+ setInternalValue(null);
+
+ /*
+ * Due to DateField's special implementation of isValid(),
+ * datefields validity may change although the logical value does
+ * not change. This is an issue for Form which expects that validity
+ * of Fields cannot change unless actual value changes.
+ *
+ * So we check if this field is inside a form and the form has
+ * registered this as a field. In this case we repaint the form.
+ * Without this hacky solution the form might not be able to clean
+ * validation errors etc. We could avoid this by firing an extra
+ * value change event, but feels like at least as bad solution as
+ * this.
+ */
+ notifyFormOfValidityChange();
+ requestRepaint();
+ return;
+ }
+
+ super.setValue(newValue, repaintIsNotNeeded);
+ }
+
+ /**
+ * Detects if this field is used in a Form (logically) and if so, notifies
+ * it (by repainting it) that the validity of this field might have changed.
+ */
+ private void notifyFormOfValidityChange() {
+ Component parenOfDateField = getParent();
+ boolean formFound = false;
+ while (parenOfDateField != null || formFound) {
+ if (parenOfDateField instanceof Form) {
+ Form f = (Form) parenOfDateField;
+ Collection<?> visibleItemProperties = f.getItemPropertyIds();
+ for (Object fieldId : visibleItemProperties) {
+ Field<?> field = f.getField(fieldId);
+ if (field == this) {
+ /*
+ * this datefield is logically in a form. Do the same
+ * thing as form does in its value change listener that
+ * it registers to all fields.
+ */
+ f.requestRepaint();
+ formFound = true;
+ break;
+ }
+ }
+ }
+ if (formFound) {
+ break;
+ }
+ parenOfDateField = parenOfDateField.getParent();
+ }
+ }
+
+ @Override
+ protected void setInternalValue(Date newValue) {
+ // Also set the internal dateString
+ if (newValue != null) {
+ dateString = newValue.toString();
+ } else {
+ dateString = null;
+ }
+
+ if (!uiHasValidDateString) {
+ // clear component error and parsing flag
+ setComponentError(null);
+ uiHasValidDateString = true;
+ currentParseErrorMessage = null;
+ }
+
+ super.setInternalValue(newValue);
+ }
+
+ /**
+ * Gets the resolution.
+ *
+ * @return int
+ */
+ public Resolution getResolution() {
+ return resolution;
+ }
+
+ /**
+ * Sets the resolution of the DateField.
+ *
+ * The default resolution is {@link Resolution#DAY} since Vaadin 7.0.
+ *
+ * @param resolution
+ * the resolution to set.
+ */
+ public void setResolution(Resolution resolution) {
+ this.resolution = resolution;
+ requestRepaint();
+ }
+
+ /**
+ * Returns new instance calendar used in Date conversions.
+ *
+ * Returns new clone of the calendar object initialized using the the
+ * current date (if available)
+ *
+ * If this is no calendar is assigned the <code>Calendar.getInstance</code>
+ * is used.
+ *
+ * @return the Calendar.
+ * @see #setCalendar(Calendar)
+ */
+ private Calendar getCalendar() {
+
+ // Makes sure we have an calendar instance
+ if (calendar == null) {
+ calendar = Calendar.getInstance();
+ // Start by a zeroed calendar to avoid having values for lower
+ // resolution variables e.g. time when resolution is day
+ for (Resolution r : Resolution.getResolutionsLowerThan(resolution)) {
+ calendar.set(r.getCalendarField(), 0);
+ }
+ calendar.set(Calendar.MILLISECOND, 0);
+ }
+
+ // Clone the instance
+ final Calendar newCal = (Calendar) calendar.clone();
+
+ // Assigns the current time tom calendar.
+ final Date currentDate = getValue();
+ if (currentDate != null) {
+ newCal.setTime(currentDate);
+ }
+
+ final TimeZone currentTimeZone = getTimeZone();
+ if (currentTimeZone != null) {
+ newCal.setTimeZone(currentTimeZone);
+ }
+
+ return newCal;
+ }
+
+ /**
+ * Sets formatting used by some component implementations. See
+ * {@link SimpleDateFormat} for format details.
+ *
+ * By default it is encouraged to used default formatting defined by Locale,
+ * but due some JVM bugs it is sometimes necessary to use this method to
+ * override formatting. See Vaadin issue #2200.
+ *
+ * @param dateFormat
+ * the dateFormat to set
+ *
+ * @see com.vaadin.ui.AbstractComponent#setLocale(Locale))
+ */
+ public void setDateFormat(String dateFormat) {
+ this.dateFormat = dateFormat;
+ requestRepaint();
+ }
+
+ /**
+ * Returns a format string used to format date value on client side or null
+ * if default formatting from {@link Component#getLocale()} is used.
+ *
+ * @return the dateFormat
+ */
+ public String getDateFormat() {
+ return dateFormat;
+ }
+
+ /**
+ * Specifies whether or not date/time interpretation in component is to be
+ * lenient.
+ *
+ * @see Calendar#setLenient(boolean)
+ * @see #isLenient()
+ *
+ * @param lenient
+ * true if the lenient mode is to be turned on; false if it is to
+ * be turned off.
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ requestRepaint();
+ }
+
+ /**
+ * Returns whether date/time interpretation is to be lenient.
+ *
+ * @see #setLenient(boolean)
+ *
+ * @return true if the interpretation mode of this calendar is lenient;
+ * false otherwise.
+ */
+ public boolean isLenient() {
+ return lenient;
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ /**
+ * Checks whether ISO 8601 week numbers are shown in the date selector.
+ *
+ * @return true if week numbers are shown, false otherwise.
+ */
+ public boolean isShowISOWeekNumbers() {
+ return showISOWeekNumbers;
+ }
+
+ /**
+ * Sets the visibility of ISO 8601 week numbers in the date selector. ISO
+ * 8601 defines that a week always starts with a Monday so the week numbers
+ * are only shown if this is the case.
+ *
+ * @param showWeekNumbers
+ * true if week numbers should be shown, false otherwise.
+ */
+ public void setShowISOWeekNumbers(boolean showWeekNumbers) {
+ showISOWeekNumbers = showWeekNumbers;
+ requestRepaint();
+ }
+
+ /**
+ * Validates the current value against registered validators if the field is
+ * not empty. Note that DateField is considered empty (value == null) and
+ * invalid if it contains text typed in by the user that couldn't be parsed
+ * into a Date value.
+ *
+ * @see com.vaadin.ui.AbstractField#validate()
+ */
+ @Override
+ public void validate() throws InvalidValueException {
+ /*
+ * To work properly in form we must throw exception if there is
+ * currently a parsing error in the datefield. Parsing error is kind of
+ * an internal validator.
+ */
+ if (!uiHasValidDateString) {
+ throw new UnparsableDateString(currentParseErrorMessage);
+ }
+ super.validate();
+ }
+
+ /**
+ * Return the error message that is shown if the user inputted value can't
+ * be parsed into a Date object. If
+ * {@link #handleUnparsableDateString(String)} is overridden and it throws a
+ * custom exception, the message returned by
+ * {@link Exception#getLocalizedMessage()} will be used instead of the value
+ * returned by this method.
+ *
+ * @see #setParseErrorMessage(String)
+ *
+ * @return the error message that the DateField uses when it can't parse the
+ * textual input from user to a Date object
+ */
+ public String getParseErrorMessage() {
+ return defaultParseErrorMessage;
+ }
+
+ /**
+ * Sets the default error message used if the DateField cannot parse the
+ * text input by user to a Date field. Note that if the
+ * {@link #handleUnparsableDateString(String)} method is overridden, the
+ * localized message from its exception is used.
+ *
+ * @see #getParseErrorMessage()
+ * @see #handleUnparsableDateString(String)
+ * @param parsingErrorMessage
+ */
+ public void setParseErrorMessage(String parsingErrorMessage) {
+ defaultParseErrorMessage = parsingErrorMessage;
+ }
+
+ /**
+ * Sets the time zone used by this date field. The time zone is used to
+ * convert the absolute time in a Date object to a logical time displayed in
+ * the selector and to convert the select time back to a Date object.
+ *
+ * If no time zone has been set, the current default time zone returned by
+ * {@code TimeZone.getDefault()} is used.
+ *
+ * @see #getTimeZone()
+ * @param timeZone
+ * the time zone to use for time calculations.
+ */
+ public void setTimeZone(TimeZone timeZone) {
+ this.timeZone = timeZone;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the time zone used by this field. The time zone is used to convert
+ * the absolute time in a Date object to a logical time displayed in the
+ * selector and to convert the select time back to a Date object.
+ *
+ * If {@code null} is returned, the current default time zone returned by
+ * {@code TimeZone.getDefault()} is used.
+ *
+ * @return the current time zone
+ */
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ public static class UnparsableDateString extends
+ Validator.InvalidValueException {
+
+ public UnparsableDateString(String message) {
+ super(message);
+ }
+
+ }
+}
diff --git a/server/src/com/vaadin/ui/DefaultFieldFactory.java b/server/src/com/vaadin/ui/DefaultFieldFactory.java
new file mode 100644
index 0000000000..e17f08c1c6
--- /dev/null
+++ b/server/src/com/vaadin/ui/DefaultFieldFactory.java
@@ -0,0 +1,146 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.util.Date;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+/**
+ * This class contains a basic implementation for both {@link FormFieldFactory}
+ * and {@link TableFieldFactory}. The class is singleton, use {@link #get()}
+ * method to get reference to the instance.
+ *
+ * <p>
+ * There are also some static helper methods available for custom built field
+ * factories.
+ *
+ */
+public class DefaultFieldFactory implements FormFieldFactory, TableFieldFactory {
+
+ private static final DefaultFieldFactory instance = new DefaultFieldFactory();
+
+ /**
+ * Singleton method to get an instance of DefaultFieldFactory.
+ *
+ * @return an instance of DefaultFieldFactory
+ */
+ public static DefaultFieldFactory get() {
+ return instance;
+ }
+
+ protected DefaultFieldFactory() {
+ }
+
+ @Override
+ public Field<?> createField(Item item, Object propertyId,
+ Component uiContext) {
+ Class<?> type = item.getItemProperty(propertyId).getType();
+ Field<?> field = createFieldByPropertyType(type);
+ field.setCaption(createCaptionByPropertyId(propertyId));
+ return field;
+ }
+
+ @Override
+ public Field<?> createField(Container container, Object itemId,
+ Object propertyId, Component uiContext) {
+ Property<?> containerProperty = container.getContainerProperty(itemId,
+ propertyId);
+ Class<?> type = containerProperty.getType();
+ Field<?> field = createFieldByPropertyType(type);
+ field.setCaption(createCaptionByPropertyId(propertyId));
+ return field;
+ }
+
+ /**
+ * If name follows method naming conventions, convert the name to spaced
+ * upper case text. For example, convert "firstName" to "First Name"
+ *
+ * @param propertyId
+ * @return the formatted caption string
+ */
+ public static String createCaptionByPropertyId(Object propertyId) {
+ String name = propertyId.toString();
+ if (name.length() > 0) {
+
+ int dotLocation = name.lastIndexOf('.');
+ if (dotLocation > 0 && dotLocation < name.length() - 1) {
+ name = name.substring(dotLocation + 1);
+ }
+ if (name.indexOf(' ') < 0
+ && name.charAt(0) == Character.toLowerCase(name.charAt(0))
+ && name.charAt(0) != Character.toUpperCase(name.charAt(0))) {
+ StringBuffer out = new StringBuffer();
+ out.append(Character.toUpperCase(name.charAt(0)));
+ int i = 1;
+
+ while (i < name.length()) {
+ int j = i;
+ for (; j < name.length(); j++) {
+ char c = name.charAt(j);
+ if (Character.toLowerCase(c) != c
+ && Character.toUpperCase(c) == c) {
+ break;
+ }
+ }
+ if (j == name.length()) {
+ out.append(name.substring(i));
+ } else {
+ out.append(name.substring(i, j));
+ out.append(" " + name.charAt(j));
+ }
+ i = j + 1;
+ }
+
+ name = out.toString();
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Creates fields based on the property type.
+ * <p>
+ * The default field type is {@link TextField}. Other field types generated
+ * by this method:
+ * <p>
+ * <b>Boolean</b>: {@link CheckBox}.<br/>
+ * <b>Date</b>: {@link DateField}(resolution: day).<br/>
+ * <b>Item</b>: {@link Form}. <br/>
+ * <b>default field type</b>: {@link TextField}.
+ * <p>
+ *
+ * @param type
+ * the type of the property
+ * @return the most suitable generic {@link Field} for given type
+ */
+ public static Field<?> createFieldByPropertyType(Class<?> type) {
+ // Null typed properties can not be edited
+ if (type == null) {
+ return null;
+ }
+
+ // Item field
+ if (Item.class.isAssignableFrom(type)) {
+ return new Form();
+ }
+
+ // Date field
+ if (Date.class.isAssignableFrom(type)) {
+ final DateField df = new DateField();
+ df.setResolution(DateField.RESOLUTION_DAY);
+ return df;
+ }
+
+ // Boolean field
+ if (Boolean.class.isAssignableFrom(type)) {
+ return new CheckBox();
+ }
+
+ return new TextField();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/DragAndDropWrapper.java b/server/src/com/vaadin/ui/DragAndDropWrapper.java
new file mode 100644
index 0000000000..67229a45fe
--- /dev/null
+++ b/server/src/com/vaadin/ui/DragAndDropWrapper.java
@@ -0,0 +1,407 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.vaadin.event.Transferable;
+import com.vaadin.event.TransferableImpl;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.HorizontalDropLocation;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.StreamVariable;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper;
+
+@SuppressWarnings("serial")
+public class DragAndDropWrapper extends CustomComponent implements DropTarget,
+ DragSource, Vaadin6Component {
+
+ public class WrapperTransferable extends TransferableImpl {
+
+ private Html5File[] files;
+
+ public WrapperTransferable(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ super(sourceComponent, rawVariables);
+ Integer fc = (Integer) rawVariables.get("filecount");
+ if (fc != null) {
+ files = new Html5File[fc];
+ for (int i = 0; i < fc; i++) {
+ Html5File file = new Html5File(
+ (String) rawVariables.get("fn" + i), // name
+ (Integer) rawVariables.get("fs" + i), // size
+ (String) rawVariables.get("ft" + i)); // mime
+ String id = (String) rawVariables.get("fi" + i);
+ files[i] = file;
+ receivers.put(id, file);
+ requestRepaint(); // paint Receivers
+ }
+ }
+ }
+
+ /**
+ * The component in wrapper that is being dragged or null if the
+ * transferable is not a component (most likely an html5 drag).
+ *
+ * @return
+ */
+ public Component getDraggedComponent() {
+ Component object = (Component) getData("component");
+ return object;
+ }
+
+ /**
+ * @return the mouse down event that started the drag and drop operation
+ */
+ public MouseEventDetails getMouseDownEvent() {
+ return MouseEventDetails.deSerialize((String) getData("mouseDown"));
+ }
+
+ public Html5File[] getFiles() {
+ return files;
+ }
+
+ public String getText() {
+ String data = (String) getData("Text"); // IE, html5
+ if (data == null) {
+ // check for "text/plain" (webkit)
+ data = (String) getData("text/plain");
+ }
+ return data;
+ }
+
+ public String getHtml() {
+ String data = (String) getData("Html"); // IE, html5
+ if (data == null) {
+ // check for "text/plain" (webkit)
+ data = (String) getData("text/html");
+ }
+ return data;
+ }
+
+ }
+
+ private Map<String, Html5File> receivers = new HashMap<String, Html5File>();
+
+ public class WrapperTargetDetails extends TargetDetailsImpl {
+
+ public WrapperTargetDetails(Map<String, Object> rawDropData) {
+ super(rawDropData, DragAndDropWrapper.this);
+ }
+
+ /**
+ * @return the absolute position of wrapper on the page
+ */
+ public Integer getAbsoluteLeft() {
+ return (Integer) getData("absoluteLeft");
+ }
+
+ /**
+ *
+ * @return the absolute position of wrapper on the page
+ */
+ public Integer getAbsoluteTop() {
+ return (Integer) getData("absoluteTop");
+ }
+
+ /**
+ * @return details about the actual event that caused the event details.
+ * Practically mouse move or mouse up.
+ */
+ public MouseEventDetails getMouseEvent() {
+ return MouseEventDetails
+ .deSerialize((String) getData("mouseEvent"));
+ }
+
+ /**
+ * @return a detail about the drags vertical position over the wrapper.
+ */
+ public VerticalDropLocation getVerticalDropLocation() {
+ return VerticalDropLocation
+ .valueOf((String) getData("verticalLocation"));
+ }
+
+ /**
+ * @return a detail about the drags horizontal position over the
+ * wrapper.
+ */
+ public HorizontalDropLocation getHorizontalDropLocation() {
+ return HorizontalDropLocation
+ .valueOf((String) getData("horizontalLocation"));
+ }
+
+ /**
+ * @deprecated use {@link #getVerticalDropLocation()} instead
+ */
+ @Deprecated
+ public VerticalDropLocation verticalDropLocation() {
+ return getVerticalDropLocation();
+ }
+
+ /**
+ * @deprecated use {@link #getHorizontalDropLocation()} instead
+ */
+ @Deprecated
+ public HorizontalDropLocation horizontalDropLocation() {
+ return getHorizontalDropLocation();
+ }
+
+ }
+
+ public enum DragStartMode {
+ /**
+ * {@link DragAndDropWrapper} does not start drag events at all
+ */
+ NONE,
+ /**
+ * The component on which the drag started will be shown as drag image.
+ */
+ COMPONENT,
+ /**
+ * The whole wrapper is used as a drag image when dragging.
+ */
+ WRAPPER,
+ /**
+ * The whole wrapper is used to start an HTML5 drag.
+ *
+ * NOTE: In Internet Explorer 6 to 8, this prevents user interactions
+ * with the wrapper's contents. For example, clicking a button inside
+ * the wrapper will no longer work.
+ */
+ HTML5,
+ }
+
+ private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>();
+ private DragStartMode dragStartMode = DragStartMode.NONE;
+
+ /**
+ * Wraps given component in a {@link DragAndDropWrapper}.
+ *
+ * @param root
+ * the component to be wrapped
+ */
+ public DragAndDropWrapper(Component root) {
+ super(root);
+ }
+
+ /**
+ * Sets data flavors available in the DragAndDropWrapper is used to start an
+ * HTML5 style drags. Most commonly the "Text" flavor should be set.
+ * Multiple data types can be set.
+ *
+ * @param type
+ * the string identifier of the drag "payload". E.g. "Text" or
+ * "text/html"
+ * @param value
+ * the value
+ */
+ public void setHTML5DataFlavor(String type, Object value) {
+ html5DataFlavors.put(type, value);
+ requestRepaint();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once Vaadin6Component is no longer implemented
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute(VDragAndDropWrapper.DRAG_START_MODE,
+ dragStartMode.ordinal());
+ if (getDropHandler() != null) {
+ getDropHandler().getAcceptCriterion().paint(target);
+ }
+ if (receivers != null && receivers.size() > 0) {
+ for (Iterator<Entry<String, Html5File>> it = receivers.entrySet()
+ .iterator(); it.hasNext();) {
+ Entry<String, com.vaadin.ui.Html5File> entry = it.next();
+ String id = entry.getKey();
+ Html5File html5File = entry.getValue();
+ if (html5File.getStreamVariable() != null) {
+ target.addVariable(this, "rec-" + id, new ProxyReceiver(
+ html5File));
+ // these are cleaned from receivers once the upload has
+ // started
+ } else {
+ // instructs the client side not to send the file
+ target.addVariable(this, "rec-" + id, (String) null);
+ // forget the file from subsequent paints
+ it.remove();
+ }
+ }
+ }
+ target.addAttribute(VDragAndDropWrapper.HTML5_DATA_FLAVORS,
+ html5DataFlavors);
+ }
+
+ private DropHandler dropHandler;
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ requestRepaint();
+ }
+
+ @Override
+ public TargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new WrapperTargetDetails(clientVariables);
+ }
+
+ @Override
+ public Transferable getTransferable(final Map<String, Object> rawVariables) {
+ return new WrapperTransferable(this, rawVariables);
+ }
+
+ public void setDragStartMode(DragStartMode dragStartMode) {
+ this.dragStartMode = dragStartMode;
+ requestRepaint();
+ }
+
+ public DragStartMode getDragStartMode() {
+ return dragStartMode;
+ }
+
+ final class ProxyReceiver implements StreamVariable {
+
+ private Html5File file;
+
+ public ProxyReceiver(Html5File file) {
+ this.file = file;
+ }
+
+ private boolean listenProgressOfUploadedFile;
+
+ @Override
+ public OutputStream getOutputStream() {
+ if (file.getStreamVariable() == null) {
+ return null;
+ }
+ return file.getStreamVariable().getOutputStream();
+ }
+
+ @Override
+ public boolean listenProgress() {
+ return file.getStreamVariable().listenProgress();
+ }
+
+ @Override
+ public void onProgress(StreamingProgressEvent event) {
+ file.getStreamVariable().onProgress(
+ new ReceivingEventWrapper(event));
+ }
+
+ @Override
+ public void streamingStarted(StreamingStartEvent event) {
+ listenProgressOfUploadedFile = file.getStreamVariable() != null;
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingStarted(
+ new ReceivingEventWrapper(event));
+ }
+ // no need tell to the client about this receiver on next paint
+ receivers.remove(file);
+ // let the terminal GC the streamvariable and not to accept other
+ // file uploads to this variable
+ event.disposeStreamVariable();
+ }
+
+ @Override
+ public void streamingFinished(StreamingEndEvent event) {
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingFinished(
+ new ReceivingEventWrapper(event));
+ }
+ }
+
+ @Override
+ public void streamingFailed(final StreamingErrorEvent event) {
+ if (listenProgressOfUploadedFile) {
+ file.getStreamVariable().streamingFailed(
+ new ReceivingEventWrapper(event));
+ }
+ }
+
+ @Override
+ public boolean isInterrupted() {
+ return file.getStreamVariable().isInterrupted();
+ }
+
+ /*
+ * With XHR2 file posts we can't provide as much information from the
+ * terminal as with multipart request. This helper class wraps the
+ * terminal event and provides the lacking information from the
+ * Html5File.
+ */
+ class ReceivingEventWrapper implements StreamingErrorEvent,
+ StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent {
+
+ private StreamingEvent wrappedEvent;
+
+ ReceivingEventWrapper(StreamingEvent e) {
+ wrappedEvent = e;
+ }
+
+ @Override
+ public String getMimeType() {
+ return file.getType();
+ }
+
+ @Override
+ public String getFileName() {
+ return file.getFileName();
+ }
+
+ @Override
+ public long getContentLength() {
+ return file.getFileSize();
+ }
+
+ public StreamVariable getReceiver() {
+ return ProxyReceiver.this;
+ }
+
+ @Override
+ public Exception getException() {
+ if (wrappedEvent instanceof StreamingErrorEvent) {
+ return ((StreamingErrorEvent) wrappedEvent).getException();
+ }
+ return null;
+ }
+
+ @Override
+ public long getBytesReceived() {
+ return wrappedEvent.getBytesReceived();
+ }
+
+ /**
+ * Calling this method has no effect. DD files are receive only once
+ * anyway.
+ */
+ @Override
+ public void disposeStreamVariable() {
+
+ }
+ }
+
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Embedded.java b/server/src/com/vaadin/ui/Embedded.java
new file mode 100644
index 0000000000..6088c5aa66
--- /dev/null
+++ b/server/src/com/vaadin/ui/Embedded.java
@@ -0,0 +1,531 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.MouseEvents.ClickListener;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.embedded.EmbeddedServerRpc;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.terminal.gwt.client.ui.embedded.EmbeddedConnector;
+
+/**
+ * Component for embedding external objects.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Embedded extends AbstractComponent implements Vaadin6Component {
+
+ /**
+ * General object type.
+ */
+ public static final int TYPE_OBJECT = 0;
+
+ /**
+ * Image types.
+ */
+ public static final int TYPE_IMAGE = 1;
+
+ /**
+ * Browser ("iframe") type.
+ */
+ public static final int TYPE_BROWSER = 2;
+
+ /**
+ * Type of the object.
+ */
+ private int type = TYPE_OBJECT;
+
+ /**
+ * Source of the embedded object.
+ */
+ private Resource source = null;
+
+ /**
+ * Generic object attributes.
+ */
+ private String mimeType = null;
+
+ private String standby = null;
+
+ /**
+ * Hash of object parameters.
+ */
+ private final Map<String, String> parameters = new HashMap<String, String>();
+
+ /**
+ * Applet or other client side runnable properties.
+ */
+ private String codebase = null;
+
+ private String codetype = null;
+
+ private String classId = null;
+
+ private String archive = null;
+
+ private String altText;
+
+ private EmbeddedServerRpc rpc = new EmbeddedServerRpc() {
+ @Override
+ public void click(MouseEventDetails mouseDetails) {
+ fireEvent(new ClickEvent(Embedded.this, mouseDetails));
+ }
+ };
+
+ /**
+ * Creates a new empty Embedded object.
+ */
+ public Embedded() {
+ registerRpc(rpc);
+ }
+
+ /**
+ * Creates a new empty Embedded object with caption.
+ *
+ * @param caption
+ */
+ public Embedded(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new Embedded object whose contents is loaded from given
+ * resource. The dimensions are assumed if possible. The type is guessed
+ * from resource.
+ *
+ * @param caption
+ * @param source
+ * the Source of the embedded object.
+ */
+ public Embedded(String caption, Resource source) {
+ this(caption);
+ setSource(source);
+ }
+
+ /**
+ * Invoked when the component state should be painted.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ switch (type) {
+ case TYPE_IMAGE:
+ target.addAttribute("type", "image");
+ break;
+ case TYPE_BROWSER:
+ target.addAttribute("type", "browser");
+ break;
+ default:
+ break;
+ }
+
+ if (getSource() != null) {
+ target.addAttribute("src", getSource());
+ }
+
+ if (mimeType != null && !"".equals(mimeType)) {
+ target.addAttribute("mimetype", mimeType);
+ }
+ if (classId != null && !"".equals(classId)) {
+ target.addAttribute("classid", classId);
+ }
+ if (codebase != null && !"".equals(codebase)) {
+ target.addAttribute("codebase", codebase);
+ }
+ if (codetype != null && !"".equals(codetype)) {
+ target.addAttribute("codetype", codetype);
+ }
+ if (standby != null && !"".equals(standby)) {
+ target.addAttribute("standby", standby);
+ }
+ if (archive != null && !"".equals(archive)) {
+ target.addAttribute("archive", archive);
+ }
+ if (altText != null && !"".equals(altText)) {
+ target.addAttribute(EmbeddedConnector.ALTERNATE_TEXT, altText);
+ }
+
+ // Params
+ for (final Iterator<String> i = getParameterNames(); i.hasNext();) {
+ target.startTag("embeddedparam");
+ final String key = i.next();
+ target.addAttribute("name", key);
+ target.addAttribute("value", getParameter(key));
+ target.endTag("embeddedparam");
+ }
+ }
+
+ /**
+ * Sets this component's "alt-text", that is, an alternate text that can be
+ * presented instead of this component's normal content, for accessibility
+ * purposes. Does not work when {@link #setType(int)} has been called with
+ * {@link #TYPE_BROWSER}.
+ *
+ * @param altText
+ * A short, human-readable description of this component's
+ * content.
+ * @since 6.8
+ */
+ public void setAlternateText(String altText) {
+ if (altText != this.altText
+ || (altText != null && !altText.equals(this.altText))) {
+ this.altText = altText;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets this component's "alt-text".
+ *
+ * @see #setAlternateText(String)
+ */
+ public String getAlternateText() {
+ return altText;
+ }
+
+ /**
+ * Sets an object parameter. Parameters are optional information, and they
+ * are passed to the instantiated object. Parameters are are stored as name
+ * value pairs. This overrides the previous value assigned to this
+ * parameter.
+ *
+ * @param name
+ * the name of the parameter.
+ * @param value
+ * the value of the parameter.
+ */
+ public void setParameter(String name, String value) {
+ parameters.put(name, value);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the value of an object parameter. Parameters are optional
+ * information, and they are passed to the instantiated object. Parameters
+ * are are stored as name value pairs.
+ *
+ * @return the Value of parameter or null if not found.
+ */
+ public String getParameter(String name) {
+ return parameters.get(name);
+ }
+
+ /**
+ * Removes an object parameter from the list.
+ *
+ * @param name
+ * the name of the parameter to remove.
+ */
+ public void removeParameter(String name) {
+ parameters.remove(name);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the embedded object parameter names.
+ *
+ * @return the Iterator of parameters names.
+ */
+ public Iterator<String> getParameterNames() {
+ return parameters.keySet().iterator();
+ }
+
+ /**
+ * This attribute specifies the base path used to resolve relative URIs
+ * specified by the classid, data, and archive attributes. When absent, its
+ * default value is the base URI of the current document.
+ *
+ * @return the code base.
+ */
+ public String getCodebase() {
+ return codebase;
+ }
+
+ /**
+ * Gets the MIME-Type of the code.
+ *
+ * @return the MIME-Type of the code.
+ */
+ public String getCodetype() {
+ return codetype;
+ }
+
+ /**
+ * Gets the MIME-Type of the object.
+ *
+ * @return the MIME-Type of the object.
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * This attribute specifies a message that a user agent may render while
+ * loading the object's implementation and data.
+ *
+ * @return The text displayed when loading
+ */
+ public String getStandby() {
+ return standby;
+ }
+
+ /**
+ * This attribute specifies the base path used to resolve relative URIs
+ * specified by the classid, data, and archive attributes. When absent, its
+ * default value is the base URI of the current document.
+ *
+ * @param codebase
+ * The base path
+ */
+ public void setCodebase(String codebase) {
+ if (codebase != this.codebase
+ || (codebase != null && !codebase.equals(this.codebase))) {
+ this.codebase = codebase;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * This attribute specifies the content type of data expected when
+ * downloading the object specified by classid. This attribute is optional
+ * but recommended when classid is specified since it allows the user agent
+ * to avoid loading information for unsupported content types. When absent,
+ * it defaults to the value of the type attribute.
+ *
+ * @param codetype
+ * the codetype to set.
+ */
+ public void setCodetype(String codetype) {
+ if (codetype != this.codetype
+ || (codetype != null && !codetype.equals(this.codetype))) {
+ this.codetype = codetype;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Sets the mimeType, the MIME-Type of the object.
+ *
+ * @param mimeType
+ * the mimeType to set.
+ */
+ public void setMimeType(String mimeType) {
+ if (mimeType != this.mimeType
+ || (mimeType != null && !mimeType.equals(this.mimeType))) {
+ this.mimeType = mimeType;
+ if ("application/x-shockwave-flash".equals(mimeType)) {
+ /*
+ * Automatically add wmode transparent as we use lots of
+ * floating layers in Vaadin. If developers need better flash
+ * performance, they can override this value programmatically
+ * back to "window" (the defautl).
+ */
+ if (getParameter("wmode") == null) {
+ setParameter("wmode", "transparent");
+ }
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * This attribute specifies a message that a user agent may render while
+ * loading the object's implementation and data.
+ *
+ * @param standby
+ * The text to display while loading
+ */
+ public void setStandby(String standby) {
+ if (standby != this.standby
+ || (standby != null && !standby.equals(this.standby))) {
+ this.standby = standby;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * This attribute may be used to specify the location of an object's
+ * implementation via a URI.
+ *
+ * @return the classid.
+ */
+ public String getClassId() {
+ return classId;
+ }
+
+ /**
+ * This attribute may be used to specify the location of an object's
+ * implementation via a URI.
+ *
+ * @param classId
+ * the classId to set.
+ */
+ public void setClassId(String classId) {
+ if (classId != this.classId
+ || (classId != null && !classId.equals(this.classId))) {
+ this.classId = classId;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the resource contained in the embedded object.
+ *
+ * @return the Resource
+ */
+ public Resource getSource() {
+ return source;
+ }
+
+ /**
+ * Gets the type of the embedded object.
+ * <p>
+ * This can be one of the following:
+ * <ul>
+ * <li>TYPE_OBJECT <i>(This is the default)</i>
+ * <li>TYPE_IMAGE
+ * </ul>
+ * </p>
+ *
+ * @return the type.
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Sets the object source resource. The dimensions are assumed if possible.
+ * The type is guessed from resource.
+ *
+ * @param source
+ * the source to set.
+ */
+ public void setSource(Resource source) {
+ if (source != null && !source.equals(this.source)) {
+ this.source = source;
+ final String mt = source.getMIMEType();
+
+ if (mimeType == null) {
+ mimeType = mt;
+ }
+
+ if (mt.equals("image/svg+xml")) {
+ type = TYPE_OBJECT;
+ } else if ((mt.substring(0, mt.indexOf("/"))
+ .equalsIgnoreCase("image"))) {
+ type = TYPE_IMAGE;
+ } else {
+ // Keep previous type
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Sets the object type.
+ * <p>
+ * This can be one of the following:
+ * <ul>
+ * <li>TYPE_OBJECT <i>(This is the default)</i>
+ * <li>TYPE_IMAGE
+ * <li>TYPE_BROWSER
+ * </ul>
+ * </p>
+ *
+ * @param type
+ * the type to set.
+ */
+ public void setType(int type) {
+ if (type != TYPE_OBJECT && type != TYPE_IMAGE && type != TYPE_BROWSER) {
+ throw new IllegalArgumentException("Unsupported type");
+ }
+ if (type != this.type) {
+ this.type = type;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * This attribute may be used to specify a space-separated list of URIs for
+ * archives containing resources relevant to the object, which may include
+ * the resources specified by the classid and data attributes. Preloading
+ * archives will generally result in reduced load times for objects.
+ * Archives specified as relative URIs should be interpreted relative to the
+ * codebase attribute.
+ *
+ * @return Space-separated list of URIs with resources relevant to the
+ * object
+ */
+ public String getArchive() {
+ return archive;
+ }
+
+ /**
+ * This attribute may be used to specify a space-separated list of URIs for
+ * archives containing resources relevant to the object, which may include
+ * the resources specified by the classid and data attributes. Preloading
+ * archives will generally result in reduced load times for objects.
+ * Archives specified as relative URIs should be interpreted relative to the
+ * codebase attribute.
+ *
+ * @param archive
+ * Space-separated list of URIs with resources relevant to the
+ * object
+ */
+ public void setArchive(String archive) {
+ if (archive != this.archive
+ || (archive != null && !archive.equals(this.archive))) {
+ this.archive = archive;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Add a click listener to the component. The listener is called whenever
+ * the user clicks inside the component. Depending on the content the event
+ * may be blocked and in that case no event is fired.
+ *
+ * Use {@link #removeListener(ClickListener)} to remove the listener.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addListener(ClickListener listener) {
+ addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class,
+ listener, ClickListener.clickMethod);
+ }
+
+ /**
+ * Remove a click listener from the component. The listener should earlier
+ * have been added using {@link #addListener(ClickListener)}.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeListener(ClickListener listener) {
+ removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER,
+ ClickEvent.class, listener);
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once Vaadin6Component is no longer implemented
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Field.java b/server/src/com/vaadin/ui/Field.java
new file mode 100644
index 0000000000..6dc40d192f
--- /dev/null
+++ b/server/src/com/vaadin/ui/Field.java
@@ -0,0 +1,97 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.data.BufferedValidatable;
+import com.vaadin.data.Property;
+import com.vaadin.ui.Component.Focusable;
+
+/**
+ * TODO document
+ *
+ * @author Vaadin Ltd.
+ *
+ * @param T
+ * the type of values in the field, which might not be the same type
+ * as that of the data source if converters are used
+ *
+ * @author IT Mill Ltd.
+ */
+public interface Field<T> extends Component, BufferedValidatable, Property<T>,
+ Property.ValueChangeNotifier, Property.ValueChangeListener,
+ Property.Editor, Focusable {
+
+ /**
+ * Is this field required.
+ *
+ * Required fields must filled by the user.
+ *
+ * @return <code>true</code> if the field is required,otherwise
+ * <code>false</code>.
+ * @since 3.1
+ */
+ public boolean isRequired();
+
+ /**
+ * Sets the field required. Required fields must filled by the user.
+ *
+ * @param required
+ * Is the field required.
+ * @since 3.1
+ */
+ public void setRequired(boolean required);
+
+ /**
+ * Sets the error message to be displayed if a required field is empty.
+ *
+ * @param requiredMessage
+ * Error message.
+ * @since 5.2.6
+ */
+ public void setRequiredError(String requiredMessage);
+
+ /**
+ * Gets the error message that is to be displayed if a required field is
+ * empty.
+ *
+ * @return Error message.
+ * @since 5.2.6
+ */
+ public String getRequiredError();
+
+ /**
+ * An <code>Event</code> object specifying the Field whose value has been
+ * changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ @SuppressWarnings("serial")
+ public static class ValueChangeEvent extends Component.Event implements
+ Property.ValueChangeEvent {
+
+ /**
+ * Constructs a new event object with the specified source field object.
+ *
+ * @param source
+ * the field that caused the event.
+ */
+ public ValueChangeEvent(Field source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property which triggered the event.
+ *
+ * @return the Source Property of the event.
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/Form.java b/server/src/com/vaadin/ui/Form.java
new file mode 100644
index 0000000000..fbc4d5a8e6
--- /dev/null
+++ b/server/src/com/vaadin/ui/Form.java
@@ -0,0 +1,1420 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+
+import com.vaadin.data.Buffered;
+import com.vaadin.data.Item;
+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.fieldgroup.FieldGroup;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.Action.ShortcutNotifier;
+import com.vaadin.event.ActionManager;
+import com.vaadin.shared.ui.form.FormState;
+import com.vaadin.terminal.AbstractErrorMessage;
+import com.vaadin.terminal.CompositeErrorMessage;
+import com.vaadin.terminal.ErrorMessage;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.UserError;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ * Form component provides easy way of creating and managing sets fields.
+ *
+ * <p>
+ * <code>Form</code> is a container for fields implementing {@link Field}
+ * interface. It provides support for any layouts and provides buffering
+ * interface for easy connection of commit and discard buttons. All the form
+ * fields can be customized by adding validators, setting captions and icons,
+ * setting immediateness, etc. Also direct mechanism for replacing existing
+ * fields with selections is given.
+ * </p>
+ *
+ * <p>
+ * <code>Form</code> provides customizable editor for classes implementing
+ * {@link com.vaadin.data.Item} interface. Also the form itself implements this
+ * interface for easier connectivity to other items. To use the form as editor
+ * for an item, just connect the item to form with
+ * {@link Form#setItemDataSource(Item)}. If only a part of the item needs to be
+ * edited, {@link Form#setItemDataSource(Item,Collection)} can be used instead.
+ * After the item has been connected to the form, the automatically created
+ * fields can be customized and new fields can be added. If you need to connect
+ * a class that does not implement {@link com.vaadin.data.Item} interface, most
+ * properties of any class following bean pattern, can be accessed trough
+ * {@link com.vaadin.data.util.BeanItem}.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ * @deprecated Use {@link FieldGroup} instead of {@link Form} for more
+ * flexibility.
+ */
+@Deprecated
+public class Form extends AbstractField<Object> implements Item.Editor,
+ Buffered, Item, Validatable, Action.Notifier, HasComponents,
+ Vaadin6Component {
+
+ private Object propertyValue;
+
+ /**
+ * Item connected to this form as datasource.
+ */
+ private Item itemDatasource;
+
+ /**
+ * Ordered list of property ids in this editor.
+ */
+ private final LinkedList<Object> propertyIds = new LinkedList<Object>();
+
+ /**
+ * Current buffered source exception.
+ */
+ private Buffered.SourceException currentBufferedSourceException = null;
+
+ /**
+ * Is the form in write trough mode.
+ */
+ private boolean writeThrough = true;
+
+ /**
+ * Is the form in read trough mode.
+ */
+ private boolean readThrough = true;
+
+ /**
+ * Mapping from propertyName to corresponding field.
+ */
+ private final HashMap<Object, Field<?>> fields = new HashMap<Object, Field<?>>();
+
+ /**
+ * Form may act as an Item, its own properties are stored here.
+ */
+ private final HashMap<Object, Property<?>> ownProperties = new HashMap<Object, Property<?>>();
+
+ /**
+ * Field factory for this form.
+ */
+ private FormFieldFactory fieldFactory;
+
+ /**
+ * Visible item properties.
+ */
+ private Collection<?> visibleItemProperties;
+
+ /**
+ * Form needs to repaint itself if child fields value changes due possible
+ * change in form validity.
+ *
+ * TODO introduce ValidityChangeEvent (#6239) and start using it instead.
+ * See e.g. DateField#notifyFormOfValidityChange().
+ */
+ private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() {
+ @Override
+ public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
+ requestRepaint();
+ }
+ };
+
+ /**
+ * If this is true, commit implicitly calls setValidationVisible(true).
+ */
+ private boolean validationVisibleOnCommit = true;
+
+ // special handling for gridlayout; remember initial cursor pos
+ private int gridlayoutCursorX = -1;
+ private int gridlayoutCursorY = -1;
+
+ /**
+ * Keeps track of the Actions added to this component, and manages the
+ * painting and handling as well. Note that the extended AbstractField is a
+ * {@link ShortcutNotifier} and has a actionManager that delegates actions
+ * to the containing window. This one does not delegate.
+ */
+ private ActionManager ownActionManager = new ActionManager(this);
+
+ /**
+ * Constructs a new form with default layout.
+ *
+ * <p>
+ * By default the form uses {@link FormLayout}.
+ * </p>
+ */
+ public Form() {
+ this(null);
+ setValidationVisible(false);
+ }
+
+ /**
+ * Constructs a new form with given {@link Layout}.
+ *
+ * @param formLayout
+ * the layout of the form.
+ */
+ public Form(Layout formLayout) {
+ this(formLayout, DefaultFieldFactory.get());
+ }
+
+ /**
+ * Constructs a new form with given {@link Layout} and
+ * {@link FormFieldFactory}.
+ *
+ * @param formLayout
+ * the layout of the form.
+ * @param fieldFactory
+ * the FieldFactory of the form.
+ */
+ public Form(Layout formLayout, FormFieldFactory fieldFactory) {
+ super();
+ setLayout(formLayout);
+ setFooter(null);
+ setFormFieldFactory(fieldFactory);
+ setValidationVisible(false);
+ setWidth(100, UNITS_PERCENTAGE);
+ }
+
+ @Override
+ public FormState getState() {
+ return (FormState) super.getState();
+ }
+
+ /* Documented in interface */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (ownActionManager != null) {
+ ownActionManager.paintActions(null, target);
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Actions
+ if (ownActionManager != null) {
+ ownActionManager.handleActions(variables, this);
+ }
+ }
+
+ /**
+ * The error message of a Form is the error of the first field with a
+ * non-empty error.
+ *
+ * Empty error messages of the contained fields are skipped, because an
+ * empty error indicator would be confusing to the user, especially if there
+ * are errors that have something to display. This is also the reason why
+ * the calculation of the error message is separate from validation, because
+ * validation fails also on empty errors.
+ */
+ @Override
+ public ErrorMessage getErrorMessage() {
+
+ // Reimplement the checking of validation error by using
+ // getErrorMessage() recursively instead of validate().
+ ErrorMessage validationError = null;
+ if (isValidationVisible()) {
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ Object f = fields.get(i.next());
+ if (f instanceof AbstractComponent) {
+ AbstractComponent field = (AbstractComponent) f;
+
+ validationError = field.getErrorMessage();
+ if (validationError != null) {
+ // Show caption as error for fields with empty errors
+ if ("".equals(validationError.toString())) {
+ validationError = new UserError(field.getCaption());
+ }
+ break;
+ } else if (f instanceof Field && !((Field<?>) f).isValid()) {
+ // Something is wrong with the field, but no proper
+ // error is given. Generate one.
+ validationError = new UserError(field.getCaption());
+ break;
+ }
+ }
+ }
+ }
+
+ // Return if there are no errors at all
+ if (getComponentError() == null && validationError == null
+ && currentBufferedSourceException == null) {
+ return null;
+ }
+
+ // Throw combination of the error types
+ return new CompositeErrorMessage(
+ new ErrorMessage[] {
+ getComponentError(),
+ validationError,
+ AbstractErrorMessage
+ .getErrorMessageForException(currentBufferedSourceException) });
+ }
+
+ /**
+ * Controls the making validation visible implicitly on commit.
+ *
+ * Having commit() call setValidationVisible(true) implicitly is the default
+ * behaviour. You can disable the implicit setting by setting this property
+ * as false.
+ *
+ * It is useful, because you usually want to start with the form free of
+ * errors and only display them after the user clicks Ok. You can disable
+ * the implicit setting by setting this property as false.
+ *
+ * @param makeVisible
+ * If true (default), validation is made visible when commit() is
+ * called. If false, the visibility is left as it is.
+ */
+ public void setValidationVisibleOnCommit(boolean makeVisible) {
+ validationVisibleOnCommit = makeVisible;
+ }
+
+ /**
+ * Is validation made automatically visible on commit?
+ *
+ * See setValidationVisibleOnCommit().
+ *
+ * @return true if validation is made automatically visible on commit.
+ */
+ public boolean isValidationVisibleOnCommit() {
+ return validationVisibleOnCommit;
+ }
+
+ /*
+ * Commit changes to the data source Don't add a JavaDoc comment here, we
+ * use the default one from the interface.
+ */
+ @Override
+ public void commit() throws Buffered.SourceException, InvalidValueException {
+
+ LinkedList<SourceException> problems = null;
+
+ // Only commit on valid state if so requested
+ if (!isInvalidCommitted() && !isValid()) {
+ /*
+ * The values are not ok and we are told not to commit invalid
+ * values
+ */
+ if (validationVisibleOnCommit) {
+ setValidationVisible(true);
+ }
+
+ // Find the first invalid value and throw the exception
+ validate();
+ }
+
+ // Try to commit all
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ try {
+ final Field<?> f = (fields.get(i.next()));
+ // Commit only non-readonly fields.
+ if (!f.isReadOnly()) {
+ f.commit();
+ }
+ } catch (final Buffered.SourceException e) {
+ if (problems == null) {
+ problems = new LinkedList<SourceException>();
+ }
+ problems.add(e);
+ }
+ }
+
+ // No problems occurred
+ if (problems == null) {
+ if (currentBufferedSourceException != null) {
+ currentBufferedSourceException = null;
+ requestRepaint();
+ }
+ return;
+ }
+
+ // Commit problems
+ final Throwable[] causes = new Throwable[problems.size()];
+ int index = 0;
+ for (final Iterator<SourceException> i = problems.iterator(); i
+ .hasNext();) {
+ causes[index++] = i.next();
+ }
+ final Buffered.SourceException e = new Buffered.SourceException(this,
+ causes);
+ currentBufferedSourceException = e;
+ requestRepaint();
+ throw e;
+ }
+
+ /*
+ * Discards local changes and refresh values from the data source Don't add
+ * a JavaDoc comment here, we use the default one from the interface.
+ */
+ @Override
+ public void discard() throws Buffered.SourceException {
+
+ LinkedList<SourceException> problems = null;
+
+ // Try to discard all changes
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ try {
+ (fields.get(i.next())).discard();
+ } catch (final Buffered.SourceException e) {
+ if (problems == null) {
+ problems = new LinkedList<SourceException>();
+ }
+ problems.add(e);
+ }
+ }
+
+ // No problems occurred
+ if (problems == null) {
+ if (currentBufferedSourceException != null) {
+ currentBufferedSourceException = null;
+ requestRepaint();
+ }
+ return;
+ }
+
+ // Discards problems occurred
+ final Throwable[] causes = new Throwable[problems.size()];
+ int index = 0;
+ for (final Iterator<SourceException> i = problems.iterator(); i
+ .hasNext();) {
+ causes[index++] = i.next();
+ }
+ final Buffered.SourceException e = new Buffered.SourceException(this,
+ causes);
+ currentBufferedSourceException = e;
+ requestRepaint();
+ throw e;
+ }
+
+ /*
+ * Is the object modified but not committed? Don't add a JavaDoc comment
+ * here, we use the default one from the interface.
+ */
+ @Override
+ public boolean isModified() {
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ final Field<?> f = fields.get(i.next());
+ if (f != null && f.isModified()) {
+ return true;
+ }
+
+ }
+ return false;
+ }
+
+ /*
+ * Is the editor in a read-through mode? Don't add a JavaDoc comment here,
+ * we use the default one from the interface.
+ */
+ @Override
+ @Deprecated
+ public boolean isReadThrough() {
+ return readThrough;
+ }
+
+ /*
+ * Is the editor in a write-through mode? Don't add a JavaDoc comment here,
+ * we use the default one from the interface.
+ */
+ @Override
+ @Deprecated
+ public boolean isWriteThrough() {
+ return writeThrough;
+ }
+
+ /*
+ * Sets the editor's read-through mode to the specified status. Don't add a
+ * JavaDoc comment here, we use the default one from the interface.
+ */
+ @Override
+ public void setReadThrough(boolean readThrough) {
+ if (readThrough != this.readThrough) {
+ this.readThrough = readThrough;
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ (fields.get(i.next())).setReadThrough(readThrough);
+ }
+ }
+ }
+
+ /*
+ * Sets the editor's read-through mode to the specified status. Don't add a
+ * JavaDoc comment here, we use the default one from the interface.
+ */
+ @Override
+ public void setWriteThrough(boolean writeThrough) throws SourceException,
+ InvalidValueException {
+ if (writeThrough != this.writeThrough) {
+ this.writeThrough = writeThrough;
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ (fields.get(i.next())).setWriteThrough(writeThrough);
+ }
+ }
+ }
+
+ /**
+ * Adds a new property to form and create corresponding field.
+ *
+ * @see com.vaadin.data.Item#addItemProperty(Object, Property)
+ */
+ @Override
+ public boolean addItemProperty(Object id, Property property) {
+
+ // Checks inputs
+ if (id == null || property == null) {
+ throw new NullPointerException("Id and property must be non-null");
+ }
+
+ // Checks that the property id is not reserved
+ if (propertyIds.contains(id)) {
+ return false;
+ }
+
+ propertyIds.add(id);
+ ownProperties.put(id, property);
+
+ // Gets suitable field
+ final Field<?> field = fieldFactory.createField(this, id, this);
+ if (field == null) {
+ return false;
+ }
+
+ // Configures the field
+ bindPropertyToField(id, property, field);
+
+ // Register and attach the created field
+ addField(id, field);
+
+ return true;
+ }
+
+ /**
+ * Registers the field with the form and adds the field to the form layout.
+ *
+ * <p>
+ * The property id must not be already used in the form.
+ * </p>
+ *
+ * <p>
+ * This field is added to the layout using the
+ * {@link #attachField(Object, Field)} method.
+ * </p>
+ *
+ * @param propertyId
+ * the Property id the the field.
+ * @param field
+ * the field which should be added to the form.
+ */
+ public void addField(Object propertyId, Field<?> field) {
+ registerField(propertyId, field);
+ attachField(propertyId, field);
+ requestRepaint();
+ }
+
+ /**
+ * Register the field with the form. All registered fields are validated
+ * when the form is validated and also committed when the form is committed.
+ *
+ * <p>
+ * The property id must not be already used in the form.
+ * </p>
+ *
+ *
+ * @param propertyId
+ * the Property id of the field.
+ * @param field
+ * the Field that should be registered
+ */
+ private void registerField(Object propertyId, Field<?> field) {
+ if (propertyId == null || field == null) {
+ return;
+ }
+
+ fields.put(propertyId, field);
+ field.addListener(fieldValueChangeListener);
+ if (!propertyIds.contains(propertyId)) {
+ // adding a field directly
+ propertyIds.addLast(propertyId);
+ }
+
+ // Update the read and write through status and immediate to match the
+ // form.
+ // Should this also include invalidCommitted (#3993)?
+ field.setReadThrough(readThrough);
+ field.setWriteThrough(writeThrough);
+ if (isImmediate() && field instanceof AbstractComponent) {
+ ((AbstractComponent) field).setImmediate(true);
+ }
+ }
+
+ /**
+ * Adds the field to the form layout.
+ * <p>
+ * The field is added to the form layout in the default position (the
+ * position used by {@link Layout#addComponent(Component)}. If the
+ * underlying layout is a {@link CustomLayout} the field is added to the
+ * CustomLayout location given by the string representation of the property
+ * id using {@link CustomLayout#addComponent(Component, String)}.
+ * </p>
+ *
+ * <p>
+ * Override this method to control how the fields are added to the layout.
+ * </p>
+ *
+ * @param propertyId
+ * @param field
+ */
+ protected void attachField(Object propertyId, Field field) {
+ if (propertyId == null || field == null) {
+ return;
+ }
+
+ Layout layout = getLayout();
+ if (layout instanceof CustomLayout) {
+ ((CustomLayout) layout).addComponent(field, propertyId.toString());
+ } else {
+ layout.addComponent(field);
+ }
+
+ }
+
+ /**
+ * The property identified by the property id.
+ *
+ * <p>
+ * The property data source of the field specified with property id is
+ * returned. If there is a (with specified property id) having no data
+ * source, the field is returned instead of the data source.
+ * </p>
+ *
+ * @see com.vaadin.data.Item#getItemProperty(Object)
+ */
+ @Override
+ public Property<?> getItemProperty(Object id) {
+ final Field<?> field = fields.get(id);
+ if (field == null) {
+ // field does not exist or it is not (yet) created for this property
+ return ownProperties.get(id);
+ }
+ final Property<?> property = field.getPropertyDataSource();
+
+ if (property != null) {
+ return property;
+ } else {
+ return field;
+ }
+ }
+
+ /**
+ * Gets the field identified by the propertyid.
+ *
+ * @param propertyId
+ * the id of the property.
+ */
+ public Field<?> getField(Object propertyId) {
+ return fields.get(propertyId);
+ }
+
+ /* Documented in interface */
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIds);
+ }
+
+ /**
+ * Removes the property and corresponding field from the form.
+ *
+ * @see com.vaadin.data.Item#removeItemProperty(Object)
+ */
+ @Override
+ public boolean removeItemProperty(Object id) {
+ ownProperties.remove(id);
+
+ final Field<?> field = fields.get(id);
+
+ if (field != null) {
+ propertyIds.remove(id);
+ fields.remove(id);
+ detachField(field);
+ field.removeListener(fieldValueChangeListener);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Called when a form field is detached from a Form. Typically when a new
+ * Item is assigned to Form via {@link #setItemDataSource(Item)}.
+ * <p>
+ * Override this method to control how the fields are removed from the
+ * layout.
+ * </p>
+ *
+ * @param field
+ * the field to be detached from the forms layout.
+ */
+ protected void detachField(final Field field) {
+ Component p = field.getParent();
+ if (p instanceof ComponentContainer) {
+ ((ComponentContainer) p).removeComponent(field);
+ }
+ }
+
+ /**
+ * Removes all properties and fields from the form.
+ *
+ * @return the Success of the operation. Removal of all fields succeeded if
+ * (and only if) the return value is <code>true</code>.
+ */
+ public boolean removeAllProperties() {
+ final Object[] properties = propertyIds.toArray();
+ boolean success = true;
+
+ for (int i = 0; i < properties.length; i++) {
+ if (!removeItemProperty(properties[i])) {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ /* Documented in the interface */
+ @Override
+ public Item getItemDataSource() {
+ return itemDatasource;
+ }
+
+ /**
+ * Sets the item datasource for the form.
+ *
+ * <p>
+ * Setting item datasource clears any fields, the form might contain and
+ * adds all the properties as fields to the form.
+ * </p>
+ *
+ * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item)
+ */
+ @Override
+ public void setItemDataSource(Item newDataSource) {
+ setItemDataSource(newDataSource,
+ newDataSource != null ? newDataSource.getItemPropertyIds()
+ : null);
+ }
+
+ /**
+ * Set the item datasource for the form, but limit the form contents to
+ * specified properties of the item.
+ *
+ * <p>
+ * Setting item datasource clears any fields, the form might contain and
+ * adds the specified the properties as fields to the form, in the specified
+ * order.
+ * </p>
+ *
+ * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item)
+ */
+ public void setItemDataSource(Item newDataSource, Collection<?> propertyIds) {
+
+ if (getLayout() instanceof GridLayout) {
+ GridLayout gl = (GridLayout) getLayout();
+ if (gridlayoutCursorX == -1) {
+ // first setItemDataSource, remember initial cursor
+ gridlayoutCursorX = gl.getCursorX();
+ gridlayoutCursorY = gl.getCursorY();
+ } else {
+ // restore initial cursor
+ gl.setCursorX(gridlayoutCursorX);
+ gl.setCursorY(gridlayoutCursorY);
+ }
+ }
+
+ // Removes all fields first from the form
+ removeAllProperties();
+
+ // Sets the datasource
+ itemDatasource = newDataSource;
+
+ // If the new datasource is null, just set null datasource
+ if (itemDatasource == null) {
+ requestRepaint();
+ return;
+ }
+
+ // Adds all the properties to this form
+ for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) {
+ final Object id = i.next();
+ final Property<?> property = itemDatasource.getItemProperty(id);
+ if (id != null && property != null) {
+ final Field<?> f = fieldFactory.createField(itemDatasource, id,
+ this);
+ if (f != null) {
+ bindPropertyToField(id, property, f);
+ addField(id, f);
+ }
+ }
+ }
+ }
+
+ /**
+ * Binds an item property to a field. The default behavior is to bind
+ * property straight to Field. If Property.Viewer type property (e.g.
+ * PropertyFormatter) is already set for field, the property is bound to
+ * that Property.Viewer.
+ *
+ * @param propertyId
+ * @param property
+ * @param field
+ * @since 6.7.3
+ */
+ protected void bindPropertyToField(final Object propertyId,
+ final Property property, final Field field) {
+ // check if field has a property that is Viewer set. In that case we
+ // expect developer has e.g. PropertyFormatter that he wishes to use and
+ // assign the property to the Viewer instead.
+ boolean hasFilterProperty = field.getPropertyDataSource() != null
+ && (field.getPropertyDataSource() instanceof Property.Viewer);
+ if (hasFilterProperty) {
+ ((Property.Viewer) field.getPropertyDataSource())
+ .setPropertyDataSource(property);
+ } else {
+ field.setPropertyDataSource(property);
+ }
+ }
+
+ /**
+ * Gets the layout of the form.
+ *
+ * <p>
+ * By default form uses <code>OrderedLayout</code> with <code>form</code>
+ * -style.
+ * </p>
+ *
+ * @return the Layout of the form.
+ */
+ public Layout getLayout() {
+ return (Layout) getState().getLayout();
+ }
+
+ /**
+ * Sets the layout of the form.
+ *
+ * <p>
+ * If set to null then Form uses a FormLayout by default.
+ * </p>
+ *
+ * @param layout
+ * the layout of the form.
+ */
+ public void setLayout(Layout layout) {
+
+ // Use orderedlayout by default
+ if (layout == null) {
+ layout = new FormLayout();
+ }
+
+ // reset cursor memory
+ gridlayoutCursorX = -1;
+ gridlayoutCursorY = -1;
+
+ // Move fields from previous layout
+ if (getLayout() != null) {
+ final Object[] properties = propertyIds.toArray();
+ for (int i = 0; i < properties.length; i++) {
+ Field<?> f = getField(properties[i]);
+ detachField(f);
+ if (layout instanceof CustomLayout) {
+ ((CustomLayout) layout).addComponent(f,
+ properties[i].toString());
+ } else {
+ layout.addComponent(f);
+ }
+ }
+
+ getLayout().setParent(null);
+ }
+
+ // Replace the previous layout
+ layout.setParent(this);
+ getState().setLayout(layout);
+
+ // Hierarchy has changed so we need to repaint (this could be a
+ // hierarchy repaint only)
+ requestRepaint();
+ }
+
+ /**
+ * Sets the form field to be selectable from static list of changes.
+ *
+ * <p>
+ * The list values and descriptions are given as array. The value-array must
+ * contain the current value of the field and the lengths of the arrays must
+ * match. Null values are not supported.
+ * </p>
+ *
+ * Note: since Vaadin 7.0, returns an {@link AbstractSelect} instead of a
+ * {@link Select}.
+ *
+ * @param propertyId
+ * the id of the property.
+ * @param values
+ * @param descriptions
+ * @return the select property generated
+ */
+ public AbstractSelect replaceWithSelect(Object propertyId, Object[] values,
+ Object[] descriptions) {
+
+ // Checks the parameters
+ if (propertyId == null || values == null || descriptions == null) {
+ throw new NullPointerException("All parameters must be non-null");
+ }
+ if (values.length != descriptions.length) {
+ throw new IllegalArgumentException(
+ "Value and description list are of different size");
+ }
+
+ // Gets the old field
+ final Field<?> oldField = fields.get(propertyId);
+ if (oldField == null) {
+ throw new IllegalArgumentException("Field with given propertyid '"
+ + propertyId.toString() + "' can not be found.");
+ }
+ final Object value = oldField.getPropertyDataSource() == null ? oldField
+ .getValue() : oldField.getPropertyDataSource().getValue();
+
+ // Checks that the value exists and check if the select should
+ // be forced in multiselect mode
+ boolean found = false;
+ boolean isMultiselect = false;
+ for (int i = 0; i < values.length && !found; i++) {
+ if (values[i] == value
+ || (value != null && value.equals(values[i]))) {
+ found = true;
+ }
+ }
+ if (value != null && !found) {
+ if (value instanceof Collection) {
+ for (final Iterator<?> it = ((Collection<?>) value).iterator(); it
+ .hasNext();) {
+ final Object val = it.next();
+ found = false;
+ for (int i = 0; i < values.length && !found; i++) {
+ if (values[i] == val
+ || (val != null && val.equals(values[i]))) {
+ found = true;
+ }
+ }
+ if (!found) {
+ throw new IllegalArgumentException(
+ "Currently selected value '" + val
+ + "' of property '"
+ + propertyId.toString()
+ + "' was not found");
+ }
+ }
+ isMultiselect = true;
+ } else {
+ throw new IllegalArgumentException("Current value '" + value
+ + "' of property '" + propertyId.toString()
+ + "' was not found");
+ }
+ }
+
+ // Creates the new field matching to old field parameters
+ final AbstractSelect newField = isMultiselect ? new ListSelect()
+ : new Select();
+ newField.setCaption(oldField.getCaption());
+ newField.setReadOnly(oldField.isReadOnly());
+ newField.setReadThrough(oldField.isReadThrough());
+ newField.setWriteThrough(oldField.isWriteThrough());
+
+ // Creates the options list
+ newField.addContainerProperty("desc", String.class, "");
+ newField.setItemCaptionPropertyId("desc");
+ for (int i = 0; i < values.length; i++) {
+ Object id = values[i];
+ final Item item;
+ if (id == null) {
+ id = newField.addItem();
+ item = newField.getItem(id);
+ newField.setNullSelectionItemId(id);
+ } else {
+ item = newField.addItem(id);
+ }
+
+ if (item != null) {
+ item.getItemProperty("desc").setValue(
+ descriptions[i].toString());
+ }
+ }
+
+ // Sets the property data source
+ final Property<?> property = oldField.getPropertyDataSource();
+ oldField.setPropertyDataSource(null);
+ newField.setPropertyDataSource(property);
+
+ // Replaces the old field with new one
+ getLayout().replaceComponent(oldField, newField);
+ fields.put(propertyId, newField);
+ newField.addListener(fieldValueChangeListener);
+ oldField.removeListener(fieldValueChangeListener);
+
+ return newField;
+ }
+
+ /**
+ * Checks the validity of the Form and all of its fields.
+ *
+ * @see com.vaadin.data.Validatable#validate()
+ */
+ @Override
+ public void validate() throws InvalidValueException {
+ super.validate();
+ for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) {
+ (fields.get(i.next())).validate();
+ }
+ }
+
+ /**
+ * Checks the validabtable object accept invalid values.
+ *
+ * @see com.vaadin.data.Validatable#isInvalidAllowed()
+ */
+ @Override
+ public boolean isInvalidAllowed() {
+ return true;
+ }
+
+ /**
+ * Should the validabtable object accept invalid values.
+ *
+ * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean)
+ */
+ @Override
+ public void setInvalidAllowed(boolean invalidValueAllowed)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the component's to read-only mode to the specified state.
+ *
+ * @see com.vaadin.ui.Component#setReadOnly(boolean)
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ super.setReadOnly(readOnly);
+ for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) {
+ (fields.get(i.next())).setReadOnly(readOnly);
+ }
+ }
+
+ /**
+ * Sets the field factory used by this Form to genarate Fields for
+ * properties.
+ *
+ * {@link FormFieldFactory} is used to create fields for form properties.
+ * {@link DefaultFieldFactory} is used by default.
+ *
+ * @param fieldFactory
+ * the new factory used to create the fields.
+ * @see Field
+ * @see FormFieldFactory
+ */
+ public void setFormFieldFactory(FormFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+ }
+
+ /**
+ * Get the field factory of the form.
+ *
+ * @return the FormFieldFactory Factory used to create the fields.
+ */
+ public FormFieldFactory getFormFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Gets the field type.
+ *
+ * @see com.vaadin.ui.AbstractField#getType()
+ */
+ @Override
+ public Class<?> getType() {
+ if (getPropertyDataSource() != null) {
+ return getPropertyDataSource().getType();
+ }
+ return Object.class;
+ }
+
+ /**
+ * Sets the internal value.
+ *
+ * This is relevant when the Form is used as Field.
+ *
+ * @see com.vaadin.ui.AbstractField#setInternalValue(java.lang.Object)
+ */
+ @Override
+ protected void setInternalValue(Object newValue) {
+ // Stores the old value
+ final Object oldValue = propertyValue;
+
+ // Sets the current Value
+ super.setInternalValue(newValue);
+ propertyValue = newValue;
+
+ // Ignores form updating if data object has not changed.
+ if (oldValue != newValue) {
+ setFormDataSource(newValue, getVisibleItemProperties());
+ }
+ }
+
+ /**
+ * Gets the first focusable field in form. If there are enabled,
+ * non-read-only fields, the first one of them is returned. Otherwise, the
+ * field for the first property (or null if none) is returned.
+ *
+ * @return the Field.
+ */
+ private Field<?> getFirstFocusableField() {
+ if (getItemPropertyIds() != null) {
+ for (Object id : getItemPropertyIds()) {
+ if (id != null) {
+ Field<?> field = getField(id);
+ if (field.isEnabled() && !field.isReadOnly()) {
+ return field;
+ }
+ }
+ }
+ // fallback: first field if none of the fields is enabled and
+ // writable
+ Object id = getItemPropertyIds().iterator().next();
+ if (id != null) {
+ return getField(id);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates the internal form datasource.
+ *
+ * Method setFormDataSource.
+ *
+ * @param data
+ * @param properties
+ */
+ protected void setFormDataSource(Object data, Collection<?> properties) {
+
+ // If data is an item use it.
+ Item item = null;
+ if (data instanceof Item) {
+ item = (Item) data;
+ } else if (data != null) {
+ item = new BeanItem<Object>(data);
+ }
+
+ // Sets the datasource to form
+ if (item != null && properties != null) {
+ // Shows only given properties
+ this.setItemDataSource(item, properties);
+ } else {
+ // Shows all properties
+ this.setItemDataSource(item);
+ }
+ }
+
+ /**
+ * Returns the visibleProperties.
+ *
+ * @return the Collection of visible Item properites.
+ */
+ public Collection<?> getVisibleItemProperties() {
+ return visibleItemProperties;
+ }
+
+ /**
+ * Sets the visibleProperties.
+ *
+ * @param visibleProperties
+ * the visibleProperties to set.
+ */
+ public void setVisibleItemProperties(Collection<?> visibleProperties) {
+ visibleItemProperties = visibleProperties;
+ Object value = getValue();
+ if (value == null) {
+ value = itemDatasource;
+ }
+ setFormDataSource(value, getVisibleItemProperties());
+ }
+
+ /**
+ * Sets the visibleProperties.
+ *
+ * @param visibleProperties
+ * the visibleProperties to set.
+ */
+ public void setVisibleItemProperties(Object[] visibleProperties) {
+ LinkedList<Object> v = new LinkedList<Object>();
+ for (int i = 0; i < visibleProperties.length; i++) {
+ v.add(visibleProperties[i]);
+ }
+ setVisibleItemProperties(v);
+ }
+
+ /**
+ * Focuses the first field in the form.
+ *
+ * @see com.vaadin.ui.Component.Focusable#focus()
+ */
+ @Override
+ public void focus() {
+ final Field<?> f = getFirstFocusableField();
+ if (f != null) {
+ f.focus();
+ }
+ }
+
+ /**
+ * Sets the Tabulator index of this Focusable component.
+ *
+ * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ super.setTabIndex(tabIndex);
+ for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) {
+ (getField(i.next())).setTabIndex(tabIndex);
+ }
+ }
+
+ /**
+ * Setting the form to be immediate also sets all the fields of the form to
+ * the same state.
+ */
+ @Override
+ public void setImmediate(boolean immediate) {
+ super.setImmediate(immediate);
+ for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) {
+ Field<?> f = i.next();
+ if (f instanceof AbstractComponent) {
+ ((AbstractComponent) f).setImmediate(immediate);
+ }
+ }
+ }
+
+ /** Form is empty if all of its fields are empty. */
+ @Override
+ protected boolean isEmpty() {
+
+ for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) {
+ Field<?> f = i.next();
+ if (f instanceof AbstractField) {
+ if (!((AbstractField<?>) f).isEmpty()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Adding validators directly to form is not supported.
+ *
+ * Add the validators to form fields instead.
+ */
+ @Override
+ public void addValidator(Validator validator) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns a layout that is rendered below normal form contents. This area
+ * can be used for example to include buttons related to form contents.
+ *
+ * @return layout rendered below normal form contents.
+ */
+ public Layout getFooter() {
+ return (Layout) getState().getFooter();
+ }
+
+ /**
+ * Sets the layout that is rendered below normal form contents. Setting this
+ * to null will cause an empty HorizontalLayout to be rendered in the
+ * footer.
+ *
+ * @param footer
+ * the new footer layout
+ */
+ public void setFooter(Layout footer) {
+ if (getFooter() != null) {
+ getFooter().setParent(null);
+ }
+ if (footer == null) {
+ footer = new HorizontalLayout();
+ }
+
+ getState().setFooter(footer);
+ footer.setParent(this);
+
+ // Hierarchy has changed so we need to repaint (this could be a
+ // hierarchy repaint only)
+ requestRepaint();
+
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (getParent() != null && !getParent().isEnabled()) {
+ // some ancestor still disabled, don't update children
+ return;
+ } else {
+ getLayout().requestRepaintAll();
+ }
+ }
+
+ /*
+ * ACTIONS
+ */
+
+ /**
+ * Gets the {@link ActionManager} responsible for handling {@link Action}s
+ * added to this Form.<br/>
+ * Note that Form has another ActionManager inherited from
+ * {@link AbstractField}. The ownActionManager handles Actions attached to
+ * this Form specifically, while the ActionManager in AbstractField
+ * delegates to the containing Window (i.e global Actions).
+ *
+ * @return
+ */
+ protected ActionManager getOwnActionManager() {
+ if (ownActionManager == null) {
+ ownActionManager = new ActionManager(this);
+ }
+ return ownActionManager;
+ }
+
+ @Override
+ public void addActionHandler(Handler actionHandler) {
+ getOwnActionManager().addActionHandler(actionHandler);
+ }
+
+ @Override
+ public void removeActionHandler(Handler actionHandler) {
+ if (ownActionManager != null) {
+ ownActionManager.removeActionHandler(actionHandler);
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ if (ownActionManager != null) {
+ ownActionManager.removeAllActionHandlers();
+ }
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void addAction(
+ T action) {
+ getOwnActionManager().addAction(action);
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void removeAction(
+ T action) {
+ if (ownActionManager != null) {
+ ownActionManager.removeAction(action);
+ }
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ return getComponentIterator();
+ }
+
+ /**
+ * Modifiable and Serializable Iterator for the components, used by
+ * {@link Form#getComponentIterator()}.
+ */
+ private class ComponentIterator implements Iterator<Component>,
+ Serializable {
+
+ int i = 0;
+
+ @Override
+ public boolean hasNext() {
+ if (i < getComponentCount()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Component next() {
+ if (!hasNext()) {
+ return null;
+ }
+ i++;
+ if (i == 1) {
+ return getLayout() != null ? getLayout() : getFooter();
+ } else if (i == 2) {
+ return getFooter();
+ }
+ return null;
+ }
+
+ @Override
+ public void remove() {
+ if (i == 1) {
+ if (getLayout() != null) {
+ setLayout(null);
+ i = 0;
+ } else {
+ setFooter(null);
+ }
+ } else if (i == 2) {
+ setFooter(null);
+ }
+ }
+ }
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return new ComponentIterator();
+ }
+
+ public int getComponentCount() {
+ int count = 0;
+ if (getLayout() != null) {
+ count++;
+ }
+ if (getFooter() != null) {
+ count++;
+ }
+
+ return count;
+ }
+
+ @Override
+ public boolean isComponentVisible(Component childComponent) {
+ return true;
+ };
+}
diff --git a/server/src/com/vaadin/ui/FormFieldFactory.java b/server/src/com/vaadin/ui/FormFieldFactory.java
new file mode 100644
index 0000000000..1efa05c5f5
--- /dev/null
+++ b/server/src/com/vaadin/ui/FormFieldFactory.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Item;
+
+/**
+ * Factory interface for creating new Field-instances based on {@link Item},
+ * property id and uiContext (the component responsible for displaying fields).
+ * Currently this interface is used by {@link Form}, but might later be used by
+ * some other components for {@link Field} generation.
+ *
+ * <p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.0
+ * @see TableFieldFactory
+ */
+public interface FormFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the item, property id and the component (most
+ * commonly {@link Form}) where the Field will be presented.
+ *
+ * @param item
+ * the item where the property belongs to.
+ * @param propertyId
+ * the Id of the property.
+ * @param uiContext
+ * the component where the field is presented, most commonly this
+ * is {@link Form}. uiContext will not necessary be the parent
+ * component of the field, but the one that is responsible for
+ * creating it.
+ * @return Field the field suitable for editing the specified data.
+ */
+ Field<?> createField(Item item, Object propertyId, Component uiContext);
+}
diff --git a/server/src/com/vaadin/ui/FormLayout.java b/server/src/com/vaadin/ui/FormLayout.java
new file mode 100644
index 0000000000..c0be784a7b
--- /dev/null
+++ b/server/src/com/vaadin/ui/FormLayout.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+/**
+ * FormLayout is used by {@link Form} to layout fields. It may also be used
+ * separately without {@link Form}.
+ *
+ * FormLayout is a close relative to vertical {@link OrderedLayout}, but in
+ * FormLayout caption is rendered on left side of component. Required and
+ * validation indicators are between captions and fields.
+ *
+ * FormLayout does not currently support some advanced methods from
+ * OrderedLayout like setExpandRatio and setComponentAlignment.
+ *
+ * FormLayout by default has component spacing on. Also margin top and margin
+ * bottom are by default on.
+ *
+ */
+public class FormLayout extends AbstractOrderedLayout {
+
+ public FormLayout() {
+ super();
+ setSpacing(true);
+ setMargin(true, false, true, false);
+ setWidth(100, UNITS_PERCENTAGE);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/GridLayout.java b/server/src/com/vaadin/ui/GridLayout.java
new file mode 100644
index 0000000000..2391a9cd3a
--- /dev/null
+++ b/server/src/com/vaadin/ui/GridLayout.java
@@ -0,0 +1,1415 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.vaadin.event.LayoutEvents.LayoutClickEvent;
+import com.vaadin.event.LayoutEvents.LayoutClickListener;
+import com.vaadin.event.LayoutEvents.LayoutClickNotifier;
+import com.vaadin.shared.Connector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc;
+import com.vaadin.shared.ui.gridlayout.GridLayoutState;
+import com.vaadin.terminal.LegacyPaint;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler;
+
+/**
+ * A layout where the components are laid out on a grid using cell coordinates.
+ *
+ * <p>
+ * The GridLayout also maintains a cursor for adding components in
+ * left-to-right, top-to-bottom order.
+ * </p>
+ *
+ * <p>
+ * Each component in a <code>GridLayout</code> uses a defined
+ * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The
+ * components may not overlap with the existing components - if you try to do so
+ * you will get an {@link OverlapsException}. Adding a component with cursor
+ * automatically extends the grid by increasing the grid height.
+ * </p>
+ *
+ * <p>
+ * The grid coordinates, which are specified by a row and column index, always
+ * start from 0 for the topmost row and the leftmost column.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class GridLayout extends AbstractLayout implements
+ Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier,
+ Vaadin6Component {
+
+ private GridLayoutServerRpc rpc = new GridLayoutServerRpc() {
+
+ @Override
+ public void layoutClick(MouseEventDetails mouseDetails,
+ Connector clickedConnector) {
+ fireEvent(LayoutClickEvent.createEvent(GridLayout.this,
+ mouseDetails, clickedConnector));
+
+ }
+ };
+ /**
+ * Cursor X position: this is where the next component with unspecified x,y
+ * is inserted
+ */
+ private int cursorX = 0;
+
+ /**
+ * Cursor Y position: this is where the next component with unspecified x,y
+ * is inserted
+ */
+ private int cursorY = 0;
+
+ /**
+ * Contains all items that are placed on the grid. These are components with
+ * grid area definition.
+ */
+ private final LinkedList<Area> areas = new LinkedList<Area>();
+
+ /**
+ * Mapping from components to their respective areas.
+ */
+ private final LinkedList<Component> components = new LinkedList<Component>();
+
+ /**
+ * Mapping from components to alignments (horizontal + vertical).
+ */
+ private Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>();
+
+ private static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT;
+
+ /**
+ * Has there been rows inserted or deleted in the middle of the layout since
+ * the last paint operation.
+ */
+ private boolean structuralChange = false;
+
+ private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>();
+ private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>();
+
+ /**
+ * Constructor for a grid of given size (number of columns and rows).
+ *
+ * The grid may grow or shrink later. Grid grows automatically if you add
+ * components outside its area.
+ *
+ * @param columns
+ * Number of columns in the grid.
+ * @param rows
+ * Number of rows in the grid.
+ */
+ public GridLayout(int columns, int rows) {
+ setColumns(columns);
+ setRows(rows);
+ registerRpc(rpc);
+ }
+
+ /**
+ * Constructs an empty (1x1) grid layout that is extended as needed.
+ */
+ public GridLayout() {
+ this(1, 1);
+ }
+
+ @Override
+ public GridLayoutState getState() {
+ return (GridLayoutState) super.getState();
+ }
+
+ /**
+ * <p>
+ * Adds a component to the grid in the specified area. The area is defined
+ * by specifying the upper left corner (column1, row1) and the lower right
+ * corner (column2, row2) of the area. The coordinates are zero-based.
+ * </p>
+ *
+ * <p>
+ * If the area overlaps with any of the existing components already present
+ * in the grid, the operation will fail and an {@link OverlapsException} is
+ * thrown.
+ * </p>
+ *
+ * @param component
+ * the component to be added.
+ * @param column1
+ * the column of the upper left corner of the area <code>c</code>
+ * is supposed to occupy. The leftmost column has index 0.
+ * @param row1
+ * the row of the upper left corner of the area <code>c</code> is
+ * supposed to occupy. The topmost row has index 0.
+ * @param column2
+ * the column of the lower right corner of the area
+ * <code>c</code> is supposed to occupy.
+ * @param row2
+ * the row of the lower right corner of the area <code>c</code>
+ * is supposed to occupy.
+ * @throws OverlapsException
+ * if the new component overlaps with any of the components
+ * already in the grid.
+ * @throws OutOfBoundsException
+ * if the cells are outside the grid area.
+ */
+ public void addComponent(Component component, int column1, int row1,
+ int column2, int row2) throws OverlapsException,
+ OutOfBoundsException {
+
+ if (component == null) {
+ throw new NullPointerException("Component must not be null");
+ }
+
+ // Checks that the component does not already exist in the container
+ if (components.contains(component)) {
+ throw new IllegalArgumentException(
+ "Component is already in the container");
+ }
+
+ // Creates the area
+ final Area area = new Area(component, column1, row1, column2, row2);
+
+ // Checks the validity of the coordinates
+ if (column2 < column1 || row2 < row1) {
+ throw new IllegalArgumentException(
+ "Illegal coordinates for the component");
+ }
+ if (column1 < 0 || row1 < 0 || column2 >= getColumns()
+ || row2 >= getRows()) {
+ throw new OutOfBoundsException(area);
+ }
+
+ // Checks that newItem does not overlap with existing items
+ checkExistingOverlaps(area);
+
+ // Inserts the component to right place at the list
+ // Respect top-down, left-right ordering
+ // component.setParent(this);
+ final Iterator<Area> i = areas.iterator();
+ int index = 0;
+ boolean done = false;
+ while (!done && i.hasNext()) {
+ final Area existingArea = i.next();
+ if ((existingArea.row1 >= row1 && existingArea.column1 > column1)
+ || existingArea.row1 > row1) {
+ areas.add(index, area);
+ components.add(index, component);
+ done = true;
+ }
+ index++;
+ }
+ if (!done) {
+ areas.addLast(area);
+ components.addLast(component);
+ }
+
+ // Attempt to add to super
+ try {
+ super.addComponent(component);
+ } catch (IllegalArgumentException e) {
+ areas.remove(area);
+ components.remove(component);
+ throw e;
+ }
+
+ // update cursor position, if it's within this area; use first position
+ // outside this area, even if it's occupied
+ if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1
+ && cursorY <= row2) {
+ // cursor within area
+ cursorX = column2 + 1; // one right of area
+ if (cursorX >= getColumns()) {
+ // overflowed columns
+ cursorX = 0; // first col
+ // move one row down, or one row under the area
+ cursorY = (column1 == 0 ? row2 : row1) + 1;
+ } else {
+ cursorY = row1;
+ }
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Tests if the given area overlaps with any of the items already on the
+ * grid.
+ *
+ * @param area
+ * the Area to be checked for overlapping.
+ * @throws OverlapsException
+ * if <code>area</code> overlaps with any existing area.
+ */
+ private void checkExistingOverlaps(Area area) throws OverlapsException {
+ for (final Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ final Area existingArea = i.next();
+ if (existingArea.overlaps(area)) {
+ // Component not added, overlaps with existing component
+ throw new OverlapsException(existingArea);
+ }
+ }
+ }
+
+ /**
+ * Adds the component to the grid in cells column1,row1 (NortWest corner of
+ * the area.) End coordinates (SouthEast corner of the area) are the same as
+ * column1,row1. The coordinates are zero-based. Component width and height
+ * is 1.
+ *
+ * @param component
+ * the component to be added.
+ * @param column
+ * the column index, starting from 0.
+ * @param row
+ * the row index, starting from 0.
+ * @throws OverlapsException
+ * if the new component overlaps with any of the components
+ * already in the grid.
+ * @throws OutOfBoundsException
+ * if the cell is outside the grid area.
+ */
+ public void addComponent(Component component, int column, int row)
+ throws OverlapsException, OutOfBoundsException {
+ this.addComponent(component, column, row, column, row);
+ }
+
+ /**
+ * Forces the next component to be added at the beginning of the next line.
+ *
+ * <p>
+ * Sets the cursor column to 0 and increments the cursor row by one.
+ * </p>
+ *
+ * <p>
+ * By calling this function you can ensure that no more components are added
+ * right of the previous component.
+ * </p>
+ *
+ * @see #space()
+ */
+ public void newLine() {
+ cursorX = 0;
+ cursorY++;
+ }
+
+ /**
+ * Moves the cursor forward by one. If the cursor goes out of the right grid
+ * border, it is moved to the first column of the next row.
+ *
+ * @see #newLine()
+ */
+ public void space() {
+ cursorX++;
+ if (cursorX >= getColumns()) {
+ cursorX = 0;
+ cursorY++;
+ }
+ }
+
+ /**
+ * Adds the component into this container to the cursor position. If the
+ * cursor position is already occupied, the cursor is moved forwards to find
+ * free position. If the cursor goes out from the bottom of the grid, the
+ * grid is automatically extended.
+ *
+ * @param component
+ * the component to be added.
+ */
+ @Override
+ public void addComponent(Component component) {
+
+ // Finds first available place from the grid
+ Area area;
+ boolean done = false;
+ while (!done) {
+ try {
+ area = new Area(component, cursorX, cursorY, cursorX, cursorY);
+ checkExistingOverlaps(area);
+ done = true;
+ } catch (final OverlapsException e) {
+ space();
+ }
+ }
+
+ // Extends the grid if needed
+ if (cursorX >= getColumns()) {
+ setColumns(cursorX + 1);
+ }
+ if (cursorY >= getRows()) {
+ setRows(cursorY + 1);
+ }
+
+ addComponent(component, cursorX, cursorY);
+ }
+
+ /**
+ * Removes the specified component from the layout.
+ *
+ * @param component
+ * the component to be removed.
+ */
+ @Override
+ public void removeComponent(Component component) {
+
+ // Check that the component is contained in the container
+ if (component == null || !components.contains(component)) {
+ return;
+ }
+
+ Area area = null;
+ for (final Iterator<Area> i = areas.iterator(); area == null
+ && i.hasNext();) {
+ final Area a = i.next();
+ if (a.getComponent() == component) {
+ area = a;
+ }
+ }
+
+ components.remove(component);
+ if (area != null) {
+ areas.remove(area);
+ }
+
+ componentToAlignment.remove(component);
+
+ super.removeComponent(component);
+
+ requestRepaint();
+ }
+
+ /**
+ * Removes the component specified by its cell coordinates.
+ *
+ * @param column
+ * the component's column, starting from 0.
+ * @param row
+ * the component's row, starting from 0.
+ */
+ public void removeComponent(int column, int row) {
+
+ // Finds the area
+ for (final Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ final Area area = i.next();
+ if (area.getColumn1() == column && area.getRow1() == row) {
+ removeComponent(area.getComponent());
+ return;
+ }
+ }
+ }
+
+ /**
+ * Gets an Iterator for the components contained in the layout. By using the
+ * Iterator it is possible to step through the contents of the layout.
+ *
+ * @return the Iterator of the components inside the layout.
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return Collections.unmodifiableCollection(components).iterator();
+ }
+
+ /**
+ * Gets the number of components contained in the layout. Consistent with
+ * the iterator returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once Vaadin6Component is no longer implemented
+ }
+
+ /**
+ * Paints the contents of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ // TODO refactor attribute names in future release.
+ target.addAttribute("structuralChange", structuralChange);
+ structuralChange = false;
+
+ // Area iterator
+ final Iterator<Area> areaiterator = areas.iterator();
+
+ // Current item to be processed (fetch first item)
+ Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null;
+
+ // Collects rowspan related information here
+ final HashMap<Integer, Integer> cellUsed = new HashMap<Integer, Integer>();
+
+ // Empty cell collector
+ int emptyCells = 0;
+
+ final String[] alignmentsArray = new String[components.size()];
+ final Integer[] columnExpandRatioArray = new Integer[getColumns()];
+ final Integer[] rowExpandRatioArray = new Integer[getRows()];
+
+ int realColExpandRatioSum = 0;
+ float colSum = getExpandRatioSum(columnExpandRatio);
+ if (colSum == 0) {
+ // no columns has been expanded, all cols have same expand
+ // rate
+ float equalSize = 1 / (float) getColumns();
+ int myRatio = Math.round(equalSize * 1000);
+ for (int i = 0; i < getColumns(); i++) {
+ columnExpandRatioArray[i] = myRatio;
+ }
+ realColExpandRatioSum = myRatio * getColumns();
+ } else {
+ for (int i = 0; i < getColumns(); i++) {
+ int myRatio = Math
+ .round((getColumnExpandRatio(i) / colSum) * 1000);
+ columnExpandRatioArray[i] = myRatio;
+ realColExpandRatioSum += myRatio;
+ }
+ }
+
+ boolean equallyDividedRows = false;
+ int realRowExpandRatioSum = 0;
+ float rowSum = getExpandRatioSum(rowExpandRatio);
+ if (rowSum == 0) {
+ // no rows have been expanded
+ equallyDividedRows = true;
+ float equalSize = 1 / (float) getRows();
+ int myRatio = Math.round(equalSize * 1000);
+ for (int i = 0; i < getRows(); i++) {
+ rowExpandRatioArray[i] = myRatio;
+ }
+ realRowExpandRatioSum = myRatio * getRows();
+ }
+
+ int index = 0;
+
+ // Iterates every applicable row
+ for (int cury = 0; cury < getRows(); cury++) {
+ target.startTag("gr");
+
+ if (!equallyDividedRows) {
+ int myRatio = Math
+ .round((getRowExpandRatio(cury) / rowSum) * 1000);
+ rowExpandRatioArray[cury] = myRatio;
+ realRowExpandRatioSum += myRatio;
+
+ }
+ // Iterates every applicable column
+ for (int curx = 0; curx < getColumns(); curx++) {
+
+ // Checks if current item is located at curx,cury
+ if (area != null && (area.row1 == cury)
+ && (area.column1 == curx)) {
+
+ // First check if empty cell needs to be rendered
+ if (emptyCells > 0) {
+ target.startTag("gc");
+ target.addAttribute("x", curx - emptyCells);
+ target.addAttribute("y", cury);
+ if (emptyCells > 1) {
+ target.addAttribute("w", emptyCells);
+ }
+ target.endTag("gc");
+ emptyCells = 0;
+ }
+
+ // Now proceed rendering current item
+ final int cols = (area.column2 - area.column1) + 1;
+ final int rows = (area.row2 - area.row1) + 1;
+ target.startTag("gc");
+
+ target.addAttribute("x", curx);
+ target.addAttribute("y", cury);
+
+ if (cols > 1) {
+ target.addAttribute("w", cols);
+ }
+ if (rows > 1) {
+ target.addAttribute("h", rows);
+ }
+ LegacyPaint.paint(area.getComponent(), target);
+
+ alignmentsArray[index++] = String
+ .valueOf(getComponentAlignment(area.getComponent())
+ .getBitMask());
+
+ target.endTag("gc");
+
+ // Fetch next item
+ if (areaiterator.hasNext()) {
+ area = areaiterator.next();
+ } else {
+ area = null;
+ }
+
+ // Updates the cellUsed if rowspan needed
+ if (rows > 1) {
+ int spannedx = curx;
+ for (int j = 1; j <= cols; j++) {
+ cellUsed.put(new Integer(spannedx), new Integer(
+ cury + rows - 1));
+ spannedx++;
+ }
+ }
+
+ // Skips the current item's spanned columns
+ if (cols > 1) {
+ curx += cols - 1;
+ }
+
+ } else {
+
+ // Checks against cellUsed, render space or ignore cell
+ if (cellUsed.containsKey(new Integer(curx))) {
+
+ // Current column contains already an item,
+ // check if rowspan affects at current x,y position
+ final int rowspanDepth = cellUsed
+ .get(new Integer(curx)).intValue();
+
+ if (rowspanDepth >= cury) {
+
+ // ignore cell
+ // Check if empty cell needs to be rendered
+ if (emptyCells > 0) {
+ target.startTag("gc");
+ target.addAttribute("x", curx - emptyCells);
+ target.addAttribute("y", cury);
+ if (emptyCells > 1) {
+ target.addAttribute("w", emptyCells);
+ }
+ target.endTag("gc");
+
+ emptyCells = 0;
+ }
+ } else {
+
+ // empty cell is needed
+ emptyCells++;
+
+ // Removes the cellUsed key as it has become
+ // obsolete
+ cellUsed.remove(Integer.valueOf(curx));
+ }
+ } else {
+
+ // empty cell is needed
+ emptyCells++;
+ }
+ }
+
+ } // iterates every column
+
+ // Last column handled of current row
+
+ // Checks if empty cell needs to be rendered
+ if (emptyCells > 0) {
+ target.startTag("gc");
+ target.addAttribute("x", getColumns() - emptyCells);
+ target.addAttribute("y", cury);
+ if (emptyCells > 1) {
+ target.addAttribute("w", emptyCells);
+ }
+ target.endTag("gc");
+
+ emptyCells = 0;
+ }
+
+ target.endTag("gr");
+ } // iterates every row
+
+ // Last row handled
+
+ // correct possible rounding error
+ if (rowExpandRatioArray.length > 0) {
+ rowExpandRatioArray[0] -= realRowExpandRatioSum - 1000;
+ }
+ if (columnExpandRatioArray.length > 0) {
+ columnExpandRatioArray[0] -= realColExpandRatioSum - 1000;
+ }
+
+ target.addAttribute("colExpand", columnExpandRatioArray);
+ target.addAttribute("rowExpand", rowExpandRatioArray);
+
+ // Add child component alignment info to layout tag
+ target.addAttribute("alignments", alignmentsArray);
+
+ }
+
+ private float getExpandRatioSum(Map<Integer, Float> ratioMap) {
+ float sum = 0;
+ for (Iterator<Entry<Integer, Float>> iterator = ratioMap.entrySet()
+ .iterator(); iterator.hasNext();) {
+ sum += iterator.next().getValue();
+ }
+ return sum;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com
+ * .vaadin.ui.Component)
+ */
+ @Override
+ public Alignment getComponentAlignment(Component childComponent) {
+ Alignment alignment = componentToAlignment.get(childComponent);
+ if (alignment == null) {
+ return ALIGNMENT_DEFAULT;
+ } else {
+ return alignment;
+ }
+ }
+
+ /**
+ * Defines a rectangular area of cells in a GridLayout.
+ *
+ * <p>
+ * Also maintains a reference to the component contained in the area.
+ * </p>
+ *
+ * <p>
+ * The area is specified by the cell coordinates of its upper left corner
+ * (column1,row1) and lower right corner (column2,row2). As otherwise with
+ * GridLayout, the column and row coordinates start from zero.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class Area implements Serializable {
+
+ /**
+ * The column of the upper left corner cell of the area.
+ */
+ private final int column1;
+
+ /**
+ * The row of the upper left corner cell of the area.
+ */
+ private int row1;
+
+ /**
+ * The column of the lower right corner cell of the area.
+ */
+ private final int column2;
+
+ /**
+ * The row of the lower right corner cell of the area.
+ */
+ private int row2;
+
+ /**
+ * Component painted in the area.
+ */
+ private Component component;
+
+ /**
+ * <p>
+ * Construct a new area on a grid.
+ * </p>
+ *
+ * @param component
+ * the component connected to the area.
+ * @param column1
+ * The column of the upper left corner cell of the area. The
+ * leftmost column has index 0.
+ * @param row1
+ * The row of the upper left corner cell of the area. The
+ * topmost row has index 0.
+ * @param column2
+ * The column of the lower right corner cell of the area. The
+ * leftmost column has index 0.
+ * @param row2
+ * The row of the lower right corner cell of the area. The
+ * topmost row has index 0.
+ */
+ public Area(Component component, int column1, int row1, int column2,
+ int row2) {
+ this.column1 = column1;
+ this.row1 = row1;
+ this.column2 = column2;
+ this.row2 = row2;
+ this.component = component;
+ }
+
+ /**
+ * Tests if this Area overlaps with another Area.
+ *
+ * @param other
+ * the other Area that is to be tested for overlap with this
+ * area
+ * @return <code>true</code> if <code>other</code> area overlaps with
+ * this on, <code>false</code> if it does not.
+ */
+ public boolean overlaps(Area other) {
+ return column1 <= other.getColumn2() && row1 <= other.getRow2()
+ && column2 >= other.getColumn1() && row2 >= other.getRow1();
+
+ }
+
+ /**
+ * Gets the component connected to the area.
+ *
+ * @return the Component.
+ */
+ public Component getComponent() {
+ return component;
+ }
+
+ /**
+ * Sets the component connected to the area.
+ *
+ * <p>
+ * This function only sets the value in the data structure and does not
+ * send any events or set parents.
+ * </p>
+ *
+ * @param newComponent
+ * the new connected overriding the existing one.
+ */
+ protected void setComponent(Component newComponent) {
+ component = newComponent;
+ }
+
+ /**
+ * @deprecated Use {@link #getColumn1()} instead.
+ */
+ @Deprecated
+ public int getX1() {
+ return getColumn1();
+ }
+
+ /**
+ * Gets the column of the top-left corner cell.
+ *
+ * @return the column of the top-left corner cell.
+ */
+ public int getColumn1() {
+ return column1;
+ }
+
+ /**
+ * @deprecated Use {@link #getColumn2()} instead.
+ */
+ @Deprecated
+ public int getX2() {
+ return getColumn2();
+ }
+
+ /**
+ * Gets the column of the bottom-right corner cell.
+ *
+ * @return the column of the bottom-right corner cell.
+ */
+ public int getColumn2() {
+ return column2;
+ }
+
+ /**
+ * @deprecated Use {@link #getRow1()} instead.
+ */
+ @Deprecated
+ public int getY1() {
+ return getRow1();
+ }
+
+ /**
+ * Gets the row of the top-left corner cell.
+ *
+ * @return the row of the top-left corner cell.
+ */
+ public int getRow1() {
+ return row1;
+ }
+
+ /**
+ * @deprecated Use {@link #getRow2()} instead.
+ */
+ @Deprecated
+ public int getY2() {
+ return getRow2();
+ }
+
+ /**
+ * Gets the row of the bottom-right corner cell.
+ *
+ * @return the row of the bottom-right corner cell.
+ */
+ public int getRow2() {
+ return row2;
+ }
+
+ }
+
+ /**
+ * Gridlayout does not support laying components on top of each other. An
+ * <code>OverlapsException</code> is thrown when a component already exists
+ * (even partly) at the same space on a grid with the new component.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class OverlapsException extends java.lang.RuntimeException {
+
+ private final Area existingArea;
+
+ /**
+ * Constructs an <code>OverlapsException</code>.
+ *
+ * @param existingArea
+ */
+ public OverlapsException(Area existingArea) {
+ this.existingArea = existingArea;
+ }
+
+ @Override
+ public String getMessage() {
+ StringBuilder sb = new StringBuilder();
+ Component component = existingArea.getComponent();
+ sb.append(component);
+ sb.append("( type = ");
+ sb.append(component.getClass().getName());
+ if (component.getCaption() != null) {
+ sb.append(", caption = \"");
+ sb.append(component.getCaption());
+ sb.append("\"");
+ }
+ sb.append(")");
+ sb.append(" is already added to ");
+ sb.append(existingArea.column1);
+ sb.append(",");
+ sb.append(existingArea.column1);
+ sb.append(",");
+ sb.append(existingArea.row1);
+ sb.append(",");
+ sb.append(existingArea.row2);
+ sb.append("(column1, column2, row1, row2).");
+
+ return sb.toString();
+ }
+
+ /**
+ * Gets the area .
+ *
+ * @return the existing area.
+ */
+ public Area getArea() {
+ return existingArea;
+ }
+ }
+
+ /**
+ * An <code>Exception</code> object which is thrown when an area exceeds the
+ * bounds of the grid.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class OutOfBoundsException extends java.lang.RuntimeException {
+
+ private final Area areaOutOfBounds;
+
+ /**
+ * Constructs an <code>OoutOfBoundsException</code> with the specified
+ * detail message.
+ *
+ * @param areaOutOfBounds
+ */
+ public OutOfBoundsException(Area areaOutOfBounds) {
+ this.areaOutOfBounds = areaOutOfBounds;
+ }
+
+ /**
+ * Gets the area that is out of bounds.
+ *
+ * @return the area out of Bound.
+ */
+ public Area getArea() {
+ return areaOutOfBounds;
+ }
+ }
+
+ /**
+ * Sets the number of columns in the grid. The column count can not be
+ * reduced if there are any areas that would be outside of the shrunk grid.
+ *
+ * @param columns
+ * the new number of columns in the grid.
+ */
+ public void setColumns(int columns) {
+
+ // The the param
+ if (columns < 1) {
+ throw new IllegalArgumentException(
+ "The number of columns and rows in the grid must be at least 1");
+ }
+
+ // In case of no change
+ if (getColumns() == columns) {
+ return;
+ }
+
+ // Checks for overlaps
+ if (getColumns() > columns) {
+ for (final Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ final Area area = i.next();
+ if (area.column2 >= columns) {
+ throw new OutOfBoundsException(area);
+ }
+ }
+ }
+
+ getState().setColumns(columns);
+
+ requestRepaint();
+ }
+
+ /**
+ * Get the number of columns in the grid.
+ *
+ * @return the number of columns in the grid.
+ */
+ public int getColumns() {
+ return getState().getColumns();
+ }
+
+ /**
+ * Sets the number of rows in the grid. The number of rows can not be
+ * reduced if there are any areas that would be outside of the shrunk grid.
+ *
+ * @param rows
+ * the new number of rows in the grid.
+ */
+ public void setRows(int rows) {
+
+ // The the param
+ if (rows < 1) {
+ throw new IllegalArgumentException(
+ "The number of columns and rows in the grid must be at least 1");
+ }
+
+ // In case of no change
+ if (getRows() == rows) {
+ return;
+ }
+
+ // Checks for overlaps
+ if (getRows() > rows) {
+ for (final Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ final Area area = i.next();
+ if (area.row2 >= rows) {
+ throw new OutOfBoundsException(area);
+ }
+ }
+ }
+
+ getState().setRows(rows);
+
+ requestRepaint();
+ }
+
+ /**
+ * Get the number of rows in the grid.
+ *
+ * @return the number of rows in the grid.
+ */
+ public int getRows() {
+ return getState().getRows();
+ }
+
+ /**
+ * Gets the current x-position (column) of the cursor.
+ *
+ * <p>
+ * The cursor position points the position for the next component that is
+ * added without specifying its coordinates (grid cell). When the cursor
+ * position is occupied, the next component will be added to first free
+ * position after the cursor.
+ * </p>
+ *
+ * @return the grid column the cursor is on, starting from 0.
+ */
+ public int getCursorX() {
+ return cursorX;
+ }
+
+ /**
+ * Sets the current cursor x-position. This is usually handled automatically
+ * by GridLayout.
+ *
+ * @param cursorX
+ */
+ public void setCursorX(int cursorX) {
+ this.cursorX = cursorX;
+ }
+
+ /**
+ * Gets the current y-position (row) of the cursor.
+ *
+ * <p>
+ * The cursor position points the position for the next component that is
+ * added without specifying its coordinates (grid cell). When the cursor
+ * position is occupied, the next component will be added to the first free
+ * position after the cursor.
+ * </p>
+ *
+ * @return the grid row the Cursor is on.
+ */
+ public int getCursorY() {
+ return cursorY;
+ }
+
+ /**
+ * Sets the current y-coordinate (row) of the cursor. This is usually
+ * handled automatically by GridLayout.
+ *
+ * @param cursorY
+ * the row number, starting from 0 for the topmost row.
+ */
+ public void setCursorY(int cursorY) {
+ this.cursorY = cursorY;
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ // Gets the locations
+ Area oldLocation = null;
+ Area newLocation = null;
+ for (final Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ final Area location = i.next();
+ final Component component = location.getComponent();
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+ }
+
+ if (oldLocation == null) {
+ addComponent(newComponent);
+ } else if (newLocation == null) {
+ removeComponent(oldComponent);
+ addComponent(newComponent, oldLocation.getColumn1(),
+ oldLocation.getRow1(), oldLocation.getColumn2(),
+ oldLocation.getRow2());
+ } else {
+ oldLocation.setComponent(newComponent);
+ newLocation.setComponent(oldComponent);
+ requestRepaint();
+ }
+ }
+
+ /*
+ * Removes all components from this container.
+ *
+ * @see com.vaadin.ui.ComponentContainer#removeAllComponents()
+ */
+ @Override
+ public void removeAllComponents() {
+ super.removeAllComponents();
+ componentToAlignment = new HashMap<Component, Alignment>();
+ cursorX = 0;
+ cursorY = 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com
+ * .vaadin.ui.Component, int, int)
+ */
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ int horizontalAlignment, int verticalAlignment) {
+ componentToAlignment.put(childComponent, new Alignment(
+ horizontalAlignment + verticalAlignment));
+ requestRepaint();
+ }
+
+ @Override
+ public void setComponentAlignment(Component childComponent,
+ Alignment alignment) {
+ componentToAlignment.put(childComponent, alignment);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean)
+ */
+ @Override
+ public void setSpacing(boolean spacing) {
+ getState().setSpacing(spacing);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing()
+ */
+ @Override
+ public boolean isSpacing() {
+ return getState().isSpacing();
+ }
+
+ /**
+ * Inserts an empty row at the specified position in the grid.
+ *
+ * @param row
+ * Index of the row before which the new row will be inserted.
+ * The leftmost row has index 0.
+ */
+ public void insertRow(int row) {
+ if (row > getRows()) {
+ throw new IllegalArgumentException("Cannot insert row at " + row
+ + " in a gridlayout with height " + getRows());
+ }
+
+ for (Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ Area existingArea = i.next();
+ // Areas ending below the row needs to be moved down or stretched
+ if (existingArea.row2 >= row) {
+ existingArea.row2++;
+
+ // Stretch areas that span over the selected row
+ if (existingArea.row1 >= row) {
+ existingArea.row1++;
+ }
+
+ }
+ }
+
+ if (cursorY >= row) {
+ cursorY++;
+ }
+
+ setRows(getRows() + 1);
+ structuralChange = true;
+ requestRepaint();
+ }
+
+ /**
+ * Removes a row and all the components in the row.
+ *
+ * <p>
+ * Components which span over several rows are removed if the selected row
+ * is on the first row of such a component.
+ * </p>
+ *
+ * <p>
+ * If the last row is removed then all remaining components will be removed
+ * and the grid will be reduced to one row. The cursor will be moved to the
+ * upper left cell of the grid.
+ * </p>
+ *
+ * @param row
+ * Index of the row to remove. The leftmost row has index 0.
+ */
+ public void removeRow(int row) {
+ if (row >= getRows()) {
+ throw new IllegalArgumentException("Cannot delete row " + row
+ + " from a gridlayout with height " + getRows());
+ }
+
+ // Remove all components in row
+ for (int col = 0; col < getColumns(); col++) {
+ removeComponent(col, row);
+ }
+
+ // Shrink or remove areas in the selected row
+ for (Iterator<Area> i = areas.iterator(); i.hasNext();) {
+ Area existingArea = i.next();
+ if (existingArea.row2 >= row) {
+ existingArea.row2--;
+
+ if (existingArea.row1 > row) {
+ existingArea.row1--;
+ }
+ }
+ }
+
+ if (getRows() == 1) {
+ /*
+ * Removing the last row means that the dimensions of the Grid
+ * layout will be truncated to 1 empty row and the cursor is moved
+ * to the first cell
+ */
+ cursorX = 0;
+ cursorY = 0;
+ } else {
+ setRows(getRows() - 1);
+ if (cursorY > row) {
+ cursorY--;
+ }
+ }
+
+ structuralChange = true;
+ requestRepaint();
+
+ }
+
+ /**
+ * Sets the expand ratio of given column.
+ *
+ * <p>
+ * The expand ratio defines how excess space is distributed among columns.
+ * Excess space means space that is left over from components that are not
+ * sized relatively. By default, the excess space is distributed evenly.
+ * </p>
+ *
+ * <p>
+ * Note that the component width of the GridLayout must be defined (fixed or
+ * relative, as opposed to undefined) for this method to have any effect.
+ * </p>
+ *
+ * @see #setWidth(float, int)
+ *
+ * @param columnIndex
+ * @param ratio
+ */
+ public void setColumnExpandRatio(int columnIndex, float ratio) {
+ columnExpandRatio.put(columnIndex, ratio);
+ requestRepaint();
+ }
+
+ /**
+ * Returns the expand ratio of given column
+ *
+ * @see #setColumnExpandRatio(int, float)
+ *
+ * @param columnIndex
+ * @return the expand ratio, 0.0f by default
+ */
+ public float getColumnExpandRatio(int columnIndex) {
+ Float r = columnExpandRatio.get(columnIndex);
+ return r == null ? 0 : r.floatValue();
+ }
+
+ /**
+ * Sets the expand ratio of given row.
+ *
+ * <p>
+ * Expand ratio defines how excess space is distributed among rows. Excess
+ * space means the space left over from components that are not sized
+ * relatively. By default, the excess space is distributed evenly.
+ * </p>
+ *
+ * <p>
+ * Note, that height needs to be defined (fixed or relative, as opposed to
+ * undefined height) for this method to have any effect.
+ * </p>
+ *
+ * @see #setHeight(float, int)
+ *
+ * @param rowIndex
+ * The row index, starting from 0 for the topmost row.
+ * @param ratio
+ */
+ public void setRowExpandRatio(int rowIndex, float ratio) {
+ rowExpandRatio.put(rowIndex, ratio);
+ requestRepaint();
+ }
+
+ /**
+ * Returns the expand ratio of given row.
+ *
+ * @see #setRowExpandRatio(int, float)
+ *
+ * @param rowIndex
+ * The row index, starting from 0 for the topmost row.
+ * @return the expand ratio, 0.0f by default
+ */
+ public float getRowExpandRatio(int rowIndex) {
+ Float r = rowExpandRatio.get(rowIndex);
+ return r == null ? 0 : r.floatValue();
+ }
+
+ /**
+ * Gets the Component at given index.
+ *
+ * @param x
+ * The column index, starting from 0 for the leftmost column.
+ * @param y
+ * The row index, starting from 0 for the topmost row.
+ * @return Component in given cell or null if empty
+ */
+ public Component getComponent(int x, int y) {
+ for (final Iterator<Area> iterator = areas.iterator(); iterator
+ .hasNext();) {
+ final Area area = iterator.next();
+ if (area.getColumn1() <= x && x <= area.getColumn2()
+ && area.getRow1() <= y && y <= area.getRow2()) {
+ return area.getComponent();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns information about the area where given component is laid in the
+ * GridLayout.
+ *
+ * @param component
+ * the component whose area information is requested.
+ * @return an Area object that contains information how component is laid in
+ * the grid
+ */
+ public Area getComponentArea(Component component) {
+ for (final Iterator<Area> iterator = areas.iterator(); iterator
+ .hasNext();) {
+ final Area area = iterator.next();
+ if (area.getComponent() == component) {
+ return area;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addListener(LayoutClickListener listener) {
+ addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener,
+ LayoutClickListener.clickMethod);
+ }
+
+ @Override
+ public void removeListener(LayoutClickListener listener) {
+ removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER,
+ LayoutClickEvent.class, listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/HasComponents.java b/server/src/com/vaadin/ui/HasComponents.java
new file mode 100644
index 0000000000..3ebd63bff2
--- /dev/null
+++ b/server/src/com/vaadin/ui/HasComponents.java
@@ -0,0 +1,49 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.util.Iterator;
+
+/**
+ * Interface that must be implemented by all {@link Component}s that contain
+ * other {@link Component}s.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ *
+ */
+public interface HasComponents extends Component, Iterable<Component> {
+ /**
+ * Gets an iterator to the collection of contained components. Using this
+ * iterator it is possible to step through all components contained in this
+ * container.
+ *
+ * @return the component iterator.
+ *
+ * @deprecated Use {@link #iterator()} instead.
+ */
+ @Deprecated
+ public Iterator<Component> getComponentIterator();
+
+ /**
+ * Checks if the child component is visible. This method allows hiding a
+ * child component from updates and communication to and from the client.
+ * This is useful for components that show only a limited number of its
+ * children at any given time and want to allow updates only for the
+ * children that are visible (e.g. TabSheet has one tab open at a time).
+ * <p>
+ * Note that this will prevent updates from reaching the child even though
+ * the child itself is set to visible. Also if a child is set to invisible
+ * this will not force it to be visible.
+ * </p>
+ *
+ * @param childComponent
+ * The child component to check
+ * @return true if the child component is visible to the user, false
+ * otherwise
+ */
+ public boolean isComponentVisible(Component childComponent);
+
+}
diff --git a/server/src/com/vaadin/ui/HorizontalLayout.java b/server/src/com/vaadin/ui/HorizontalLayout.java
new file mode 100644
index 0000000000..b9dc1c13ca
--- /dev/null
+++ b/server/src/com/vaadin/ui/HorizontalLayout.java
@@ -0,0 +1,24 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+/**
+ * Horizontal layout
+ *
+ * <code>HorizontalLayout</code> is a component container, which shows the
+ * subcomponents in the order of their addition (horizontally).
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.3
+ */
+@SuppressWarnings("serial")
+public class HorizontalLayout extends AbstractOrderedLayout {
+
+ public HorizontalLayout() {
+
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/HorizontalSplitPanel.java b/server/src/com/vaadin/ui/HorizontalSplitPanel.java
new file mode 100644
index 0000000000..5bd6c8a075
--- /dev/null
+++ b/server/src/com/vaadin/ui/HorizontalSplitPanel.java
@@ -0,0 +1,34 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+/**
+ * A horizontal split panel contains two components and lays them horizontally.
+ * The first component is on the left side.
+ *
+ * <pre>
+ *
+ * +---------------------++----------------------+
+ * | || |
+ * | The first component || The second component |
+ * | || |
+ * +---------------------++----------------------+
+ *
+ * ^
+ * |
+ * the splitter
+ *
+ * </pre>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.5
+ */
+public class HorizontalSplitPanel extends AbstractSplitPanel {
+ public HorizontalSplitPanel() {
+ super();
+ setSizeFull();
+ }
+}
diff --git a/server/src/com/vaadin/ui/Html5File.java b/server/src/com/vaadin/ui/Html5File.java
new file mode 100644
index 0000000000..aa3fb558fa
--- /dev/null
+++ b/server/src/com/vaadin/ui/Html5File.java
@@ -0,0 +1,65 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.terminal.StreamVariable;
+
+/**
+ * {@link DragAndDropWrapper} can receive also files from client computer if
+ * appropriate HTML 5 features are supported on client side. This class wraps
+ * information about dragged file on server side.
+ */
+public class Html5File implements Serializable {
+
+ private String name;
+ private long size;
+ private StreamVariable streamVariable;
+ private String type;
+
+ Html5File(String name, long size, String mimeType) {
+ this.name = name;
+ this.size = size;
+ type = mimeType;
+ }
+
+ public String getFileName() {
+ return name;
+ }
+
+ public long getFileSize() {
+ return size;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Sets the {@link StreamVariable} that into which the file contents will be
+ * written. Usage of StreamVariable is similar to {@link Upload} component.
+ * <p>
+ * If the {@link StreamVariable} is not set in the {@link DropHandler} the
+ * file contents will not be sent to server.
+ * <p>
+ * <em>Note!</em> receiving file contents is experimental feature depending
+ * on HTML 5 API's. It is supported only by modern web browsers like Firefox
+ * 3.6 and above and recent webkit based browsers (Safari 5, Chrome 6) at
+ * this time.
+ *
+ * @param streamVariable
+ * the callback that returns stream where the implementation
+ * writes the file contents as it arrives.
+ */
+ public void setStreamVariable(StreamVariable streamVariable) {
+ this.streamVariable = streamVariable;
+ }
+
+ public StreamVariable getStreamVariable() {
+ return streamVariable;
+ }
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/InlineDateField.java b/server/src/com/vaadin/ui/InlineDateField.java
new file mode 100644
index 0000000000..cf61703318
--- /dev/null
+++ b/server/src/com/vaadin/ui/InlineDateField.java
@@ -0,0 +1,46 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Date;
+
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A date entry component, which displays the actual date selector inline.
+ *
+ * </p>
+ *
+ * @see DateField
+ * @see PopupDateField
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class InlineDateField extends DateField {
+
+ public InlineDateField() {
+ super();
+ }
+
+ public InlineDateField(Property dataSource) throws IllegalArgumentException {
+ super(dataSource);
+ }
+
+ public InlineDateField(String caption, Date value) {
+ super(caption, value);
+ }
+
+ public InlineDateField(String caption, Property dataSource) {
+ super(caption, dataSource);
+ }
+
+ public InlineDateField(String caption) {
+ super(caption);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/JavaScript.java b/server/src/com/vaadin/ui/JavaScript.java
new file mode 100644
index 0000000000..0b4669728a
--- /dev/null
+++ b/server/src/com/vaadin/ui/JavaScript.java
@@ -0,0 +1,157 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc;
+import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState;
+import com.vaadin.terminal.AbstractExtension;
+import com.vaadin.terminal.Page;
+
+/**
+ * Provides access to JavaScript functionality in the web browser. To get an
+ * instance of JavaScript, either use Page.getJavaScript() or
+ * JavaScript.getCurrent() as a shorthand for getting the JavaScript object
+ * corresponding to the current Page.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public class JavaScript extends AbstractExtension {
+ private Map<String, JavaScriptFunction> functions = new HashMap<String, JavaScriptFunction>();
+
+ // Can not be defined in client package as this JSONArray is not available
+ // in GWT
+ public interface JavaScriptCallbackRpc extends ServerRpc {
+ public void call(String name, JSONArray arguments);
+ }
+
+ /**
+ * Creates a new JavaScript object. You should typically not this, but
+ * instead use the JavaScript object already associated with your Page
+ * object.
+ */
+ public JavaScript() {
+ registerRpc(new JavaScriptCallbackRpc() {
+ @Override
+ public void call(String name, JSONArray arguments) {
+ JavaScriptFunction function = functions.get(name);
+ // TODO handle situation if name is not registered
+ try {
+ function.call(arguments);
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public JavaScriptManagerState getState() {
+ return (JavaScriptManagerState) super.getState();
+ }
+
+ /**
+ * Add a new function to the global JavaScript namespace (i.e. the window
+ * object). The <code>call</code> method in the passed
+ * {@link JavaScriptFunction} object will be invoked with the same
+ * parameters whenever the JavaScript function is called in the browser.
+ *
+ * A function added with the name <code>"myFunction"</code> can thus be
+ * invoked with the following JavaScript code:
+ * <code>window.myFunction(argument1, argument2)</code>.
+ *
+ * If the name parameter contains dots, simple objects are created on demand
+ * to allow calling the function using the same name (e.g.
+ * <code>window.myObject.myFunction</code>).
+ *
+ * @param name
+ * the name that the function should get in the global JavaScript
+ * namespace.
+ * @param function
+ * the JavaScriptFunction that will be invoked if the JavaScript
+ * function is called.
+ */
+ public void addFunction(String name, JavaScriptFunction function) {
+ functions.put(name, function);
+ if (getState().getNames().add(name)) {
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Removes a JavaScripFunction from the browser's global JavaScript
+ * namespace.
+ *
+ * If the name contains dots and intermediate objects were created by
+ * {@link #addFunction(String, JavaScriptFunction)}, these objects will not
+ * be removed by this method.
+ *
+ * @param name
+ * the name of the callback to remove
+ */
+ public void removeFunction(String name) {
+ functions.remove(name);
+ if (getState().getNames().remove(name)) {
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Executes the given JavaScript code in the browser.
+ *
+ * @param script
+ * The JavaScript code to run.
+ */
+ public void execute(String script) {
+ getRpcProxy(ExecuteJavaScriptRpc.class).executeJavaScript(script);
+ }
+
+ /**
+ * Executes the given JavaScript code in the browser.
+ *
+ * @param script
+ * The JavaScript code to run.
+ */
+ public static void eval(String script) {
+ getCurrent().execute(script);
+ }
+
+ /**
+ * Get the JavaScript object for the current Page, or null if there is no
+ * current page.
+ *
+ * @see Page#getCurrent()
+ *
+ * @return the JavaScript object corresponding to the current Page, or
+ * <code>null</code> if there is no current page.
+ */
+ public static JavaScript getCurrent() {
+ Page page = Page.getCurrent();
+ if (page == null) {
+ return null;
+ }
+ return page.getJavaScript();
+ }
+
+ /**
+ * JavaScript is not designed to be removed.
+ *
+ * @throws UnsupportedOperationException
+ * when invoked
+ */
+ @Override
+ public void removeFromTarget() {
+ throw new UnsupportedOperationException(
+ "JavaScript is not designed to be removed.");
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/JavaScriptFunction.java b/server/src/com/vaadin/ui/JavaScriptFunction.java
new file mode 100644
index 0000000000..e39ae9b87b
--- /dev/null
+++ b/server/src/com/vaadin/ui/JavaScriptFunction.java
@@ -0,0 +1,41 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.external.json.JSONArray;
+import com.vaadin.external.json.JSONException;
+import com.vaadin.terminal.AbstractJavaScriptExtension;
+
+/**
+ * Defines a method that is called by a client-side JavaScript function. When
+ * the corresponding JavaScript function is called, the {@link #call(JSONArray)}
+ * method is invoked.
+ *
+ * @see JavaScript#addFunction(String, JavaScriptCallback)
+ * @see AbstractJavaScriptComponent#addFunction(String, JavaScriptCallback)
+ * @see AbstractJavaScriptExtension#addFunction(String, JavaScriptCallback)
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0.0
+ */
+public interface JavaScriptFunction extends Serializable {
+ /**
+ * Invoked whenever the corresponding JavaScript function is called in the
+ * browser.
+ * <p>
+ * Because of the asynchronous nature of the communication between client
+ * and server, no return value can be sent back to the browser.
+ *
+ * @param arguments
+ * an array with JSON representations of the arguments with which
+ * the JavaScript function was called.
+ * @throws JSONException
+ * if the arguments can not be interpreted
+ */
+ public void call(JSONArray arguments) throws JSONException;
+}
diff --git a/server/src/com/vaadin/ui/Label.java b/server/src/com/vaadin/ui/Label.java
new file mode 100644
index 0000000000..7e50a37805
--- /dev/null
+++ b/server/src/com/vaadin/ui/Label.java
@@ -0,0 +1,483 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.lang.reflect.Method;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Property;
+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;
+
+/**
+ * Label component for showing non-editable short texts.
+ *
+ * The label content can be set to the modes specified by {@link ContentMode}
+ *
+ * <p>
+ * The contents of the label may contain simple formatting:
+ * <ul>
+ * <li><b>&lt;b></b> Bold
+ * <li><b>&lt;i></b> Italic
+ * <li><b>&lt;u></b> Underlined
+ * <li><b>&lt;br/></b> Linebreak
+ * <li><b>&lt;ul>&lt;li>item 1&lt;/li>&lt;li>item 2&lt;/li>&lt;/ul></b> List of
+ * items
+ * </ul>
+ * The <b>b</b>,<b>i</b>,<b>u</b> and <b>li</b> tags can contain all the tags in
+ * the list recursively.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Label extends AbstractComponent implements Property<String>,
+ Property.Viewer, Property.ValueChangeListener,
+ Property.ValueChangeNotifier, Comparable<Label> {
+
+ private static final Logger logger = Logger
+ .getLogger(Label.class.getName());
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#TEXT} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_TEXT = ContentMode.TEXT;
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#PREFORMATTED} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED;
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#XHTML} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_XHTML = ContentMode.XHTML;
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#XML} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_XML = ContentMode.XML;
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#RAW} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_RAW = ContentMode.RAW;
+
+ /**
+ * @deprecated From 7.0, use {@link ContentMode#TEXT} instead
+ */
+ @Deprecated
+ public static final ContentMode CONTENT_DEFAULT = ContentMode.TEXT;
+
+ /**
+ * A converter used to convert from the data model type to the field type
+ * and vice versa. Label type is always String.
+ */
+ private Converter<String, Object> converter = null;
+
+ private Property<String> dataSource = null;
+
+ /**
+ * Creates an empty Label.
+ */
+ public Label() {
+ this("");
+ }
+
+ /**
+ * Creates a new instance of Label with text-contents.
+ *
+ * @param content
+ */
+ public Label(String content) {
+ this(content, ContentMode.TEXT);
+ }
+
+ /**
+ * Creates a new instance of Label with text-contents read from given
+ * datasource.
+ *
+ * @param contentSource
+ */
+ public Label(Property contentSource) {
+ this(contentSource, ContentMode.TEXT);
+ }
+
+ /**
+ * Creates a new instance of Label with text-contents.
+ *
+ * @param content
+ * @param contentMode
+ */
+ public Label(String content, ContentMode contentMode) {
+ setValue(content);
+ setContentMode(contentMode);
+ setWidth(100, Unit.PERCENTAGE);
+ }
+
+ /**
+ * Creates a new instance of Label with text-contents read from given
+ * datasource.
+ *
+ * @param contentSource
+ * @param contentMode
+ */
+ public Label(Property contentSource, ContentMode contentMode) {
+ setPropertyDataSource(contentSource);
+ setContentMode(contentMode);
+ setWidth(100, Unit.PERCENTAGE);
+ }
+
+ @Override
+ public LabelState getState() {
+ return (LabelState) super.getState();
+ }
+
+ /**
+ * Gets the value of the label.
+ * <p>
+ * The value of the label is the text that is shown to the end user.
+ * Depending on the {@link ContentMode} it is plain text or markup.
+ * </p>
+ *
+ * @return the value of the label.
+ */
+ @Override
+ public String getValue() {
+ if (getPropertyDataSource() == null) {
+ // Use internal value if we are running without a data source
+ return getState().getText();
+ }
+ return ConverterUtil.convertFromModel(getPropertyDataSource()
+ .getValue(), String.class, getConverter(), getLocale());
+ }
+
+ /**
+ * Set the value of the label. Value of the label is the XML contents of the
+ * label.
+ *
+ * @param newStringValue
+ * the New value of the label.
+ */
+ @Override
+ public void setValue(Object newStringValue) {
+ if (newStringValue != null && newStringValue.getClass() != String.class) {
+ throw new Converter.ConversionException("Value of type "
+ + newStringValue.getClass() + " cannot be assigned to "
+ + String.class.getName());
+ }
+ if (getPropertyDataSource() == null) {
+ getState().setText((String) newStringValue);
+ requestRepaint();
+ } else {
+ throw new IllegalStateException(
+ "Label is only a Property.Viewer and cannot update its data source");
+ }
+ }
+
+ /**
+ * Returns the value displayed by this label.
+ *
+ * @see java.lang.Object#toString()
+ * @deprecated As of 7.0.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 is not recommended and will not be supported in future versions.");
+ return getValue();
+ }
+
+ /**
+ * Gets the type of the Property.
+ *
+ * @see com.vaadin.data.Property#getType()
+ */
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Gets the viewing data-source property.
+ *
+ * @return the data source property.
+ * @see com.vaadin.data.Property.Viewer#getPropertyDataSource()
+ */
+ @Override
+ public Property getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the property as data-source for viewing.
+ *
+ * @param newDataSource
+ * the new data source Property
+ * @see com.vaadin.data.Property.Viewer#setPropertyDataSource(com.vaadin.data.Property)
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+ // Stops listening the old data source changes
+ if (dataSource != null
+ && Property.ValueChangeNotifier.class
+ .isAssignableFrom(dataSource.getClass())) {
+ ((Property.ValueChangeNotifier) dataSource).removeListener(this);
+ }
+
+ if (!ConverterUtil.canConverterHandle(getConverter(), String.class,
+ newDataSource.getType())) {
+ // Try to find a converter
+ Converter<String, ?> c = ConverterUtil.getConverter(String.class,
+ newDataSource.getType(), getApplication());
+ setConverter(c);
+ }
+ dataSource = newDataSource;
+
+ // Listens the new data source if possible
+ if (dataSource != null
+ && Property.ValueChangeNotifier.class
+ .isAssignableFrom(dataSource.getClass())) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Gets the content mode of the Label.
+ *
+ * @return the Content mode of the label.
+ *
+ * @see ContentMode
+ */
+ public ContentMode getContentMode() {
+ return getState().getContentMode();
+ }
+
+ /**
+ * Sets the content mode of the Label.
+ *
+ * @param contentMode
+ * the New content mode of the label.
+ *
+ * @see ContentMode
+ */
+ public void setContentMode(ContentMode contentMode) {
+ if (contentMode == null) {
+ throw new IllegalArgumentException("Content mode can not be null");
+ }
+
+ getState().setContentMode(contentMode);
+ requestRepaint();
+ }
+
+ /* Value change events */
+
+ private static final Method VALUE_CHANGE_METHOD;
+
+ static {
+ try {
+ VALUE_CHANGE_METHOD = Property.ValueChangeListener.class
+ .getDeclaredMethod("valueChange",
+ new Class[] { Property.ValueChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in Label");
+ }
+ }
+
+ /**
+ * Value change event
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class ValueChangeEvent extends Component.Event implements
+ Property.ValueChangeEvent {
+
+ /**
+ * New instance of text change event
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public ValueChangeEvent(Label source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property that has been modified.
+ *
+ * @see com.vaadin.data.Property.ValueChangeEvent#getProperty()
+ */
+ @Override
+ public Property getProperty() {
+ return (Property) getSource();
+ }
+ }
+
+ /**
+ * Adds the value change listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void addListener(Property.ValueChangeListener listener) {
+ addListener(Label.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes the value change listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com.vaadin.data.Property.ValueChangeListener)
+ */
+ @Override
+ public void removeListener(Property.ValueChangeListener listener) {
+ removeListener(Label.ValueChangeEvent.class, listener,
+ VALUE_CHANGE_METHOD);
+ }
+
+ /**
+ * Emits the options change event.
+ */
+ protected void fireValueChange() {
+ // Set the error message
+ fireEvent(new Label.ValueChangeEvent(this));
+ }
+
+ /**
+ * Listens the value change events from data source.
+ *
+ * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent)
+ */
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ // Update the internal value from the data source
+ getState().setText(getValue());
+ requestRepaint();
+
+ fireValueChange();
+ }
+
+ private String getComparableValue() {
+ String stringValue = getValue();
+ if (stringValue == null) {
+ stringValue = "";
+ }
+
+ if (getContentMode() == ContentMode.XHTML
+ || getContentMode() == ContentMode.XML) {
+ return stripTags(stringValue);
+ } else {
+ return stringValue;
+ }
+
+ }
+
+ /**
+ * Compares the Label to other objects.
+ *
+ * <p>
+ * Labels can be compared to other labels for sorting label contents. This
+ * is especially handy for sorting table columns.
+ * </p>
+ *
+ * <p>
+ * In RAW, PREFORMATTED and TEXT modes, the label contents are compared as
+ * is. In XML, UIDL and XHTML modes, only CDATA is compared and tags
+ * ignored. If the other object is not a Label, its toString() return value
+ * is used in comparison.
+ * </p>
+ *
+ * @param other
+ * the Other object to compare to.
+ * @return a negative integer, zero, or a positive integer as this object is
+ * less than, equal to, or greater than the specified object.
+ * @see java.lang.Comparable#compareTo(java.lang.Object)
+ */
+ @Override
+ public int compareTo(Label other) {
+
+ String thisValue = getComparableValue();
+ String otherValue = other.getComparableValue();
+
+ return thisValue.compareTo(otherValue);
+ }
+
+ /**
+ * Strips the tags from the XML.
+ *
+ * @param xml
+ * the String containing a XML snippet.
+ * @return the original XML without tags.
+ */
+ private String stripTags(String xml) {
+
+ final StringBuffer res = new StringBuffer();
+
+ int processed = 0;
+ final int xmlLen = xml.length();
+ while (processed < xmlLen) {
+ int next = xml.indexOf('<', processed);
+ if (next < 0) {
+ next = xmlLen;
+ }
+ res.append(xml.substring(processed, next));
+ if (processed < xmlLen) {
+ next = xml.indexOf('>', processed);
+ if (next < 0) {
+ next = xmlLen;
+ }
+ processed = next + 1;
+ }
+ }
+
+ return res.toString();
+ }
+
+ /**
+ * Gets the converter used to convert the property data source value to the
+ * label value.
+ *
+ * @return The converter or null if none is set.
+ */
+ public Converter<String, Object> getConverter() {
+ return converter;
+ }
+
+ /**
+ * Sets the converter used to convert the label value to the property data
+ * source type. The converter must have a presentation type of String.
+ *
+ * @param converter
+ * The new converter to use.
+ */
+ public void setConverter(Converter<String, ?> converter) {
+ this.converter = (Converter<String, Object>) converter;
+ requestRepaint();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Layout.java b/server/src/com/vaadin/ui/Layout.java
new file mode 100644
index 0000000000..d083f9afdc
--- /dev/null
+++ b/server/src/com/vaadin/ui/Layout.java
@@ -0,0 +1,229 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.VMarginInfo;
+import com.vaadin.shared.ui.AlignmentInfo.Bits;
+
+/**
+ * Extension to the {@link ComponentContainer} interface which adds the
+ * layouting control to the elements in the container. This is required by the
+ * various layout components to enable them to place other components in
+ * specific locations in the UI.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Layout extends ComponentContainer, Serializable {
+
+ /**
+ * Enable layout margins. Affects all four sides of the layout. This will
+ * tell the client-side implementation to leave extra space around the
+ * layout. The client-side implementation decides the actual amount, and it
+ * can vary between themes.
+ *
+ * @param enabled
+ */
+ public void setMargin(boolean enabled);
+
+ /**
+ * Enable specific layout margins. This will tell the client-side
+ * implementation to leave extra space around the layout in specified edges,
+ * clockwise from top (top, right, bottom, left). The client-side
+ * implementation decides the actual amount, and it can vary between themes.
+ *
+ * @param top
+ * @param right
+ * @param bottom
+ * @param left
+ */
+ public void setMargin(boolean top, boolean right, boolean bottom,
+ boolean left);
+
+ /**
+ * AlignmentHandler is most commonly an advanced {@link Layout} that can
+ * align its components.
+ */
+ public interface AlignmentHandler extends Serializable {
+
+ /**
+ * Contained component should be aligned horizontally to the left.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_LEFT = Bits.ALIGNMENT_LEFT;
+
+ /**
+ * Contained component should be aligned horizontally to the right.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_RIGHT = Bits.ALIGNMENT_RIGHT;
+
+ /**
+ * Contained component should be aligned vertically to the top.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_TOP = Bits.ALIGNMENT_TOP;
+
+ /**
+ * Contained component should be aligned vertically to the bottom.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_BOTTOM = Bits.ALIGNMENT_BOTTOM;
+
+ /**
+ * Contained component should be horizontally aligned to center.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_HORIZONTAL_CENTER = Bits.ALIGNMENT_HORIZONTAL_CENTER;
+
+ /**
+ * Contained component should be vertically aligned to center.
+ *
+ * @deprecated Use of {@link Alignment} class and its constants
+ */
+ @Deprecated
+ public static final int ALIGNMENT_VERTICAL_CENTER = Bits.ALIGNMENT_VERTICAL_CENTER;
+
+ /**
+ * Set alignment for one contained component in this layout. Alignment
+ * is calculated as a bit mask of the two passed values.
+ *
+ * @deprecated Use {@link #setComponentAlignment(Component, Alignment)}
+ * instead
+ *
+ * @param childComponent
+ * the component to align within it's layout cell.
+ * @param horizontalAlignment
+ * the horizontal alignment for the child component (left,
+ * center, right). Use ALIGNMENT constants.
+ * @param verticalAlignment
+ * the vertical alignment for the child component (top,
+ * center, bottom). Use ALIGNMENT constants.
+ */
+ @Deprecated
+ public void setComponentAlignment(Component childComponent,
+ int horizontalAlignment, int verticalAlignment);
+
+ /**
+ * Set alignment for one contained component in this layout. Use
+ * predefined alignments from Alignment class.
+ *
+ * Example: <code>
+ * layout.setComponentAlignment(myComponent, Alignment.TOP_RIGHT);
+ * </code>
+ *
+ * @param childComponent
+ * the component to align within it's layout cell.
+ * @param alignment
+ * the Alignment value to be set
+ */
+ public void setComponentAlignment(Component childComponent,
+ Alignment alignment);
+
+ /**
+ * Returns the current Alignment of given component.
+ *
+ * @param childComponent
+ * @return the {@link Alignment}
+ */
+ public Alignment getComponentAlignment(Component childComponent);
+
+ }
+
+ /**
+ * This type of layout supports automatic addition of space between its
+ * components.
+ *
+ */
+ public interface SpacingHandler extends Serializable {
+ /**
+ * Enable spacing between child components within this layout.
+ *
+ * <p>
+ * <strong>NOTE:</strong> This will only affect the space between
+ * components, not the space around all the components in the layout
+ * (i.e. do not confuse this with the cellspacing attribute of a HTML
+ * Table). Use {@link #setMargin(boolean)} to add space around the
+ * layout.
+ * </p>
+ *
+ * <p>
+ * See the reference manual for more information about CSS rules for
+ * defining the amount of spacing to use.
+ * </p>
+ *
+ * @param enabled
+ * true if spacing should be turned on, false if it should be
+ * turned off
+ */
+ public void setSpacing(boolean enabled);
+
+ /**
+ *
+ * @return true if spacing between child components within this layout
+ * is enabled, false otherwise
+ */
+ public boolean isSpacing();
+ }
+
+ /**
+ * This type of layout supports automatic addition of margins (space around
+ * its components).
+ */
+ public interface MarginHandler extends Serializable {
+ /**
+ * Enable margins for this layout.
+ *
+ * <p>
+ * <strong>NOTE:</strong> This will only affect the space around the
+ * components in the layout, not space between the components in the
+ * layout. Use {@link #setSpacing(boolean)} to add space between the
+ * components in the layout.
+ * </p>
+ *
+ * <p>
+ * See the reference manual for more information about CSS rules for
+ * defining the size of the margin.
+ * </p>
+ *
+ * @param marginInfo
+ * MarginInfo object containing the new margins.
+ */
+ public void setMargin(MarginInfo marginInfo);
+
+ /**
+ *
+ * @return MarginInfo containing the currently enabled margins.
+ */
+ public MarginInfo getMargin();
+ }
+
+ @SuppressWarnings("serial")
+ public static class MarginInfo extends VMarginInfo implements Serializable {
+
+ public MarginInfo(boolean enabled) {
+ super(enabled, enabled, enabled, enabled);
+ }
+
+ public MarginInfo(boolean top, boolean right, boolean bottom,
+ boolean left) {
+ super(top, right, bottom, left);
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/Link.java b/server/src/com/vaadin/ui/Link.java
new file mode 100644
index 0000000000..fd105f3255
--- /dev/null
+++ b/server/src/com/vaadin/ui/Link.java
@@ -0,0 +1,242 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Map;
+
+import com.vaadin.terminal.Page;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ * Link is used to create external or internal URL links.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Link extends AbstractComponent implements Vaadin6Component {
+
+ /* Target window border type constant: No window border */
+ public static final int TARGET_BORDER_NONE = Page.BORDER_NONE;
+
+ /* Target window border type constant: Minimal window border */
+ public static final int TARGET_BORDER_MINIMAL = Page.BORDER_MINIMAL;
+
+ /* Target window border type constant: Default window border */
+ public static final int TARGET_BORDER_DEFAULT = Page.BORDER_DEFAULT;
+
+ private Resource resource = null;
+
+ private String targetName;
+
+ private int targetBorder = TARGET_BORDER_DEFAULT;
+
+ private int targetWidth = -1;
+
+ private int targetHeight = -1;
+
+ /**
+ * Creates a new link.
+ */
+ public Link() {
+
+ }
+
+ /**
+ * Creates a new instance of Link.
+ *
+ * @param caption
+ * @param resource
+ */
+ public Link(String caption, Resource resource) {
+ setCaption(caption);
+ this.resource = resource;
+ }
+
+ /**
+ * Creates a new instance of Link that opens a new window.
+ *
+ *
+ * @param caption
+ * the Link text.
+ * @param targetName
+ * the name of the target window where the link opens to. Empty
+ * name of null implies that the target is opened to the window
+ * containing the link.
+ * @param width
+ * the Width of the target window.
+ * @param height
+ * the Height of the target window.
+ * @param border
+ * the Border style of the target window.
+ *
+ */
+ public Link(String caption, Resource resource, String targetName,
+ int width, int height, int border) {
+ setCaption(caption);
+ this.resource = resource;
+ setTargetName(targetName);
+ setTargetWidth(width);
+ setTargetHeight(height);
+ setTargetBorder(border);
+ }
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ if (resource != null) {
+ target.addAttribute("src", resource);
+ } else {
+ return;
+ }
+
+ // Target window name
+ final String name = getTargetName();
+ if (name != null && name.length() > 0) {
+ target.addAttribute("name", name);
+ }
+
+ // Target window size
+ if (getTargetWidth() >= 0) {
+ target.addAttribute("targetWidth", getTargetWidth());
+ }
+ if (getTargetHeight() >= 0) {
+ target.addAttribute("targetHeight", getTargetHeight());
+ }
+
+ // Target window border
+ switch (getTargetBorder()) {
+ case TARGET_BORDER_MINIMAL:
+ target.addAttribute("border", "minimal");
+ break;
+ case TARGET_BORDER_NONE:
+ target.addAttribute("border", "none");
+ break;
+ }
+ }
+
+ /**
+ * Returns the target window border.
+ *
+ * @return the target window border.
+ */
+ public int getTargetBorder() {
+ return targetBorder;
+ }
+
+ /**
+ * Returns the target window height or -1 if not set.
+ *
+ * @return the target window height.
+ */
+ public int getTargetHeight() {
+ return targetHeight < 0 ? -1 : targetHeight;
+ }
+
+ /**
+ * Returns the target window name. Empty name of null implies that the
+ * target is opened to the window containing the link.
+ *
+ * @return the target window name.
+ */
+ public String getTargetName() {
+ return targetName;
+ }
+
+ /**
+ * Returns the target window width or -1 if not set.
+ *
+ * @return the target window width.
+ */
+ public int getTargetWidth() {
+ return targetWidth < 0 ? -1 : targetWidth;
+ }
+
+ /**
+ * Sets the border of the target window.
+ *
+ * @param targetBorder
+ * the targetBorder to set.
+ */
+ public void setTargetBorder(int targetBorder) {
+ if (targetBorder == TARGET_BORDER_DEFAULT
+ || targetBorder == TARGET_BORDER_MINIMAL
+ || targetBorder == TARGET_BORDER_NONE) {
+ this.targetBorder = targetBorder;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Sets the target window height.
+ *
+ * @param targetHeight
+ * the targetHeight to set.
+ */
+ public void setTargetHeight(int targetHeight) {
+ this.targetHeight = targetHeight;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the target window name.
+ *
+ * @param targetName
+ * the targetName to set.
+ */
+ public void setTargetName(String targetName) {
+ this.targetName = targetName;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the target window width.
+ *
+ * @param targetWidth
+ * the targetWidth to set.
+ */
+ public void setTargetWidth(int targetWidth) {
+ this.targetWidth = targetWidth;
+ requestRepaint();
+ }
+
+ /**
+ * Returns the resource this link opens.
+ *
+ * @return the Resource.
+ */
+ public Resource getResource() {
+ return resource;
+ }
+
+ /**
+ * Sets the resource this link opens.
+ *
+ * @param resource
+ * the resource to set.
+ */
+ public void setResource(Resource resource) {
+ this.resource = resource;
+ requestRepaint();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once Vaadin6Component is no longer implemented
+ }
+}
diff --git a/server/src/com/vaadin/ui/ListSelect.java b/server/src/com/vaadin/ui/ListSelect.java
new file mode 100644
index 0000000000..35ccb34b3c
--- /dev/null
+++ b/server/src/com/vaadin/ui/ListSelect.java
@@ -0,0 +1,96 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * This is a simple list select without, for instance, support for new items,
+ * lazyloading, and other advanced features.
+ */
+@SuppressWarnings("serial")
+public class ListSelect extends AbstractSelect {
+
+ private int columns = 0;
+ private int rows = 0;
+
+ public ListSelect() {
+ super();
+ }
+
+ public ListSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public ListSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public ListSelect(String caption) {
+ super(caption);
+ }
+
+ /**
+ * Sets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ *
+ * @param columns
+ * the number of columns to set.
+ */
+ public void setColumns(int columns) {
+ if (columns < 0) {
+ columns = 0;
+ }
+ if (this.columns != columns) {
+ this.columns = columns;
+ requestRepaint();
+ }
+ }
+
+ public int getColumns() {
+ return columns;
+ }
+
+ public int getRows() {
+ return rows;
+ }
+
+ /**
+ * Sets the number of rows in the editor. If the number of rows is set 0,
+ * the actual number of displayed rows is determined implicitly by the
+ * adapter.
+ *
+ * @param rows
+ * the number of rows to set.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ if (this.rows != rows) {
+ this.rows = rows;
+ requestRepaint();
+ }
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute("type", "list");
+ // Adds the number of columns
+ if (columns != 0) {
+ target.addAttribute("cols", columns);
+ }
+ // Adds the number of rows
+ if (rows != 0) {
+ target.addAttribute("rows", rows);
+ }
+ super.paintContent(target);
+ }
+}
diff --git a/server/src/com/vaadin/ui/LoginForm.java b/server/src/com/vaadin/ui/LoginForm.java
new file mode 100644
index 0000000000..db7e5f9dd9
--- /dev/null
+++ b/server/src/com/vaadin/ui/LoginForm.java
@@ -0,0 +1,353 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.DownloadStream;
+import com.vaadin.terminal.RequestHandler;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+/**
+ * LoginForm is a Vaadin component to handle common problem among Ajax
+ * applications: browsers password managers don't fill dynamically created forms
+ * like all those UI elements created by Vaadin.
+ * <p>
+ * For developer it is easy to use: add component to a desired place in you UI
+ * and add LoginListener to validate form input. Behind the curtain LoginForm
+ * creates an iframe with static html that browsers detect.
+ * <p>
+ * Login form is by default 100% width and height, so consider using it inside a
+ * sized {@link Panel} or {@link Window}.
+ * <p>
+ * Login page html can be overridden by replacing protected getLoginHTML method.
+ * As the login page is actually an iframe, styles must be handled manually. By
+ * default component tries to guess the right place for theme css.
+ *
+ * @since 5.3
+ */
+public class LoginForm extends CustomComponent {
+
+ private String usernameCaption = "Username";
+ private String passwordCaption = "Password";
+ private String loginButtonCaption = "Login";
+
+ private Embedded iframe = new Embedded();
+
+ private ApplicationResource loginPage = new ApplicationResource() {
+
+ @Override
+ public Application getApplication() {
+ return LoginForm.this.getApplication();
+ }
+
+ @Override
+ public int getBufferSize() {
+ return getLoginHTML().length;
+ }
+
+ @Override
+ public long getCacheTime() {
+ return -1;
+ }
+
+ @Override
+ public String getFilename() {
+ return "login";
+ }
+
+ @Override
+ public DownloadStream getStream() {
+ return new DownloadStream(new ByteArrayInputStream(getLoginHTML()),
+ getMIMEType(), getFilename());
+ }
+
+ @Override
+ public String getMIMEType() {
+ return "text/html; charset=utf-8";
+ }
+ };
+
+ private final RequestHandler requestHandler = new RequestHandler() {
+ @Override
+ public boolean handleRequest(Application application,
+ WrappedRequest request, WrappedResponse response)
+ throws IOException {
+ String requestPathInfo = request.getRequestPathInfo();
+ if ("/loginHandler".equals(requestPathInfo)) {
+ response.setCacheTime(-1);
+ response.setContentType("text/html; charset=utf-8");
+ response.getWriter()
+ .write("<html><body>Login form handled."
+ + "<script type='text/javascript'>parent.parent.vaadin.forceSync();"
+ + "</script></body></html>");
+
+ Map<String, String[]> parameters = request.getParameterMap();
+
+ HashMap<String, String> params = new HashMap<String, String>();
+ // expecting single params
+ for (Iterator<String> it = parameters.keySet().iterator(); it
+ .hasNext();) {
+ String key = it.next();
+ String value = (parameters.get(key))[0];
+ params.put(key, value);
+ }
+ LoginEvent event = new LoginEvent(params);
+ fireEvent(event);
+ return true;
+ }
+ return false;
+ }
+ };
+
+ public LoginForm() {
+ iframe.setType(Embedded.TYPE_BROWSER);
+ iframe.setSizeFull();
+ setSizeFull();
+ setCompositionRoot(iframe);
+ addStyleName("v-loginform");
+ }
+
+ /**
+ * Returns byte array containing login page html. If you need to override
+ * the login html, use the default html as basis. Login page sets its target
+ * with javascript.
+ *
+ * @return byte array containing login page html
+ */
+ protected byte[] getLoginHTML() {
+ String appUri = getApplication().getURL().toString();
+
+ try {
+ return ("<!DOCTYPE html PUBLIC \"-//W3C//DTD "
+ + "XHTML 1.0 Transitional//EN\" "
+ + "\"http://www.w3.org/TR/xhtml1/"
+ + "DTD/xhtml1-transitional.dtd\">\n" + "<html>"
+ + "<head><script type='text/javascript'>"
+ + "var setTarget = function() {" + "var uri = '"
+ + appUri
+ + "loginHandler"
+ + "'; var f = document.getElementById('loginf');"
+ + "document.forms[0].action = uri;document.forms[0].username.focus();};"
+ + ""
+ + "var styles = window.parent.document.styleSheets;"
+ + "for(var j = 0; j < styles.length; j++) {\n"
+ + "if(styles[j].href) {"
+ + "var stylesheet = document.createElement('link');\n"
+ + "stylesheet.setAttribute('rel', 'stylesheet');\n"
+ + "stylesheet.setAttribute('type', 'text/css');\n"
+ + "stylesheet.setAttribute('href', styles[j].href);\n"
+ + "document.getElementsByTagName('head')[0].appendChild(stylesheet);\n"
+ + "}"
+ + "}\n"
+ + "function submitOnEnter(e) { var keycode = e.keyCode || e.which;"
+ + " if (keycode == 13) {document.forms[0].submit();} } \n"
+ + "</script>"
+ + "</head><body onload='setTarget();' style='margin:0;padding:0; background:transparent;' class=\""
+ + ApplicationConnection.GENERATED_BODY_CLASSNAME
+ + "\">"
+ + "<div class='v-app v-app-loginpage' style=\"background:transparent;\">"
+ + "<iframe name='logintarget' style='width:0;height:0;"
+ + "border:0;margin:0;padding:0;'></iframe>"
+ + "<form id='loginf' target='logintarget' onkeypress=\"submitOnEnter(event)\" method=\"post\">"
+ + "<div>"
+ + usernameCaption
+ + "</div><div >"
+ + "<input class='v-textfield v-connector' style='display:block;' type='text' name='username'></div>"
+ + "<div>"
+ + passwordCaption
+ + "</div>"
+ + "<div><input class='v-textfield v-connector' style='display:block;' type='password' name='password'></div>"
+ + "<div><div onclick=\"document.forms[0].submit();\" tabindex=\"0\" class=\"v-button\" role=\"button\" ><span class=\"v-button-wrap\"><span class=\"v-button-caption\">"
+ + loginButtonCaption
+ + "</span></span></div></div></form></div>" + "</body></html>")
+ .getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not avalable", e);
+ }
+ }
+
+ @Override
+ public void attach() {
+ super.attach();
+ getApplication().addResource(loginPage);
+ getApplication().addRequestHandler(requestHandler);
+ iframe.setSource(loginPage);
+ }
+
+ @Override
+ public void detach() {
+ getApplication().removeResource(loginPage);
+ getApplication().removeRequestHandler(requestHandler);
+
+ super.detach();
+ }
+
+ /**
+ * This event is sent when login form is submitted.
+ */
+ public class LoginEvent extends Event {
+
+ private Map<String, String> params;
+
+ private LoginEvent(Map<String, String> params) {
+ super(LoginForm.this);
+ this.params = params;
+ }
+
+ /**
+ * Access method to form values by field names.
+ *
+ * @param name
+ * @return value in given field
+ */
+ public String getLoginParameter(String name) {
+ if (params.containsKey(name)) {
+ return params.get(name);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Login listener is a class capable to listen LoginEvents sent from
+ * LoginBox
+ */
+ public interface LoginListener extends Serializable {
+ /**
+ * This method is fired on each login form post.
+ *
+ * @param event
+ */
+ public void onLogin(LoginForm.LoginEvent event);
+ }
+
+ private static final Method ON_LOGIN_METHOD;
+
+ private static final String UNDEFINED_HEIGHT = "140px";
+ private static final String UNDEFINED_WIDTH = "200px";
+
+ static {
+ try {
+ ON_LOGIN_METHOD = LoginListener.class.getDeclaredMethod("onLogin",
+ new Class[] { LoginEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in LoginForm");
+ }
+ }
+
+ /**
+ * Adds LoginListener to handle login logic
+ *
+ * @param listener
+ */
+ public void addListener(LoginListener listener) {
+ addListener(LoginEvent.class, listener, ON_LOGIN_METHOD);
+ }
+
+ /**
+ * Removes LoginListener
+ *
+ * @param listener
+ */
+ public void removeListener(LoginListener listener) {
+ removeListener(LoginEvent.class, listener, ON_LOGIN_METHOD);
+ }
+
+ @Override
+ public void setWidth(float width, Unit unit) {
+ super.setWidth(width, unit);
+ if (iframe != null) {
+ if (width < 0) {
+ iframe.setWidth(UNDEFINED_WIDTH);
+ } else {
+ iframe.setWidth("100%");
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ if (iframe != null) {
+ if (height < 0) {
+ iframe.setHeight(UNDEFINED_HEIGHT);
+ } else {
+ iframe.setHeight("100%");
+ }
+ }
+ }
+
+ /**
+ * Returns the caption for the user name field.
+ *
+ * @return String
+ */
+ public String getUsernameCaption() {
+ return usernameCaption;
+ }
+
+ /**
+ * Sets the caption to show for the user name field. The caption cannot be
+ * changed after the form has been shown to the user.
+ *
+ * @param usernameCaption
+ */
+ public void setUsernameCaption(String usernameCaption) {
+ this.usernameCaption = usernameCaption;
+ }
+
+ /**
+ * Returns the caption for the password field.
+ *
+ * @return String
+ */
+ public String getPasswordCaption() {
+ return passwordCaption;
+ }
+
+ /**
+ * Sets the caption to show for the password field. The caption cannot be
+ * changed after the form has been shown to the user.
+ *
+ * @param passwordCaption
+ */
+ public void setPasswordCaption(String passwordCaption) {
+ this.passwordCaption = passwordCaption;
+ }
+
+ /**
+ * Returns the caption for the login button.
+ *
+ * @return String
+ */
+ public String getLoginButtonCaption() {
+ return loginButtonCaption;
+ }
+
+ /**
+ * Sets the caption (button text) to show for the login button. The caption
+ * cannot be changed after the form has been shown to the user.
+ *
+ * @param loginButtonCaption
+ */
+ public void setLoginButtonCaption(String loginButtonCaption) {
+ this.loginButtonCaption = loginButtonCaption;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/MenuBar.java b/server/src/com/vaadin/ui/MenuBar.java
new file mode 100644
index 0000000000..5b5dc13e20
--- /dev/null
+++ b/server/src/com/vaadin/ui/MenuBar.java
@@ -0,0 +1,890 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.menubar.VMenuBar;
+
+/**
+ * <p>
+ * A class representing a horizontal menu bar. The menu can contain MenuItem
+ * objects, which in turn can contain more MenuBars. These sub-level MenuBars
+ * are represented as vertical menu.
+ * </p>
+ */
+@SuppressWarnings("serial")
+public class MenuBar extends AbstractComponent implements Vaadin6Component {
+
+ // Items of the top-level menu
+ private final List<MenuItem> menuItems;
+
+ // Number of items in this menu
+ private int numberOfItems = 0;
+
+ private MenuItem moreItem;
+
+ private boolean openRootOnHover;
+
+ private boolean htmlContentAllowed;
+
+ /** Paint (serialise) the component for the client. */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute(VMenuBar.OPEN_ROOT_MENU_ON_HOWER, openRootOnHover);
+
+ if (isHtmlContentAllowed()) {
+ target.addAttribute(VMenuBar.HTML_CONTENT_ALLOWED, true);
+ }
+
+ target.startTag("options");
+
+ if (getWidth() > -1) {
+ target.startTag("moreItem");
+ target.addAttribute("text", moreItem.getText());
+ if (moreItem.getIcon() != null) {
+ target.addAttribute("icon", moreItem.getIcon());
+ }
+ target.endTag("moreItem");
+ }
+
+ target.endTag("options");
+ target.startTag("items");
+
+ // This generates the tree from the contents of the menu
+ for (MenuItem item : menuItems) {
+ paintItem(target, item);
+ }
+
+ target.endTag("items");
+ }
+
+ private void paintItem(PaintTarget target, MenuItem item)
+ throws PaintException {
+ if (!item.isVisible()) {
+ return;
+ }
+
+ target.startTag("item");
+
+ target.addAttribute("id", item.getId());
+
+ if (item.getStyleName() != null) {
+ target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_STYLE,
+ item.getStyleName());
+ }
+
+ if (item.isSeparator()) {
+ target.addAttribute("separator", true);
+ } else {
+ target.addAttribute("text", item.getText());
+
+ Command command = item.getCommand();
+ if (command != null) {
+ target.addAttribute("command", true);
+ }
+
+ Resource icon = item.getIcon();
+ if (icon != null) {
+ target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_ICON, icon);
+ }
+
+ if (!item.isEnabled()) {
+ target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DISABLED, true);
+ }
+
+ String description = item.getDescription();
+ if (description != null && description.length() > 0) {
+ target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DESCRIPTION,
+ description);
+ }
+ if (item.isCheckable()) {
+ // if the "checked" attribute is present (either true or false),
+ // the item is checkable
+ target.addAttribute(VMenuBar.ATTRIBUTE_CHECKED,
+ item.isChecked());
+ }
+ if (item.hasChildren()) {
+ for (MenuItem child : item.getChildren()) {
+ paintItem(target, child);
+ }
+ }
+
+ }
+
+ target.endTag("item");
+ }
+
+ /** Deserialize changes received from client. */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ Stack<MenuItem> items = new Stack<MenuItem>();
+ boolean found = false;
+
+ if (variables.containsKey("clickedId")) {
+
+ Integer clickedId = (Integer) variables.get("clickedId");
+ Iterator<MenuItem> itr = getItems().iterator();
+ while (itr.hasNext()) {
+ items.push(itr.next());
+ }
+
+ MenuItem tmpItem = null;
+
+ // Go through all the items in the menu
+ while (!found && !items.empty()) {
+ tmpItem = items.pop();
+ found = (clickedId.intValue() == tmpItem.getId());
+
+ if (tmpItem.hasChildren()) {
+ itr = tmpItem.getChildren().iterator();
+ while (itr.hasNext()) {
+ items.push(itr.next());
+ }
+ }
+
+ }// while
+
+ // If we got the clicked item, launch the command.
+ if (found && tmpItem.isEnabled()) {
+ if (tmpItem.isCheckable()) {
+ tmpItem.setChecked(!tmpItem.isChecked());
+ }
+ if (null != tmpItem.getCommand()) {
+ tmpItem.getCommand().menuSelected(tmpItem);
+ }
+ }
+ }// if
+ }// changeVariables
+
+ /**
+ * Constructs an empty, horizontal menu
+ */
+ public MenuBar() {
+ menuItems = new ArrayList<MenuItem>();
+ setMoreMenuItem(null);
+ }
+
+ /**
+ * Add a new item to the menu bar. Command can be null, but a caption must
+ * be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param command
+ * the command for the menu item
+ * @throws IllegalArgumentException
+ */
+ public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) {
+ return addItem(caption, null, command);
+ }
+
+ /**
+ * Add a new item to the menu bar. Icon and command can be null, but a
+ * caption must be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param icon
+ * the icon for the menu item
+ * @param command
+ * the command for the menu item
+ * @throws IllegalArgumentException
+ */
+ public MenuBar.MenuItem addItem(String caption, Resource icon,
+ MenuBar.Command command) {
+ if (caption == null) {
+ throw new IllegalArgumentException("caption cannot be null");
+ }
+ MenuItem newItem = new MenuItem(caption, icon, command);
+ menuItems.add(newItem);
+ requestRepaint();
+
+ return newItem;
+
+ }
+
+ /**
+ * Add an item before some item. If the given item does not exist the item
+ * is added at the end of the menu. Icon and command can be null, but a
+ * caption must be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param icon
+ * the icon for the menu item
+ * @param command
+ * the command for the menu item
+ * @param itemToAddBefore
+ * the item that will be after the new item
+ * @throws IllegalArgumentException
+ */
+ public MenuBar.MenuItem addItemBefore(String caption, Resource icon,
+ MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) {
+ if (caption == null) {
+ throw new IllegalArgumentException("caption cannot be null");
+ }
+
+ MenuItem newItem = new MenuItem(caption, icon, command);
+ if (menuItems.contains(itemToAddBefore)) {
+ int index = menuItems.indexOf(itemToAddBefore);
+ menuItems.add(index, newItem);
+
+ } else {
+ menuItems.add(newItem);
+ }
+
+ requestRepaint();
+
+ return newItem;
+ }
+
+ /**
+ * Returns a list with all the MenuItem objects in the menu bar
+ *
+ * @return a list containing the MenuItem objects in the menu bar
+ */
+ public List<MenuItem> getItems() {
+ return menuItems;
+ }
+
+ /**
+ * Remove first occurrence the specified item from the main menu
+ *
+ * @param item
+ * The item to be removed
+ */
+ public void removeItem(MenuBar.MenuItem item) {
+ if (item != null) {
+ menuItems.remove(item);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Empty the menu bar
+ */
+ public void removeItems() {
+ menuItems.clear();
+ requestRepaint();
+ }
+
+ /**
+ * Returns the size of the menu.
+ *
+ * @return The size of the menu
+ */
+ public int getSize() {
+ return menuItems.size();
+ }
+
+ /**
+ * Set the item that is used when collapsing the top level menu. All
+ * "overflowing" items will be added below this. The item command will be
+ * ignored. If set to null, the default item with a downwards arrow is used.
+ *
+ * The item command (if specified) is ignored.
+ *
+ * @param item
+ */
+ public void setMoreMenuItem(MenuItem item) {
+ if (item != null) {
+ moreItem = item;
+ } else {
+ moreItem = new MenuItem("", null, null);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Get the MenuItem used as the collapse menu item.
+ *
+ * @return
+ */
+ public MenuItem getMoreMenuItem() {
+ return moreItem;
+ }
+
+ /**
+ * Using this method menubar can be put into a special mode where top level
+ * menus opens without clicking on the menu, but automatically when mouse
+ * cursor is moved over the menu. In this mode the menu also closes itself
+ * if the mouse is moved out of the opened menu.
+ * <p>
+ * Note, that on touch devices the menu still opens on a click event.
+ *
+ * @param autoOpenTopLevelMenu
+ * true if menus should be opened without click, the default is
+ * false
+ */
+ public void setAutoOpen(boolean autoOpenTopLevelMenu) {
+ if (autoOpenTopLevelMenu != openRootOnHover) {
+ openRootOnHover = autoOpenTopLevelMenu;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Detects whether the menubar is in a mode where top level menus are
+ * automatically opened when the mouse cursor is moved over the menu.
+ * Normally root menu opens only by clicking on the menu. Submenus always
+ * open automatically.
+ *
+ * @return true if the root menus open without click, the default is false
+ */
+ public boolean isAutoOpen() {
+ return openRootOnHover;
+ }
+
+ /**
+ * Sets whether html is allowed in the item captions. If set to true, the
+ * captions are passed to the browser as html and the developer is
+ * responsible for ensuring no harmful html is used. If set to false, the
+ * content is passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * true if the captions are used as html, false if used as plain
+ * text
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ requestRepaint();
+ }
+
+ /**
+ * Checks whether item captions are interpreted as html or plain text.
+ *
+ * @return true if the captions are used as html, false if used as plain
+ * text
+ * @see #setHtmlContentAllowed(boolean)
+ */
+ public boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ /**
+ * This interface contains the layer for menu commands of the
+ * {@link com.vaadin.ui.MenuBar} class. It's method will fire when the user
+ * clicks on the containing {@link com.vaadin.ui.MenuBar.MenuItem}. The
+ * selected item is given as an argument.
+ */
+ public interface Command extends Serializable {
+ public void menuSelected(MenuBar.MenuItem selectedItem);
+ }
+
+ /**
+ * A composite class for menu items and sub-menus. You can set commands to
+ * be fired on user click by implementing the
+ * {@link com.vaadin.ui.MenuBar.Command} interface. You can also add
+ * multiple MenuItems to a MenuItem and create a sub-menu.
+ *
+ */
+ public class MenuItem implements Serializable {
+
+ /** Private members * */
+ private final int itsId;
+ private Command itsCommand;
+ private String itsText;
+ private List<MenuItem> itsChildren;
+ private Resource itsIcon;
+ private MenuItem itsParent;
+ private boolean enabled = true;
+ private boolean visible = true;
+ private boolean isSeparator = false;
+ private String styleName;
+ private String description;
+ private boolean checkable = false;
+ private boolean checked = false;
+
+ /**
+ * Constructs a new menu item that can optionally have an icon and a
+ * command associated with it. Icon and command can be null, but a
+ * caption must be given.
+ *
+ * @param text
+ * The text associated with the command
+ * @param command
+ * The command to be fired
+ * @throws IllegalArgumentException
+ */
+ public MenuItem(String caption, Resource icon, MenuBar.Command command) {
+ if (caption == null) {
+ throw new IllegalArgumentException("caption cannot be null");
+ }
+ itsId = ++numberOfItems;
+ itsText = caption;
+ itsIcon = icon;
+ itsCommand = command;
+ }
+
+ /**
+ * Checks if the item has children (if it is a sub-menu).
+ *
+ * @return True if this item has children
+ */
+ public boolean hasChildren() {
+ return !isSeparator() && itsChildren != null;
+ }
+
+ /**
+ * Adds a separator to this menu. A separator is a way to visually group
+ * items in a menu, to make it easier for users to find what they are
+ * looking for in the menu.
+ *
+ * @author Jouni Koivuviita / Vaadin Ltd.
+ * @since 6.2.0
+ */
+ public MenuBar.MenuItem addSeparator() {
+ MenuItem item = addItem("", null, null);
+ item.setSeparator(true);
+ return item;
+ }
+
+ public MenuBar.MenuItem addSeparatorBefore(MenuItem itemToAddBefore) {
+ MenuItem item = addItemBefore("", null, null, itemToAddBefore);
+ item.setSeparator(true);
+ return item;
+ }
+
+ /**
+ * Add a new item inside this item, thus creating a sub-menu. Command
+ * can be null, but a caption must be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param command
+ * the command for the menu item
+ */
+ public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) {
+ return addItem(caption, null, command);
+ }
+
+ /**
+ * Add a new item inside this item, thus creating a sub-menu. Icon and
+ * command can be null, but a caption must be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param icon
+ * the icon for the menu item
+ * @param command
+ * the command for the menu item
+ * @throws IllegalStateException
+ * If the item is checkable and thus cannot have children.
+ */
+ public MenuBar.MenuItem addItem(String caption, Resource icon,
+ MenuBar.Command command) throws IllegalStateException {
+ if (isSeparator()) {
+ throw new UnsupportedOperationException(
+ "Cannot add items to a separator");
+ }
+ if (isCheckable()) {
+ throw new IllegalStateException(
+ "A checkable item cannot have children");
+ }
+ if (caption == null) {
+ throw new IllegalArgumentException("Caption cannot be null");
+ }
+
+ if (itsChildren == null) {
+ itsChildren = new ArrayList<MenuItem>();
+ }
+
+ MenuItem newItem = new MenuItem(caption, icon, command);
+
+ // The only place where the parent is set
+ newItem.setParent(this);
+ itsChildren.add(newItem);
+
+ requestRepaint();
+
+ return newItem;
+ }
+
+ /**
+ * Add an item before some item. If the given item does not exist the
+ * item is added at the end of the menu. Icon and command can be null,
+ * but a caption must be given.
+ *
+ * @param caption
+ * the text for the menu item
+ * @param icon
+ * the icon for the menu item
+ * @param command
+ * the command for the menu item
+ * @param itemToAddBefore
+ * the item that will be after the new item
+ * @throws IllegalStateException
+ * If the item is checkable and thus cannot have children.
+ */
+ public MenuBar.MenuItem addItemBefore(String caption, Resource icon,
+ MenuBar.Command command, MenuBar.MenuItem itemToAddBefore)
+ throws IllegalStateException {
+ if (isCheckable()) {
+ throw new IllegalStateException(
+ "A checkable item cannot have children");
+ }
+ MenuItem newItem = null;
+
+ if (hasChildren() && itsChildren.contains(itemToAddBefore)) {
+ int index = itsChildren.indexOf(itemToAddBefore);
+ newItem = new MenuItem(caption, icon, command);
+ newItem.setParent(this);
+ itsChildren.add(index, newItem);
+ } else {
+ newItem = addItem(caption, icon, command);
+ }
+
+ requestRepaint();
+
+ return newItem;
+ }
+
+ /**
+ * For the associated command.
+ *
+ * @return The associated command, or null if there is none
+ */
+ public Command getCommand() {
+ return itsCommand;
+ }
+
+ /**
+ * Gets the objects icon.
+ *
+ * @return The icon of the item, null if the item doesn't have an icon
+ */
+ public Resource getIcon() {
+ return itsIcon;
+ }
+
+ /**
+ * For the containing item. This will return null if the item is in the
+ * top-level menu bar.
+ *
+ * @return The containing {@link com.vaadin.ui.MenuBar.MenuItem} , or
+ * null if there is none
+ */
+ public MenuBar.MenuItem getParent() {
+ return itsParent;
+ }
+
+ /**
+ * This will return the children of this item or null if there are none.
+ *
+ * @return List of children items, or null if there are none
+ */
+ public List<MenuItem> getChildren() {
+ return itsChildren;
+ }
+
+ /**
+ * Gets the objects text
+ *
+ * @return The text
+ */
+ public java.lang.String getText() {
+ return itsText;
+ }
+
+ /**
+ * Returns the number of children.
+ *
+ * @return The number of child items
+ */
+ public int getSize() {
+ if (itsChildren != null) {
+ return itsChildren.size();
+ }
+ return -1;
+ }
+
+ /**
+ * Get the unique identifier for this item.
+ *
+ * @return The id of this item
+ */
+ public int getId() {
+ return itsId;
+ }
+
+ /**
+ * Set the command for this item. Set null to remove.
+ *
+ * @param command
+ * The MenuCommand of this item
+ */
+ public void setCommand(MenuBar.Command command) {
+ itsCommand = command;
+ }
+
+ /**
+ * Sets the icon. Set null to remove.
+ *
+ * @param icon
+ * The icon for this item
+ */
+ public void setIcon(Resource icon) {
+ itsIcon = icon;
+ requestRepaint();
+ }
+
+ /**
+ * Set the text of this object.
+ *
+ * @param text
+ * Text for this object
+ */
+ public void setText(java.lang.String text) {
+ if (text != null) {
+ itsText = text;
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Remove the first occurrence of the item.
+ *
+ * @param item
+ * The item to be removed
+ */
+ public void removeChild(MenuBar.MenuItem item) {
+ if (item != null && itsChildren != null) {
+ itsChildren.remove(item);
+ if (itsChildren.isEmpty()) {
+ itsChildren = null;
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Empty the list of children items.
+ */
+ public void removeChildren() {
+ if (itsChildren != null) {
+ itsChildren.clear();
+ itsChildren = null;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Set the parent of this item. This is called by the addItem method.
+ *
+ * @param parent
+ * The parent item
+ */
+ protected void setParent(MenuBar.MenuItem parent) {
+ itsParent = parent;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ requestRepaint();
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ requestRepaint();
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ private void setSeparator(boolean isSeparator) {
+ this.isSeparator = isSeparator;
+ requestRepaint();
+ }
+
+ public boolean isSeparator() {
+ return isSeparator;
+ }
+
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ requestRepaint();
+ }
+
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets the items's description. See {@link #getDescription()} for more
+ * information on what the description is. This method will trigger a
+ * {@link RepaintRequestEvent}.
+ *
+ * @param description
+ * the new description string for the component.
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ requestRepaint();
+ }
+
+ /**
+ * <p>
+ * Gets the items's description. The description can be used to briefly
+ * describe the state of the item to the user. The description string
+ * may contain certain XML tags:
+ * </p>
+ *
+ * <p>
+ * <table border=1>
+ * <tr>
+ * <td width=120><b>Tag</b></td>
+ * <td width=120><b>Description</b></td>
+ * <td width=120><b>Example</b></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;b></td>
+ * <td>bold</td>
+ * <td><b>bold text</b></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;i></td>
+ * <td>italic</td>
+ * <td><i>italic text</i></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;u></td>
+ * <td>underlined</td>
+ * <td><u>underlined text</u></td>
+ * </tr>
+ * <tr>
+ * <td>&lt;br></td>
+ * <td>linebreak</td>
+ * <td>N/A</td>
+ * </tr>
+ * <tr>
+ * <td>&lt;ul><br>
+ * &lt;li>item1<br>
+ * &lt;li>item1<br>
+ * &lt;/ul></td>
+ * <td>item list</td>
+ * <td>
+ * <ul>
+ * <li>item1
+ * <li>item2
+ * </ul>
+ * </td>
+ * </tr>
+ * </table>
+ * </p>
+ *
+ * <p>
+ * These tags may be nested.
+ * </p>
+ *
+ * @return item's description <code>String</code>
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Gets the checkable state of the item - whether the item has checked
+ * and unchecked states. If an item is checkable its checked state (as
+ * returned by {@link #isChecked()}) is indicated in the UI.
+ *
+ * <p>
+ * An item is not checkable by default.
+ * </p>
+ *
+ * @return true if the item is checkable, false otherwise
+ * @since 6.6.2
+ */
+ public boolean isCheckable() {
+ return checkable;
+ }
+
+ /**
+ * Sets the checkable state of the item. If an item is checkable its
+ * checked state (as returned by {@link #isChecked()}) is indicated in
+ * the UI.
+ *
+ * <p>
+ * An item is not checkable by default.
+ * </p>
+ *
+ * <p>
+ * Items with sub items cannot be checkable.
+ * </p>
+ *
+ * @param checkable
+ * true if the item should be checkable, false otherwise
+ * @throws IllegalStateException
+ * If the item has children
+ * @since 6.6.2
+ */
+ public void setCheckable(boolean checkable)
+ throws IllegalStateException {
+ if (hasChildren()) {
+ throw new IllegalStateException(
+ "A menu item with children cannot be checkable");
+ }
+ this.checkable = checkable;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the checked state of the item (checked or unchecked). Only used
+ * if the item is checkable (as indicated by {@link #isCheckable()}).
+ * The checked state is indicated in the UI with the item, if the item
+ * is checkable.
+ *
+ * <p>
+ * An item is not checked by default.
+ * </p>
+ *
+ * <p>
+ * The CSS style corresponding to the checked state is "-checked".
+ * </p>
+ *
+ * @return true if the item is checked, false otherwise
+ * @since 6.6.2
+ */
+ public boolean isChecked() {
+ return checked;
+ }
+
+ /**
+ * Sets the checked state of the item. Only used if the item is
+ * checkable (indicated by {@link #isCheckable()}). The checked state is
+ * indicated in the UI with the item, if the item is checkable.
+ *
+ * <p>
+ * An item is not checked by default.
+ * </p>
+ *
+ * <p>
+ * The CSS style corresponding to the checked state is "-checked".
+ * </p>
+ *
+ * @return true if the item is checked, false otherwise
+ * @since 6.6.2
+ */
+ public void setChecked(boolean checked) {
+ this.checked = checked;
+ requestRepaint();
+ }
+
+ }// class MenuItem
+
+}// class MenuBar
diff --git a/server/src/com/vaadin/ui/NativeButton.java b/server/src/com/vaadin/ui/NativeButton.java
new file mode 100644
index 0000000000..6eb4379261
--- /dev/null
+++ b/server/src/com/vaadin/ui/NativeButton.java
@@ -0,0 +1,21 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+@SuppressWarnings("serial")
+public class NativeButton extends Button {
+
+ public NativeButton() {
+ super();
+ }
+
+ public NativeButton(String caption) {
+ super(caption);
+ }
+
+ public NativeButton(String caption, ClickListener listener) {
+ super(caption, listener);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/NativeSelect.java b/server/src/com/vaadin/ui/NativeSelect.java
new file mode 100644
index 0000000000..1f85f57c97
--- /dev/null
+++ b/server/src/com/vaadin/ui/NativeSelect.java
@@ -0,0 +1,91 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * This is a simple drop-down select without, for instance, support for
+ * multiselect, new items, lazyloading, and other advanced features. Sometimes
+ * "native" select without all the bells-and-whistles of the ComboBox is a
+ * better choice.
+ */
+@SuppressWarnings("serial")
+public class NativeSelect extends AbstractSelect {
+
+ // width in characters, mimics TextField
+ private int columns = 0;
+
+ public NativeSelect() {
+ super();
+ }
+
+ public NativeSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public NativeSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public NativeSelect(String caption) {
+ super(caption);
+ }
+
+ /**
+ * Sets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ *
+ * @param columns
+ * the number of columns to set.
+ */
+ public void setColumns(int columns) {
+ if (columns < 0) {
+ columns = 0;
+ }
+ if (this.columns != columns) {
+ this.columns = columns;
+ requestRepaint();
+ }
+ }
+
+ public int getColumns() {
+ return columns;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute("type", "native");
+ // Adds the number of columns
+ if (columns != 0) {
+ target.addAttribute("cols", columns);
+ }
+
+ super.paintContent(target);
+ }
+
+ @Override
+ public void setMultiSelect(boolean multiSelect)
+ throws UnsupportedOperationException {
+ if (multiSelect == true) {
+ throw new UnsupportedOperationException("Multiselect not supported");
+ }
+ }
+
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions == true) {
+ throw new UnsupportedOperationException(
+ "newItemsAllowed not supported");
+ }
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Notification.java b/server/src/com/vaadin/ui/Notification.java
new file mode 100644
index 0000000000..502e5ff788
--- /dev/null
+++ b/server/src/com/vaadin/ui/Notification.java
@@ -0,0 +1,367 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.terminal.Page;
+import com.vaadin.terminal.Resource;
+
+/**
+ * A notification message, used to display temporary messages to the user - for
+ * example "Document saved", or "Save failed".
+ * <p>
+ * The notification message can consist of several parts: caption, description
+ * and icon. It is usually used with only caption - one should be wary of
+ * filling the notification with too much information.
+ * </p>
+ * <p>
+ * The notification message tries to be as unobtrusive as possible, while still
+ * drawing needed attention. There are several basic types of messages that can
+ * be used in different situations:
+ * <ul>
+ * <li>TYPE_HUMANIZED_MESSAGE fades away quickly as soon as the user uses the
+ * mouse or types something. It can be used to show fairly unimportant messages,
+ * such as feedback that an operation succeeded ("Document Saved") - the kind of
+ * messages the user ignores once the application is familiar.</li>
+ * <li>TYPE_WARNING_MESSAGE is shown for a short while after the user uses the
+ * mouse or types something. It's default style is also more noticeable than the
+ * humanized message. It can be used for messages that do not contain a lot of
+ * important information, but should be noticed by the user. Despite the name,
+ * it does not have to be a warning, but can be used instead of the humanized
+ * message whenever you want to make the message a little more noticeable.</li>
+ * <li>TYPE_ERROR_MESSAGE requires to user to click it before disappearing, and
+ * can be used for critical messages.</li>
+ * <li>TYPE_TRAY_NOTIFICATION is shown for a while in the lower left corner of
+ * the window, and can be used for "convenience notifications" that do not have
+ * to be noticed immediately, and should not interfere with the current task -
+ * for instance to show "You have a new message in your inbox" while the user is
+ * working in some other area of the application.</li>
+ * </ul>
+ * </p>
+ * <p>
+ * In addition to the basic pre-configured types, a Notification can also be
+ * configured to show up in a custom position, for a specified time (or until
+ * clicked), and with a custom stylename. An icon can also be added.
+ * </p>
+ *
+ */
+public class Notification implements Serializable {
+ public static final int TYPE_HUMANIZED_MESSAGE = 1;
+ public static final int TYPE_WARNING_MESSAGE = 2;
+ public static final int TYPE_ERROR_MESSAGE = 3;
+ public static final int TYPE_TRAY_NOTIFICATION = 4;
+
+ public static final int POSITION_CENTERED = 1;
+ public static final int POSITION_CENTERED_TOP = 2;
+ public static final int POSITION_CENTERED_BOTTOM = 3;
+ public static final int POSITION_TOP_LEFT = 4;
+ public static final int POSITION_TOP_RIGHT = 5;
+ public static final int POSITION_BOTTOM_LEFT = 6;
+ public static final int POSITION_BOTTOM_RIGHT = 7;
+
+ public static final int DELAY_FOREVER = -1;
+ public static final int DELAY_NONE = 0;
+
+ private String caption;
+ private String description;
+ private Resource icon;
+ private int position = POSITION_CENTERED;
+ private int delayMsec = 0;
+ private String styleName;
+ private boolean htmlContentAllowed;
+
+ /**
+ * Creates a "humanized" notification message.
+ *
+ * The caption is rendered as plain text with HTML automatically escaped.
+ *
+ * @param caption
+ * The message to show
+ */
+ public Notification(String caption) {
+ this(caption, null, TYPE_HUMANIZED_MESSAGE);
+ }
+
+ /**
+ * Creates a notification message of the specified type.
+ *
+ * The caption is rendered as plain text with HTML automatically escaped.
+ *
+ * @param caption
+ * The message to show
+ * @param type
+ * The type of message
+ */
+ public Notification(String caption, int type) {
+ this(caption, null, type);
+ }
+
+ /**
+ * Creates a "humanized" notification message with a bigger caption and
+ * smaller description.
+ *
+ * The caption and description are rendered as plain text with HTML
+ * automatically escaped.
+ *
+ * @param caption
+ * The message caption
+ * @param description
+ * The message description
+ */
+ public Notification(String caption, String description) {
+ this(caption, description, TYPE_HUMANIZED_MESSAGE);
+ }
+
+ /**
+ * Creates a notification message of the specified type, with a bigger
+ * caption and smaller description.
+ *
+ * The caption and description are rendered as plain text with HTML
+ * automatically escaped.
+ *
+ * @param caption
+ * The message caption
+ * @param description
+ * The message description
+ * @param type
+ * The type of message
+ */
+ public Notification(String caption, String description, int type) {
+ this(caption, description, type, false);
+ }
+
+ /**
+ * Creates a notification message of the specified type, with a bigger
+ * caption and smaller description.
+ *
+ * Care should be taken to to avoid XSS vulnerabilities if html is allowed.
+ *
+ * @param caption
+ * The message caption
+ * @param description
+ * The message description
+ * @param type
+ * The type of message
+ * @param htmlContentAllowed
+ * Whether html in the caption and description should be
+ * displayed as html or as plain text
+ */
+ public Notification(String caption, String description, int type,
+ boolean htmlContentAllowed) {
+ this.caption = caption;
+ this.description = description;
+ this.htmlContentAllowed = htmlContentAllowed;
+ setType(type);
+ }
+
+ private void setType(int type) {
+ switch (type) {
+ case TYPE_WARNING_MESSAGE:
+ delayMsec = 1500;
+ styleName = "warning";
+ break;
+ case TYPE_ERROR_MESSAGE:
+ delayMsec = -1;
+ styleName = "error";
+ break;
+ case TYPE_TRAY_NOTIFICATION:
+ delayMsec = 3000;
+ position = POSITION_BOTTOM_RIGHT;
+ styleName = "tray";
+
+ case TYPE_HUMANIZED_MESSAGE:
+ default:
+ break;
+ }
+
+ }
+
+ /**
+ * Gets the caption part of the notification message.
+ *
+ * @return The message caption
+ */
+ public String getCaption() {
+ return caption;
+ }
+
+ /**
+ * Sets the caption part of the notification message
+ *
+ * @param caption
+ * The message caption
+ */
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ /**
+ * Gets the description part of the notification message.
+ *
+ * @return The message description.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the description part of the notification message.
+ *
+ * @param description
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Gets the position of the notification message.
+ *
+ * @return The position
+ */
+ public int getPosition() {
+ return position;
+ }
+
+ /**
+ * Sets the position of the notification message.
+ *
+ * @param position
+ * The desired notification position
+ */
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ /**
+ * Gets the icon part of the notification message.
+ *
+ * @return The message icon
+ */
+ public Resource getIcon() {
+ return icon;
+ }
+
+ /**
+ * Sets the icon part of the notification message.
+ *
+ * @param icon
+ * The desired message icon
+ */
+ public void setIcon(Resource icon) {
+ this.icon = icon;
+ }
+
+ /**
+ * Gets the delay before the notification disappears.
+ *
+ * @return the delay in msec, -1 indicates the message has to be clicked.
+ */
+ public int getDelayMsec() {
+ return delayMsec;
+ }
+
+ /**
+ * Sets the delay before the notification disappears.
+ *
+ * @param delayMsec
+ * the desired delay in msec, -1 to require the user to click the
+ * message
+ */
+ public void setDelayMsec(int delayMsec) {
+ this.delayMsec = delayMsec;
+ }
+
+ /**
+ * Sets the style name for the notification message.
+ *
+ * @param styleName
+ * The desired style name.
+ */
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ }
+
+ /**
+ * Gets the style name for the notification message.
+ *
+ * @return
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * Sets whether html is allowed in the caption and description. If set to
+ * true, the texts are passed to the browser as html and the developer is
+ * responsible for ensuring no harmful html is used. If set to false, the
+ * texts are passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * true if the texts are used as html, false if used as plain
+ * text
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ }
+
+ /**
+ * Checks whether caption and description are interpreted as html or plain
+ * text.
+ *
+ * @return true if the texts are used as html, false if used as plain text
+ * @see #setHtmlContentAllowed(boolean)
+ */
+ public boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ /**
+ * Shows this notification on a Page.
+ *
+ * @param page
+ * The page on which the notification should be shown
+ */
+ public void show(Page page) {
+ // TODO Can avoid deprecated API when Notification extends Extension
+ page.showNotification(this);
+ }
+
+ /**
+ * Shows a notification message on the middle of the current page. The
+ * message automatically disappears ("humanized message").
+ *
+ * The caption is rendered as plain text with HTML automatically escaped.
+ *
+ * @see #Notification(String)
+ * @see #show(Page)
+ *
+ * @param caption
+ * The message
+ */
+ public static void show(String caption) {
+ new Notification(caption).show(Page.getCurrent());
+ }
+
+ /**
+ * Shows a notification message the current page. The position and behavior
+ * of the message depends on the type, which is one of the basic types
+ * defined in {@link Notification}, for instance
+ * Notification.TYPE_WARNING_MESSAGE.
+ *
+ * The caption is rendered as plain text with HTML automatically escaped.
+ *
+ * @see #Notification(String, int)
+ * @see #show(Page)
+ *
+ * @param caption
+ * The message
+ * @param type
+ * The message type
+ */
+ public static void show(String caption, int type) {
+ new Notification(caption, type).show(Page.getCurrent());
+ }
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/OptionGroup.java b/server/src/com/vaadin/ui/OptionGroup.java
new file mode 100644
index 0000000000..e3bcdd61b7
--- /dev/null
+++ b/server/src/com/vaadin/ui/OptionGroup.java
@@ -0,0 +1,203 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroup;
+
+/**
+ * Configures select to be used as an option group.
+ */
+@SuppressWarnings("serial")
+public class OptionGroup extends AbstractSelect implements
+ FieldEvents.BlurNotifier, FieldEvents.FocusNotifier {
+
+ private Set<Object> disabledItemIds = new HashSet<Object>();
+ private boolean htmlContentAllowed = false;
+
+ public OptionGroup() {
+ super();
+ }
+
+ public OptionGroup(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public OptionGroup(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public OptionGroup(String caption) {
+ super(caption);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute("type", "optiongroup");
+ if (isHtmlContentAllowed()) {
+ target.addAttribute(VOptionGroup.HTML_CONTENT_ALLOWED, true);
+ }
+ super.paintContent(target);
+ }
+
+ @Override
+ protected void paintItem(PaintTarget target, Object itemId)
+ throws PaintException {
+ super.paintItem(target, itemId);
+ if (!isItemEnabled(itemId)) {
+ target.addAttribute(VOptionGroup.ATTRIBUTE_OPTION_DISABLED, true);
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ super.changeVariables(source, variables);
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+
+ }
+
+ @Override
+ protected void setValue(Object newValue, boolean repaintIsNotNeeded) {
+ if (repaintIsNotNeeded) {
+ /*
+ * Check that value from changeVariables() doesn't contain unallowed
+ * selections: In the multi select mode, the user has selected or
+ * deselected a disabled item. In the single select mode, the user
+ * has selected a disabled item.
+ */
+ if (isMultiSelect()) {
+ Set<?> currentValueSet = (Set<?>) getValue();
+ Set<?> newValueSet = (Set<?>) newValue;
+ for (Object itemId : currentValueSet) {
+ if (!isItemEnabled(itemId) && !newValueSet.contains(itemId)) {
+ requestRepaint();
+ return;
+ }
+ }
+ for (Object itemId : newValueSet) {
+ if (!isItemEnabled(itemId)
+ && !currentValueSet.contains(itemId)) {
+ requestRepaint();
+ return;
+ }
+ }
+ } else {
+ if (newValue == null) {
+ newValue = getNullSelectionItemId();
+ }
+ if (!isItemEnabled(newValue)) {
+ requestRepaint();
+ return;
+ }
+ }
+ }
+ super.setValue(newValue, repaintIsNotNeeded);
+ }
+
+ /**
+ * Sets an item disabled or enabled. In the multiselect mode, a disabled
+ * item cannot be selected or deselected by the user. In the single
+ * selection mode, a disable item cannot be selected.
+ *
+ * However, programmatical selection or deselection of an disable item is
+ * possible. By default, items are enabled.
+ *
+ * @param itemId
+ * the id of the item to be disabled or enabled
+ * @param enabled
+ * if true the item is enabled, otherwise the item is disabled
+ */
+ public void setItemEnabled(Object itemId, boolean enabled) {
+ if (itemId != null) {
+ if (enabled) {
+ disabledItemIds.remove(itemId);
+ } else {
+ disabledItemIds.add(itemId);
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Returns true if the item is enabled.
+ *
+ * @param itemId
+ * the id of the item to be checked
+ * @return true if the item is enabled, false otherwise
+ * @see #setItemEnabled(Object, boolean)
+ */
+ public boolean isItemEnabled(Object itemId) {
+ if (itemId != null) {
+ return !disabledItemIds.contains(itemId);
+ }
+ return true;
+ }
+
+ /**
+ * Sets whether html is allowed in the item captions. If set to true, the
+ * captions are passed to the browser as html and the developer is
+ * responsible for ensuring no harmful html is used. If set to false, the
+ * content is passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * true if the captions are used as html, false if used as plain
+ * text
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ requestRepaint();
+ }
+
+ /**
+ * Checks whether captions are interpreted as html or plain text.
+ *
+ * @return true if the captions are used as html, false if used as plain
+ * text
+ * @see #setHtmlContentAllowed(boolean)
+ */
+ public boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+}
diff --git a/server/src/com/vaadin/ui/Panel.java b/server/src/com/vaadin/ui/Panel.java
new file mode 100644
index 0000000000..3c26b73f09
--- /dev/null
+++ b/server/src/com/vaadin/ui/Panel.java
@@ -0,0 +1,486 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.ActionManager;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.MouseEvents.ClickListener;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.panel.PanelServerRpc;
+import com.vaadin.shared.ui.panel.PanelState;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Scrollable;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.ClickEventHandler;
+import com.vaadin.ui.Component.Focusable;
+
+/**
+ * Panel - a simple single component container.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Panel extends AbstractComponentContainer implements Scrollable,
+ ComponentContainer.ComponentAttachListener,
+ ComponentContainer.ComponentDetachListener, Action.Notifier, Focusable,
+ Vaadin6Component {
+
+ /**
+ * Content of the panel.
+ */
+ private ComponentContainer content;
+
+ /**
+ * Keeps track of the Actions added to this component, and manages the
+ * painting and handling as well.
+ */
+ protected ActionManager actionManager;
+
+ private PanelServerRpc rpc = new PanelServerRpc() {
+ @Override
+ public void click(MouseEventDetails mouseDetails) {
+ fireEvent(new ClickEvent(Panel.this, mouseDetails));
+ }
+ };
+
+ /**
+ * Creates a new empty panel. A VerticalLayout is used as content.
+ */
+ public Panel() {
+ this((ComponentContainer) null);
+ }
+
+ /**
+ * Creates a new empty panel which contains the given content. The content
+ * cannot be null.
+ *
+ * @param content
+ * the content for the panel.
+ */
+ public Panel(ComponentContainer content) {
+ registerRpc(rpc);
+ setContent(content);
+ setWidth(100, Unit.PERCENTAGE);
+ getState().setTabIndex(-1);
+ }
+
+ /**
+ * Creates a new empty panel with caption. Default layout is used.
+ *
+ * @param caption
+ * the caption used in the panel (HTML/XHTML).
+ */
+ public Panel(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new empty panel with the given caption and content.
+ *
+ * @param caption
+ * the caption of the panel (HTML/XHTML).
+ * @param content
+ * the content used in the panel.
+ */
+ public Panel(String caption, ComponentContainer content) {
+ this(content);
+ setCaption(caption);
+ }
+
+ /**
+ * Sets the caption of the panel.
+ *
+ * Note that the caption is interpreted as HTML/XHTML and therefore care
+ * should be taken not to enable HTML injection and XSS attacks using panel
+ * captions. This behavior may change in future versions.
+ *
+ * @see AbstractComponent#setCaption(String)
+ */
+ @Override
+ public void setCaption(String caption) {
+ super.setCaption(caption);
+ }
+
+ /**
+ * Returns the content of the Panel.
+ *
+ * @return
+ */
+ public ComponentContainer getContent() {
+ return content;
+ }
+
+ /**
+ *
+ * Set the content of the Panel. If null is given as the new content then a
+ * layout is automatically created and set as the content.
+ *
+ * @param content
+ * The new content
+ */
+ public void setContent(ComponentContainer newContent) {
+
+ // If the content is null we create the default content
+ if (newContent == null) {
+ newContent = createDefaultContent();
+ }
+
+ // if (newContent == null) {
+ // throw new IllegalArgumentException("Content cannot be null");
+ // }
+
+ if (newContent == content) {
+ // don't set the same content twice
+ return;
+ }
+
+ // detach old content if present
+ if (content != null) {
+ content.setParent(null);
+ content.removeListener((ComponentContainer.ComponentAttachListener) this);
+ content.removeListener((ComponentContainer.ComponentDetachListener) this);
+ }
+
+ // Sets the panel to be parent for the content
+ newContent.setParent(this);
+
+ // Sets the new content
+ content = newContent;
+
+ // Adds the event listeners for new content
+ newContent
+ .addListener((ComponentContainer.ComponentAttachListener) this);
+ newContent
+ .addListener((ComponentContainer.ComponentDetachListener) this);
+
+ content = newContent;
+ requestRepaint();
+ }
+
+ /**
+ * Create a ComponentContainer which is added by default to the Panel if
+ * user does not specify any content.
+ *
+ * @return
+ */
+ private ComponentContainer createDefaultContent() {
+ VerticalLayout layout = new VerticalLayout();
+ // Force margins by default
+ layout.setMargin(true);
+ return layout;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.Vaadin6Component#paintContent(com.vaadin.terminal
+ * .PaintTarget)
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (actionManager != null) {
+ actionManager.paintActions(null, target);
+ }
+ }
+
+ /**
+ * Adds the component into this container.
+ *
+ * @param c
+ * the component to be added.
+ * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component)
+ */
+ @Override
+ public void addComponent(Component c) {
+ content.addComponent(c);
+ // No repaint request is made as we except the underlying container to
+ // request repaints
+ }
+
+ /**
+ * Removes the component from this container.
+ *
+ * @param c
+ * The component to be removed.
+ * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component)
+ */
+ @Override
+ public void removeComponent(Component c) {
+ content.removeComponent(c);
+ // No repaint request is made as we except the underlying container to
+ // request repaints
+ }
+
+ /**
+ * Gets the component container iterator for going through all the
+ * components in the container.
+ *
+ * @return the Iterator of the components inside the container.
+ * @see com.vaadin.ui.ComponentContainer#getComponentIterator()
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return Collections.singleton((Component) content).iterator();
+ }
+
+ /**
+ * Called when one or more variables handled by the implementing class are
+ * changed.
+ *
+ * @see com.vaadin.terminal.VariableOwner#changeVariables(Object, Map)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Get new size
+ final Integer newWidth = (Integer) variables.get("width");
+ final Integer newHeight = (Integer) variables.get("height");
+ if (newWidth != null && newWidth.intValue() != getWidth()) {
+ setWidth(newWidth.intValue(), UNITS_PIXELS);
+ }
+ if (newHeight != null && newHeight.intValue() != getHeight()) {
+ setHeight(newHeight.intValue(), UNITS_PIXELS);
+ }
+
+ // Scrolling
+ final Integer newScrollX = (Integer) variables.get("scrollLeft");
+ final Integer newScrollY = (Integer) variables.get("scrollTop");
+ if (newScrollX != null && newScrollX.intValue() != getScrollLeft()) {
+ // set internally, not to fire request repaint
+ getState().setScrollLeft(newScrollX.intValue());
+ }
+ if (newScrollY != null && newScrollY.intValue() != getScrollTop()) {
+ // set internally, not to fire request repaint
+ getState().setScrollTop(newScrollY.intValue());
+ }
+
+ // Actions
+ if (actionManager != null) {
+ actionManager.handleActions(variables, this);
+ }
+
+ }
+
+ /* Scrolling functionality */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Scrollable#setScrollable(boolean)
+ */
+ @Override
+ public int getScrollLeft() {
+ return getState().getScrollLeft();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Scrollable#setScrollable(boolean)
+ */
+ @Override
+ public int getScrollTop() {
+ return getState().getScrollTop();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Scrollable#setScrollLeft(int)
+ */
+ @Override
+ public void setScrollLeft(int scrollLeft) {
+ if (scrollLeft < 0) {
+ throw new IllegalArgumentException(
+ "Scroll offset must be at least 0");
+ }
+ getState().setScrollLeft(scrollLeft);
+ requestRepaint();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.Scrollable#setScrollTop(int)
+ */
+ @Override
+ public void setScrollTop(int scrollTop) {
+ if (scrollTop < 0) {
+ throw new IllegalArgumentException(
+ "Scroll offset must be at least 0");
+ }
+ getState().setScrollTop(scrollTop);
+ requestRepaint();
+ }
+
+ /* Documented in superclass */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ content.replaceComponent(oldComponent, newComponent);
+ }
+
+ /**
+ * A new component is attached to container.
+ *
+ * @see com.vaadin.ui.ComponentContainer.ComponentAttachListener#componentAttachedToContainer(com.vaadin.ui.ComponentContainer.ComponentAttachEvent)
+ */
+ @Override
+ public void componentAttachedToContainer(ComponentAttachEvent event) {
+ if (event.getContainer() == content) {
+ fireComponentAttachEvent(event.getAttachedComponent());
+ }
+ }
+
+ /**
+ * A component has been detached from container.
+ *
+ * @see com.vaadin.ui.ComponentContainer.ComponentDetachListener#componentDetachedFromContainer(com.vaadin.ui.ComponentContainer.ComponentDetachEvent)
+ */
+ @Override
+ public void componentDetachedFromContainer(ComponentDetachEvent event) {
+ if (event.getContainer() == content) {
+ fireComponentDetachEvent(event.getDetachedComponent());
+ }
+ }
+
+ /**
+ * Removes all components from this container.
+ *
+ * @see com.vaadin.ui.ComponentContainer#removeAllComponents()
+ */
+ @Override
+ public void removeAllComponents() {
+ content.removeAllComponents();
+ }
+
+ /*
+ * ACTIONS
+ */
+ @Override
+ protected ActionManager getActionManager() {
+ if (actionManager == null) {
+ actionManager = new ActionManager(this);
+ }
+ return actionManager;
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void addAction(
+ T action) {
+ getActionManager().addAction(action);
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void removeAction(
+ T action) {
+ if (actionManager != null) {
+ actionManager.removeAction(action);
+ }
+ }
+
+ @Override
+ public void addActionHandler(Handler actionHandler) {
+ getActionManager().addActionHandler(actionHandler);
+ }
+
+ @Override
+ public void removeActionHandler(Handler actionHandler) {
+ if (actionManager != null) {
+ actionManager.removeActionHandler(actionHandler);
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ if (actionManager != null) {
+ actionManager.removeAllActionHandlers();
+ }
+ }
+
+ /**
+ * Add a click listener to the Panel. The listener is called whenever the
+ * user clicks inside the Panel. Also when the click targets a component
+ * inside the Panel, provided the targeted component does not prevent the
+ * click event from propagating.
+ *
+ * Use {@link #removeListener(ClickListener)} to remove the listener.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addListener(ClickListener listener) {
+ addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class,
+ listener, ClickListener.clickMethod);
+ }
+
+ /**
+ * Remove a click listener from the Panel. The listener should earlier have
+ * been added using {@link #addListener(ClickListener)}.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeListener(ClickListener listener) {
+ removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER,
+ ClickEvent.class, listener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getTabIndex() {
+ return getState().getTabIndex();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ getState().setTabIndex(tabIndex);
+ requestRepaint();
+ }
+
+ /**
+ * Moves keyboard focus to the component. {@see Focusable#focus()}
+ *
+ */
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.ComponentContainer#getComponentCount()
+ */
+ @Override
+ public int getComponentCount() {
+ // This is so wrong... (#2924)
+ return content.getComponentCount();
+ }
+
+ @Override
+ public PanelState getState() {
+ return (PanelState) super.getState();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/PasswordField.java b/server/src/com/vaadin/ui/PasswordField.java
new file mode 100644
index 0000000000..c1fccebbfe
--- /dev/null
+++ b/server/src/com/vaadin/ui/PasswordField.java
@@ -0,0 +1,67 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import com.vaadin.data.Property;
+
+/**
+ * A field that is used to enter secret text information like passwords. The
+ * entered text is not displayed on the screen.
+ */
+public class PasswordField extends AbstractTextField {
+
+ /**
+ * Constructs an empty PasswordField.
+ */
+ public PasswordField() {
+ setValue("");
+ }
+
+ /**
+ * Constructs a PasswordField with given property data source.
+ *
+ * @param dataSource
+ * the property data source for the field
+ */
+ public PasswordField(Property dataSource) {
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a PasswordField with given caption and property data source.
+ *
+ * @param caption
+ * the caption for the field
+ * @param dataSource
+ * the property data source for the field
+ */
+ public PasswordField(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a PasswordField with given value and caption.
+ *
+ * @param caption
+ * the caption for the field
+ * @param value
+ * the value for the field
+ */
+ public PasswordField(String caption, String value) {
+ setValue(value);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a PasswordField with given caption.
+ *
+ * @param caption
+ * the caption for the field
+ */
+ public PasswordField(String caption) {
+ this();
+ setCaption(caption);
+ }
+}
diff --git a/server/src/com/vaadin/ui/PopupDateField.java b/server/src/com/vaadin/ui/PopupDateField.java
new file mode 100644
index 0000000000..3688d4035f
--- /dev/null
+++ b/server/src/com/vaadin/ui/PopupDateField.java
@@ -0,0 +1,80 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Date;
+
+import com.vaadin.data.Property;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * <p>
+ * A date entry component, which displays the actual date selector as a popup.
+ *
+ * </p>
+ *
+ * @see DateField
+ * @see InlineDateField
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class PopupDateField extends DateField {
+
+ private String inputPrompt = null;
+
+ public PopupDateField() {
+ super();
+ }
+
+ public PopupDateField(Property dataSource) throws IllegalArgumentException {
+ super(dataSource);
+ }
+
+ public PopupDateField(String caption, Date value) {
+ super(caption, value);
+ }
+
+ public PopupDateField(String caption, Property dataSource) {
+ super(caption, dataSource);
+ }
+
+ public PopupDateField(String caption) {
+ super(caption);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+
+ if (inputPrompt != null) {
+ target.addAttribute("prompt", inputPrompt);
+ }
+ }
+
+ /**
+ * Gets the current input prompt.
+ *
+ * @see #setInputPrompt(String)
+ * @return the current input prompt, or null if not enabled
+ */
+ public String getInputPrompt() {
+ return inputPrompt;
+ }
+
+ /**
+ * Sets the input prompt - a textual prompt that is displayed when the field
+ * would otherwise be empty, to prompt the user for input.
+ *
+ * @param inputPrompt
+ */
+ public void setInputPrompt(String inputPrompt) {
+ this.inputPrompt = inputPrompt;
+ requestRepaint();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/PopupView.java b/server/src/com/vaadin/ui/PopupView.java
new file mode 100644
index 0000000000..766181b50f
--- /dev/null
+++ b/server/src/com/vaadin/ui/PopupView.java
@@ -0,0 +1,453 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.vaadin.terminal.LegacyPaint;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ *
+ * A component for displaying a two different views to data. The minimized view
+ * is normally used to render the component, and when it is clicked the full
+ * view is displayed on a popup. The inner class {@link PopupView.Content} is
+ * used to deliver contents to this component.
+ *
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class PopupView extends AbstractComponentContainer implements
+ Vaadin6Component {
+
+ private Content content;
+ private boolean hideOnMouseOut;
+ private Component visibleComponent;
+
+ private static final Method POPUP_VISIBILITY_METHOD;
+ static {
+ try {
+ POPUP_VISIBILITY_METHOD = PopupVisibilityListener.class
+ .getDeclaredMethod("popupVisibilityChange",
+ new Class[] { PopupVisibilityEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in PopupView");
+ }
+ }
+
+ /**
+ * Iterator for the visible components (zero or one components), used by
+ * {@link PopupView#getComponentIterator()}.
+ */
+ private static class SingleComponentIterator implements
+ Iterator<Component>, Serializable {
+
+ private final Component component;
+ private boolean first;
+
+ public SingleComponentIterator(Component component) {
+ this.component = component;
+ first = (component == null);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !first;
+ }
+
+ @Override
+ public Component next() {
+ if (!first) {
+ first = true;
+ return component;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /* Constructors */
+
+ /**
+ * A simple way to create a PopupPanel. Note that the minimal representation
+ * may not be dynamically updated, in order to achieve this create your own
+ * Content object and use {@link PopupView#PopupView(Content)}.
+ *
+ * @param small
+ * the minimal textual representation as HTML
+ * @param large
+ * the full, Component-type representation
+ */
+ public PopupView(final java.lang.String small, final Component large) {
+ this(new PopupView.Content() {
+ @Override
+ public java.lang.String getMinimizedValueAsHTML() {
+ return small;
+ }
+
+ @Override
+ public Component getPopupComponent() {
+ return large;
+ }
+ });
+
+ }
+
+ /**
+ * Creates a PopupView through the PopupView.Content interface. This allows
+ * the creator to dynamically change the contents of the PopupView.
+ *
+ * @param content
+ * the PopupView.Content that contains the information for this
+ */
+ public PopupView(PopupView.Content content) {
+ super();
+ hideOnMouseOut = true;
+ setContent(content);
+ }
+
+ /**
+ * This method will replace the current content of the panel with a new one.
+ *
+ * @param newContent
+ * PopupView.Content object containing new information for the
+ * PopupView
+ * @throws IllegalArgumentException
+ * if the method is passed a null value, or if one of the
+ * content methods returns null
+ */
+ public void setContent(PopupView.Content newContent)
+ throws IllegalArgumentException {
+ if (newContent == null) {
+ throw new IllegalArgumentException("Content must not be null");
+ }
+ content = newContent;
+ requestRepaint();
+ }
+
+ /**
+ * Returns the content-package for this PopupView.
+ *
+ * @return the PopupView.Content for this object or null
+ */
+ public PopupView.Content getContent() {
+ return content;
+ }
+
+ /**
+ * @deprecated Use {@link #setPopupVisible()} instead.
+ */
+ @Deprecated
+ public void setPopupVisibility(boolean visible) {
+ setPopupVisible(visible);
+ }
+
+ /**
+ * @deprecated Use {@link #isPopupVisible()} instead.
+ */
+ @Deprecated
+ public boolean getPopupVisibility() {
+ return isPopupVisible();
+ }
+
+ /**
+ * Set the visibility of the popup. Does not hide the minimal
+ * representation.
+ *
+ * @param visible
+ */
+ public void setPopupVisible(boolean visible) {
+ if (isPopupVisible() != visible) {
+ if (visible) {
+ visibleComponent = content.getPopupComponent();
+ if (visibleComponent == null) {
+ throw new java.lang.IllegalStateException(
+ "PopupView.Content did not return Component to set visible");
+ }
+ super.addComponent(visibleComponent);
+ } else {
+ super.removeComponent(visibleComponent);
+ visibleComponent = null;
+ }
+ fireEvent(new PopupVisibilityEvent(this));
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Return whether the popup is visible.
+ *
+ * @return true if the popup is showing
+ */
+ public boolean isPopupVisible() {
+ return visibleComponent != null;
+ }
+
+ /**
+ * Check if this popup will be hidden when the user takes the mouse cursor
+ * out of the popup area.
+ *
+ * @return true if the popup is hidden on mouse out, false otherwise
+ */
+ public boolean isHideOnMouseOut() {
+ return hideOnMouseOut;
+ }
+
+ /**
+ * Should the popup automatically hide when the user takes the mouse cursor
+ * out of the popup area? If this is false, the user must click outside the
+ * popup to close it. The default is true.
+ *
+ * @param hideOnMouseOut
+ *
+ */
+ public void setHideOnMouseOut(boolean hideOnMouseOut) {
+ this.hideOnMouseOut = hideOnMouseOut;
+ }
+
+ /*
+ * Methods inherited from AbstractComponentContainer. These are unnecessary
+ * (but mandatory). Most of them are not supported in this implementation.
+ */
+
+ /**
+ * This class only contains other components when the popup is showing.
+ *
+ * @see com.vaadin.ui.ComponentContainer#getComponentIterator()
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return new SingleComponentIterator(visibleComponent);
+ }
+
+ /**
+ * Gets the number of contained components. Consistent with the iterator
+ * returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components (zero or one)
+ */
+ @Override
+ public int getComponentCount() {
+ return (visibleComponent != null ? 1 : 0);
+ }
+
+ /**
+ * Not supported in this implementation.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents()
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void removeAllComponents() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not supported in this implementation.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer)
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void moveComponentsFrom(ComponentContainer source)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not supported in this implementation.
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component)
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void addComponent(Component c) throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+
+ }
+
+ /**
+ * Not supported in this implementation.
+ *
+ * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component,
+ * com.vaadin.ui.Component)
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent)
+ throws UnsupportedOperationException {
+
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not supported in this implementation
+ *
+ * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component)
+ */
+ @Override
+ public void removeComponent(Component c)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+
+ }
+
+ /*
+ * Methods for server-client communications.
+ */
+
+ /**
+ * Paint (serialize) the component for the client.
+ *
+ * @see com.vaadin.ui.AbstractComponent#paintContent(com.vaadin.terminal.PaintTarget)
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ String html = content.getMinimizedValueAsHTML();
+ if (html == null) {
+ html = "";
+ }
+ target.addAttribute("html", html);
+ target.addAttribute("hideOnMouseOut", hideOnMouseOut);
+
+ // Only paint component to client if we know that the popup is showing
+ if (isPopupVisible()) {
+ target.startTag("popupComponent");
+ LegacyPaint.paint(visibleComponent, target);
+ target.endTag("popupComponent");
+ }
+
+ target.addVariable(this, "popupVisibility", isPopupVisible());
+ }
+
+ /**
+ * Deserialize changes received from client.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ if (variables.containsKey("popupVisibility")) {
+ setPopupVisible(((Boolean) variables.get("popupVisibility"))
+ .booleanValue());
+ }
+ }
+
+ /**
+ * Used to deliver customized content-packages to the PopupView. These are
+ * dynamically loaded when they are redrawn. The user must take care that
+ * neither of these methods ever return null.
+ */
+ public interface Content extends Serializable {
+
+ /**
+ * This should return a small view of the full data.
+ *
+ * @return value in HTML format
+ */
+ public String getMinimizedValueAsHTML();
+
+ /**
+ * This should return the full Component representing the data
+ *
+ * @return a Component for the value
+ */
+ public Component getPopupComponent();
+ }
+
+ /**
+ * Add a listener that is called whenever the visibility of the popup is
+ * changed.
+ *
+ * @param listener
+ * the listener to add
+ * @see PopupVisibilityListener
+ * @see PopupVisibilityEvent
+ * @see #removeListener(PopupVisibilityListener)
+ *
+ */
+ public void addListener(PopupVisibilityListener listener) {
+ addListener(PopupVisibilityEvent.class, listener,
+ POPUP_VISIBILITY_METHOD);
+ }
+
+ /**
+ * Removes a previously added listener, so that it no longer receives events
+ * when the visibility of the popup changes.
+ *
+ * @param listener
+ * the listener to remove
+ * @see PopupVisibilityListener
+ * @see #addListener(PopupVisibilityListener)
+ */
+ public void removeListener(PopupVisibilityListener listener) {
+ removeListener(PopupVisibilityEvent.class, listener,
+ POPUP_VISIBILITY_METHOD);
+ }
+
+ /**
+ * This event is received by the PopupVisibilityListeners when the
+ * visibility of the popup changes. You can get the new visibility directly
+ * with {@link #isPopupVisible()}, or get the PopupView that produced the
+ * event with {@link #getPopupView()}.
+ *
+ */
+ public class PopupVisibilityEvent extends Event {
+
+ public PopupVisibilityEvent(PopupView source) {
+ super(source);
+ }
+
+ /**
+ * Get the PopupView instance that is the source of this event.
+ *
+ * @return the source PopupView
+ */
+ public PopupView getPopupView() {
+ return (PopupView) getSource();
+ }
+
+ /**
+ * Returns the current visibility of the popup.
+ *
+ * @return true if the popup is visible
+ */
+ public boolean isPopupVisible() {
+ return getPopupView().isPopupVisible();
+ }
+ }
+
+ /**
+ * Defines a listener that can receive a PopupVisibilityEvent when the
+ * visibility of the popup changes.
+ *
+ */
+ public interface PopupVisibilityListener extends Serializable {
+ /**
+ * Pass to {@link PopupView#PopupVisibilityEvent} to start listening for
+ * popup visibility changes.
+ *
+ * @param event
+ * the event
+ *
+ * @see {@link PopupVisibilityEvent}
+ * @see {@link PopupView#addListener(PopupVisibilityListener)}
+ */
+ public void popupVisibilityChange(PopupVisibilityEvent event);
+ }
+}
diff --git a/server/src/com/vaadin/ui/ProgressIndicator.java b/server/src/com/vaadin/ui/ProgressIndicator.java
new file mode 100644
index 0000000000..fef54a267c
--- /dev/null
+++ b/server/src/com/vaadin/ui/ProgressIndicator.java
@@ -0,0 +1,257 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Map;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.ObjectProperty;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ * <code>ProgressIndicator</code> is component that shows user state of a
+ * process (like long computing or file upload)
+ *
+ * <code>ProgressIndicator</code> has two mainmodes. One for indeterminate
+ * processes and other (default) for processes which progress can be measured
+ *
+ * May view an other property that indicates progress 0...1
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 4
+ */
+@SuppressWarnings("serial")
+public class ProgressIndicator extends AbstractField<Number> implements
+ Property.Viewer, Property.ValueChangeListener, Vaadin6Component {
+
+ /**
+ * Content mode, where the label contains only plain text. The getValue()
+ * result is coded to XML when painting.
+ */
+ public static final int CONTENT_TEXT = 0;
+
+ /**
+ * Content mode, where the label contains preformatted text.
+ */
+ public static final int CONTENT_PREFORMATTED = 1;
+
+ private boolean indeterminate = false;
+
+ private Property dataSource;
+
+ private int pollingInterval = 1000;
+
+ /**
+ * Creates an a new ProgressIndicator.
+ */
+ public ProgressIndicator() {
+ setPropertyDataSource(new ObjectProperty<Float>(new Float(0),
+ Float.class));
+ }
+
+ /**
+ * Creates a new instance of ProgressIndicator with given state.
+ *
+ * @param value
+ */
+ public ProgressIndicator(Float value) {
+ setPropertyDataSource(new ObjectProperty<Float>(value, Float.class));
+ }
+
+ /**
+ * Creates a new instance of ProgressIndicator with stae read from given
+ * datasource.
+ *
+ * @param contentSource
+ */
+ public ProgressIndicator(Property contentSource) {
+ setPropertyDataSource(contentSource);
+ }
+
+ /**
+ * Sets the component to read-only. Readonly is not used in
+ * ProgressIndicator.
+ *
+ * @param readOnly
+ * True to enable read-only mode, False to disable it.
+ */
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ if (dataSource == null) {
+ throw new IllegalStateException("Datasource must be se");
+ }
+ dataSource.setReadOnly(readOnly);
+ }
+
+ /**
+ * Is the component read-only ? Readonly is not used in ProgressIndicator -
+ * this returns allways false.
+ *
+ * @return True if the component is in read only mode.
+ */
+ @Override
+ public boolean isReadOnly() {
+ if (dataSource == null) {
+ throw new IllegalStateException("Datasource must be se");
+ }
+ return dataSource.isReadOnly();
+ }
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the Paint Operation fails.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute("indeterminate", indeterminate);
+ target.addAttribute("pollinginterval", pollingInterval);
+ target.addAttribute("state", getValue().toString());
+ }
+
+ /**
+ * Gets the value of the ProgressIndicator. Value of the ProgressIndicator
+ * is Float between 0 and 1.
+ *
+ * @return the Value of the ProgressIndicator.
+ * @see com.vaadin.ui.AbstractField#getValue()
+ */
+ @Override
+ public Number getValue() {
+ if (dataSource == null) {
+ throw new IllegalStateException("Datasource must be set");
+ }
+ // TODO conversions to eliminate cast
+ return (Number) dataSource.getValue();
+ }
+
+ /**
+ * Sets the value of the ProgressIndicator. Value of the ProgressIndicator
+ * is the Float between 0 and 1.
+ *
+ * @param newValue
+ * the New value of the ProgressIndicator.
+ * @see com.vaadin.ui.AbstractField#setValue()
+ */
+ @Override
+ public void setValue(Object newValue) {
+ if (dataSource == null) {
+ throw new IllegalStateException("Datasource must be set");
+ }
+ dataSource.setValue(newValue);
+ }
+
+ /**
+ * @see com.vaadin.ui.AbstractField#getType()
+ */
+ @Override
+ public Class<? extends Number> getType() {
+ if (dataSource == null) {
+ throw new IllegalStateException("Datasource must be set");
+ }
+ return dataSource.getType();
+ }
+
+ /**
+ * Gets the viewing data-source property.
+ *
+ * @return the datasource.
+ * @see com.vaadin.ui.AbstractField#getPropertyDataSource()
+ */
+ @Override
+ public Property getPropertyDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * Sets the property as data-source for viewing.
+ *
+ * @param newDataSource
+ * the new data source.
+ * @see com.vaadin.ui.AbstractField#setPropertyDataSource(com.vaadin.data.Property)
+ */
+ @Override
+ public void setPropertyDataSource(Property newDataSource) {
+ // Stops listening the old data source changes
+ if (dataSource != null
+ && Property.ValueChangeNotifier.class
+ .isAssignableFrom(dataSource.getClass())) {
+ ((Property.ValueChangeNotifier) dataSource).removeListener(this);
+ }
+
+ // Sets the new data source
+ dataSource = newDataSource;
+
+ // Listens the new data source if possible
+ if (dataSource != null
+ && Property.ValueChangeNotifier.class
+ .isAssignableFrom(dataSource.getClass())) {
+ ((Property.ValueChangeNotifier) dataSource).addListener(this);
+ }
+ }
+
+ /**
+ * Gets the mode of ProgressIndicator.
+ *
+ * @return true if in indeterminate mode.
+ */
+ public boolean getContentMode() {
+ return indeterminate;
+ }
+
+ /**
+ * Sets wheter or not the ProgressIndicator is indeterminate.
+ *
+ * @param newValue
+ * true to set to indeterminate mode.
+ */
+ public void setIndeterminate(boolean newValue) {
+ indeterminate = newValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets whether or not the ProgressIndicator is indeterminate.
+ *
+ * @return true to set to indeterminate mode.
+ */
+ public boolean isIndeterminate() {
+ return indeterminate;
+ }
+
+ /**
+ * Sets the interval that component checks for progress.
+ *
+ * @param newValue
+ * the interval in milliseconds.
+ */
+ public void setPollingInterval(int newValue) {
+ pollingInterval = newValue;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the interval that component checks for progress.
+ *
+ * @return the interval in milliseconds.
+ */
+ public int getPollingInterval() {
+ return pollingInterval;
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // TODO Remove once Vaadin6Component is no longer implemented
+
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/RichTextArea.java b/server/src/com/vaadin/ui/RichTextArea.java
new file mode 100644
index 0000000000..cec952926b
--- /dev/null
+++ b/server/src/com/vaadin/ui/RichTextArea.java
@@ -0,0 +1,344 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.text.Format;
+import java.util.Map;
+
+import com.vaadin.data.Property;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ * A simple RichTextArea to edit HTML format text.
+ *
+ * Note, that using {@link TextField#setMaxLength(int)} method in
+ * {@link RichTextArea} may produce unexpected results as formatting is counted
+ * into length of field.
+ */
+public class RichTextArea extends AbstractField<String> implements
+ Vaadin6Component {
+
+ /**
+ * Value formatter used to format the string contents.
+ */
+ @Deprecated
+ private Format format;
+
+ /**
+ * Null representation.
+ */
+ private String nullRepresentation = "null";
+
+ /**
+ * Is setting to null from non-null value allowed by setting with null
+ * representation .
+ */
+ private boolean nullSettingAllowed = false;
+
+ /**
+ * Temporary flag that indicates all content will be selected after the next
+ * paint. Reset to false after painted.
+ */
+ private boolean selectAll = false;
+
+ /**
+ * Constructs an empty <code>RichTextArea</code> with no caption.
+ */
+ public RichTextArea() {
+ setValue("");
+ }
+
+ /**
+ *
+ * Constructs an empty <code>RichTextArea</code> with the given caption.
+ *
+ * @param caption
+ * the caption for the editor.
+ */
+ public RichTextArea(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> that's bound to the specified
+ * <code>Property</code> and has no caption.
+ *
+ * @param dataSource
+ * the data source for the editor value
+ */
+ public RichTextArea(Property dataSource) {
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> that's bound to the specified
+ * <code>Property</code> and has the given caption.
+ *
+ * @param caption
+ * the caption for the editor.
+ * @param dataSource
+ * the data source for the editor value
+ */
+ public RichTextArea(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> with the given caption and
+ * initial text contents.
+ *
+ * @param caption
+ * the caption for the editor.
+ * @param value
+ * the initial text content of the editor.
+ */
+ public RichTextArea(String caption, String value) {
+ setValue(value);
+ setCaption(caption);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (selectAll) {
+ target.addAttribute("selectAll", true);
+ selectAll = false;
+ }
+
+ // Adds the content as variable
+ String value = getFormattedValue();
+ if (value == null) {
+ value = getNullRepresentation();
+ }
+ if (value == null) {
+ throw new IllegalStateException(
+ "Null values are not allowed if the null-representation is null");
+ }
+ target.addVariable(this, "text", value);
+
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ super.setReadOnly(readOnly);
+ // IE6 cannot support multi-classname selectors properly
+ // TODO Can be optimized now that support for I6 is dropped
+ if (readOnly) {
+ addStyleName("v-richtextarea-readonly");
+ } else {
+ removeStyleName("v-richtextarea-readonly");
+ }
+ }
+
+ /**
+ * Selects all text in the rich text area. As a side effect, focuses the
+ * rich text area.
+ *
+ * @since 6.5
+ */
+ public void selectAll() {
+ /*
+ * Set selection range functionality is currently being
+ * planned/developed for GWT RTA. Only selecting all is currently
+ * supported. Consider moving selectAll and other selection related
+ * functions to AbstractTextField at that point to share the
+ * implementation. Some third party components extending
+ * AbstractTextField might however not want to support them.
+ */
+ selectAll = true;
+ focus();
+ requestRepaint();
+ }
+
+ /**
+ * Gets the formatted string value. Sets the field value by using the
+ * assigned Format.
+ *
+ * @return the Formatted value.
+ * @see #setFormat(Format)
+ * @see Format
+ * @deprecated
+ */
+ @Deprecated
+ protected String getFormattedValue() {
+ Object v = getValue();
+ if (v == null) {
+ return null;
+ }
+ return v.toString();
+ }
+
+ @Override
+ public String getValue() {
+ String v = super.getValue();
+ if (format == null || v == null) {
+ return v;
+ }
+ try {
+ return format.format(v);
+ } catch (final IllegalArgumentException e) {
+ return v;
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Sets the text
+ if (variables.containsKey("text") && !isReadOnly()) {
+
+ // Only do the setting if the string representation of the value
+ // has been updated
+ String newValue = (String) variables.get("text");
+
+ final String oldValue = getFormattedValue();
+ if (newValue != null
+ && (oldValue == null || isNullSettingAllowed())
+ && newValue.equals(getNullRepresentation())) {
+ newValue = null;
+ }
+ if (newValue != oldValue
+ && (newValue == null || !newValue.equals(oldValue))) {
+ boolean wasModified = isModified();
+ setValue(newValue, true);
+
+ // If the modified status changes, or if we have a formatter,
+ // repaint is needed after all.
+ if (format != null || wasModified != isModified()) {
+ requestRepaint();
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Gets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'.
+ * </p>
+ *
+ * @return the String Textual representation for null strings.
+ * @see TextField#isNullSettingAllowed()
+ */
+ public String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Is setting nulls with null-string representation allowed.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false
+ * </p>
+ *
+ * @return boolean Should the null-string represenation be always converted
+ * to null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public boolean isNullSettingAllowed() {
+ return nullSettingAllowed;
+ }
+
+ /**
+ * Sets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'
+ * </p>
+ *
+ * @param nullRepresentation
+ * Textual representation for null strings.
+ * @see TextField#setNullSettingAllowed(boolean)
+ */
+ public void setNullRepresentation(String nullRepresentation) {
+ this.nullRepresentation = nullRepresentation;
+ }
+
+ /**
+ * Sets the null conversion mode.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false.
+ * </p>
+ *
+ * @param nullSettingAllowed
+ * Should the null-string represenation be always converted to
+ * null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public void setNullSettingAllowed(boolean nullSettingAllowed) {
+ this.nullSettingAllowed = nullSettingAllowed;
+ }
+
+ /**
+ * Gets the value formatter of TextField.
+ *
+ * @return the Format used to format the value.
+ * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter}
+ */
+ @Deprecated
+ public Format getFormat() {
+ return format;
+ }
+
+ /**
+ * Gets the value formatter of TextField.
+ *
+ * @param format
+ * the Format used to format the value. Null disables the
+ * formatting.
+ * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter}
+ */
+ @Deprecated
+ public void setFormat(Format format) {
+ this.format = format;
+ requestRepaint();
+ }
+
+ @Override
+ protected boolean isEmpty() {
+ return super.isEmpty() || getValue().length() == 0;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Root.java b/server/src/com/vaadin/ui/Root.java
new file mode 100644
index 0000000000..bd4842632b
--- /dev/null
+++ b/server/src/com/vaadin/ui/Root.java
@@ -0,0 +1,1227 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+
+import com.vaadin.Application;
+import com.vaadin.annotations.EagerInit;
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.ActionManager;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.MouseEvents.ClickListener;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.root.RootServerRpc;
+import com.vaadin.shared.ui.root.RootState;
+import com.vaadin.terminal.Page;
+import com.vaadin.terminal.Page.BrowserWindowResizeEvent;
+import com.vaadin.terminal.Page.BrowserWindowResizeListener;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.WrappedRequest;
+import com.vaadin.terminal.WrappedRequest.BrowserDetails;
+import com.vaadin.terminal.gwt.client.ui.root.VRoot;
+import com.vaadin.ui.Window.CloseListener;
+
+/**
+ * The topmost component in any component hierarchy. There is one root for every
+ * Vaadin instance in a browser window. A root may either represent an entire
+ * browser window (or tab) or some part of a html page where a Vaadin
+ * application is embedded.
+ * <p>
+ * The root is the server side entry point for various client side features that
+ * are not represented as components added to a layout, e.g notifications, sub
+ * windows, and executing javascript in the browser.
+ * </p>
+ * <p>
+ * When a new application instance is needed, typically because the user opens
+ * the application in a browser window,
+ * {@link Application#gerRoot(WrappedRequest)} is invoked to get a root. That
+ * method does by default create a root according to the
+ * {@value Application#ROOT_PARAMETER} parameter from web.xml.
+ * </p>
+ * <p>
+ * After a root has been created by the application, it is initialized using
+ * {@link #init(WrappedRequest)}. This method is intended to be overridden by
+ * the developer to add components to the user interface and initialize
+ * non-component functionality. The component hierarchy is initialized by
+ * passing a {@link ComponentContainer} with the main layout of the view to
+ * {@link #setContent(ComponentContainer)}.
+ * </p>
+ * <p>
+ * If a {@link EagerInit} annotation is present on a class extending
+ * <code>Root</code>, the framework will use a faster initialization method
+ * which will not ensure that {@link BrowserDetails} are present in the
+ * {@link WrappedRequest} passed to the init method.
+ * </p>
+ *
+ * @see #init(WrappedRequest)
+ * @see Application#getRoot(WrappedRequest)
+ *
+ * @since 7.0
+ */
+public abstract class Root extends AbstractComponentContainer implements
+ Action.Container, Action.Notifier, Vaadin6Component {
+
+ /**
+ * Helper class to emulate the main window from Vaadin 6 using roots. This
+ * class should be used in the same way as Window used as a browser level
+ * window in Vaadin 6 with {@link com.vaadin.Application.LegacyApplication}
+ */
+ @Deprecated
+ @EagerInit
+ public static class LegacyWindow extends Root {
+ private String name;
+
+ /**
+ * Create a new legacy window
+ */
+ public LegacyWindow() {
+ super();
+ }
+
+ /**
+ * Creates a new legacy window with the given caption
+ *
+ * @param caption
+ * the caption of the window
+ */
+ public LegacyWindow(String caption) {
+ super(caption);
+ }
+
+ /**
+ * Creates a legacy window with the given caption and content layout
+ *
+ * @param caption
+ * @param content
+ */
+ public LegacyWindow(String caption, ComponentContainer content) {
+ super(caption, content);
+ }
+
+ @Override
+ protected void init(WrappedRequest request) {
+ // Just empty
+ }
+
+ /**
+ * Gets the unique name of the window. The name of the window is used to
+ * uniquely identify it.
+ * <p>
+ * The name also determines the URL that can be used for direct access
+ * to a window. All windows can be accessed through
+ * {@code http://host:port/app/win} where {@code http://host:port/app}
+ * is the application URL (as returned by {@link Application#getURL()}
+ * and {@code win} is the window name.
+ * </p>
+ * <p>
+ * Note! Portlets do not support direct window access through URLs.
+ * </p>
+ *
+ * @return the Name of the Window.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the unique name of the window. The name of the window is used to
+ * uniquely identify it inside the application.
+ * <p>
+ * The name also determines the URL that can be used for direct access
+ * to a window. All windows can be accessed through
+ * {@code http://host:port/app/win} where {@code http://host:port/app}
+ * is the application URL (as returned by {@link Application#getURL()}
+ * and {@code win} is the window name.
+ * </p>
+ * <p>
+ * This method can only be called before the window is added to an
+ * application.
+ * <p>
+ * Note! Portlets do not support direct window access through URLs.
+ * </p>
+ *
+ * @param name
+ * the new name for the window or null if the application
+ * should automatically assign a name to it
+ * @throws IllegalStateException
+ * if the window is attached to an application
+ */
+ public void setName(String name) {
+ this.name = name;
+ // The name can not be changed in application
+ if (getApplication() != null) {
+ throw new IllegalStateException(
+ "Window name can not be changed while "
+ + "the window is in application");
+ }
+
+ }
+
+ /**
+ * Gets the full URL of the window. The returned URL is window specific
+ * and can be used to directly refer to the window.
+ * <p>
+ * Note! This method can not be used for portlets.
+ * </p>
+ *
+ * @return the URL of the window or null if the window is not attached
+ * to an application
+ */
+ public URL getURL() {
+ Application application = getApplication();
+ if (application == null) {
+ return null;
+ }
+
+ try {
+ return new URL(application.getURL(), getName() + "/");
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(
+ "Internal problem getting window URL, please report");
+ }
+ }
+
+ /**
+ * Opens the given resource in this root. The contents of this Root is
+ * replaced by the {@code Resource}.
+ *
+ * @param resource
+ * the resource to show in this root
+ *
+ * @deprecated As of 7.0, use getPage().open instead
+ */
+ @Deprecated
+ public void open(Resource resource) {
+ getPage().open(resource);
+ }
+
+ /* ********************************************************************* */
+
+ /**
+ * Opens the given resource in a window with the given name.
+ * <p>
+ * The supplied {@code windowName} is used as the target name in a
+ * window.open call in the client. This means that special values such
+ * as "_blank", "_self", "_top", "_parent" have special meaning. An
+ * empty or <code>null</code> window name is also a special case.
+ * </p>
+ * <p>
+ * "", null and "_self" as {@code windowName} all causes the resource to
+ * be opened in the current window, replacing any old contents. For
+ * downloadable content you should avoid "_self" as "_self" causes the
+ * client to skip rendering of any other changes as it considers them
+ * irrelevant (the page will be replaced by the resource). This can
+ * speed up the opening of a resource, but it might also put the client
+ * side into an inconsistent state if the window content is not
+ * completely replaced e.g., if the resource is downloaded instead of
+ * displayed in the browser.
+ * </p>
+ * <p>
+ * "_blank" as {@code windowName} causes the resource to always be
+ * opened in a new window or tab (depends on the browser and browser
+ * settings).
+ * </p>
+ * <p>
+ * "_top" and "_parent" as {@code windowName} works as specified by the
+ * HTML standard.
+ * </p>
+ * <p>
+ * Any other {@code windowName} will open the resource in a window with
+ * that name, either by opening a new window/tab in the browser or by
+ * replacing the contents of an existing window with that name.
+ * </p>
+ *
+ * @param resource
+ * the resource.
+ * @param windowName
+ * the name of the window.
+ * @deprecated As of 7.0, use getPage().open instead
+ */
+ @Deprecated
+ public void open(Resource resource, String windowName) {
+ getPage().open(resource, windowName);
+ }
+
+ /**
+ * Opens the given resource in a window with the given size, border and
+ * name. For more information on the meaning of {@code windowName}, see
+ * {@link #open(Resource, String)}.
+ *
+ * @param resource
+ * the resource.
+ * @param windowName
+ * the name of the window.
+ * @param width
+ * the width of the window in pixels
+ * @param height
+ * the height of the window in pixels
+ * @param border
+ * the border style of the window. See {@link #BORDER_NONE
+ * Window.BORDER_* constants}
+ * @deprecated As of 7.0, use getPage().open instead
+ */
+ @Deprecated
+ public void open(Resource resource, String windowName, int width,
+ int height, int border) {
+ getPage().open(resource, windowName, width, height, border);
+ }
+
+ /**
+ * Adds a new {@link BrowserWindowResizeListener} to this root. The
+ * listener will be notified whenever the browser window within which
+ * this root resides is resized.
+ *
+ * @param resizeListener
+ * the listener to add
+ *
+ * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent)
+ * @see #setResizeLazy(boolean)
+ *
+ * @deprecated As of 7.0, use the similarly named api in Page instead
+ */
+ @Deprecated
+ public void addListener(BrowserWindowResizeListener resizeListener) {
+ getPage().addListener(resizeListener);
+ }
+
+ /**
+ * Removes a {@link BrowserWindowResizeListener} from this root. The
+ * listener will no longer be notified when the browser window is
+ * resized.
+ *
+ * @param resizeListener
+ * the listener to remove
+ * @deprecated As of 7.0, use the similarly named api in Page instead
+ */
+ @Deprecated
+ public void removeListener(BrowserWindowResizeListener resizeListener) {
+ getPage().removeListener(resizeListener);
+ }
+
+ /**
+ * Gets the last known height of the browser window in which this root
+ * resides.
+ *
+ * @return the browser window height in pixels
+ * @deprecated As of 7.0, use the similarly named api in Page instead
+ */
+ @Deprecated
+ public int getBrowserWindowHeight() {
+ return getPage().getBrowserWindowHeight();
+ }
+
+ /**
+ * Gets the last known width of the browser window in which this root
+ * resides.
+ *
+ * @return the browser window width in pixels
+ *
+ * @deprecated As of 7.0, use the similarly named api in Page instead
+ */
+ @Deprecated
+ public int getBrowserWindowWidth() {
+ return getPage().getBrowserWindowWidth();
+ }
+
+ /**
+ * Executes JavaScript in this window.
+ *
+ * <p>
+ * This method allows one to inject javascript from the server to
+ * client. A client implementation is not required to implement this
+ * functionality, but currently all web-based clients do implement this.
+ * </p>
+ *
+ * <p>
+ * Executing javascript this way often leads to cross-browser
+ * compatibility issues and regressions that are hard to resolve. Use of
+ * this method should be avoided and instead it is recommended to create
+ * new widgets with GWT. For more info on creating own, reusable
+ * client-side widgets in Java, read the corresponding chapter in Book
+ * of Vaadin.
+ * </p>
+ *
+ * @param script
+ * JavaScript snippet that will be executed.
+ *
+ * @deprecated as of 7.0, use JavaScript.getCurrent().execute(String)
+ * instead
+ */
+ @Deprecated
+ public void executeJavaScript(String script) {
+ getPage().getJavaScript().execute(script);
+ }
+
+ @Override
+ public void setCaption(String caption) {
+ // Override to provide backwards compatibility
+ getState().setCaption(caption);
+ getPage().setTitle(caption);
+ }
+
+ }
+
+ /**
+ * The application to which this root belongs
+ */
+ private Application application;
+
+ /**
+ * List of windows in this root.
+ */
+ private final LinkedHashSet<Window> windows = new LinkedHashSet<Window>();
+
+ /**
+ * The component that should be scrolled into view after the next repaint.
+ * Null if nothing should be scrolled into view.
+ */
+ private Component scrollIntoView;
+
+ /**
+ * The id of this root, used to find the server side instance of the root
+ * form which a request originates. A negative value indicates that the root
+ * id has not yet been assigned by the Application.
+ *
+ * @see Application#nextRootId
+ */
+ private int rootId = -1;
+
+ /**
+ * Keeps track of the Actions added to this component, and manages the
+ * painting and handling as well.
+ */
+ protected ActionManager actionManager;
+
+ /**
+ * Thread local for keeping track of the current root.
+ */
+ private static final ThreadLocal<Root> currentRoot = new ThreadLocal<Root>();
+
+ /** Identifies the click event */
+ private static final String CLICK_EVENT_ID = VRoot.CLICK_EVENT_ID;
+
+ private ConnectorTracker connectorTracker = new ConnectorTracker(this);
+
+ private Page page = new Page(this);
+
+ private RootServerRpc rpc = new RootServerRpc() {
+ @Override
+ public void click(MouseEventDetails mouseDetails) {
+ fireEvent(new ClickEvent(Root.this, mouseDetails));
+ }
+ };
+
+ /**
+ * Creates a new empty root without a caption. This root will have a
+ * {@link VerticalLayout} with margins enabled as its content.
+ */
+ public Root() {
+ this((ComponentContainer) null);
+ }
+
+ /**
+ * Creates a new root with the given component container as its content.
+ *
+ * @param content
+ * the content container to use as this roots content.
+ *
+ * @see #setContent(ComponentContainer)
+ */
+ public Root(ComponentContainer content) {
+ registerRpc(rpc);
+ setSizeFull();
+ setContent(content);
+ }
+
+ /**
+ * Creates a new empty root with the given caption. This root will have a
+ * {@link VerticalLayout} with margins enabled as its content.
+ *
+ * @param caption
+ * the caption of the root, used as the page title if there's
+ * nothing but the application on the web page
+ *
+ * @see #setCaption(String)
+ */
+ public Root(String caption) {
+ this((ComponentContainer) null);
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new root with the given caption and content.
+ *
+ * @param caption
+ * the caption of the root, used as the page title if there's
+ * nothing but the application on the web page
+ * @param content
+ * the content container to use as this roots content.
+ *
+ * @see #setContent(ComponentContainer)
+ * @see #setCaption(String)
+ */
+ public Root(String caption, ComponentContainer content) {
+ this(content);
+ setCaption(caption);
+ }
+
+ @Override
+ public RootState getState() {
+ return (RootState) super.getState();
+ }
+
+ @Override
+ public Class<? extends RootState> getStateType() {
+ // This is a workaround for a problem with creating the correct state
+ // object during build
+ return RootState.class;
+ }
+
+ /**
+ * Overridden to return a value instead of referring to the parent.
+ *
+ * @return this root
+ *
+ * @see com.vaadin.ui.AbstractComponent#getRoot()
+ */
+ @Override
+ public Root getRoot() {
+ return this;
+ }
+
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Application getApplication() {
+ return application;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ page.paintContent(target);
+
+ if (scrollIntoView != null) {
+ target.addAttribute("scrollTo", scrollIntoView);
+ scrollIntoView = null;
+ }
+
+ if (pendingFocus != null) {
+ // ensure focused component is still attached to this main window
+ if (pendingFocus.getRoot() == this
+ || (pendingFocus.getRoot() != null && pendingFocus
+ .getRoot().getParent() == this)) {
+ target.addAttribute("focused", pendingFocus);
+ }
+ pendingFocus = null;
+ }
+
+ if (actionManager != null) {
+ actionManager.paintActions(null, target);
+ }
+
+ if (isResizeLazy()) {
+ target.addAttribute(VRoot.RESIZE_LAZY, true);
+ }
+ }
+
+ /**
+ * Fire a click event to all click listeners.
+ *
+ * @param object
+ * The raw "value" of the variable change from the client side.
+ */
+ private void fireClick(Map<String, Object> parameters) {
+ MouseEventDetails mouseDetails = MouseEventDetails
+ .deSerialize((String) parameters.get("mouseDetails"));
+ fireEvent(new ClickEvent(this, mouseDetails));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ if (variables.containsKey(CLICK_EVENT_ID)) {
+ fireClick((Map<String, Object>) variables.get(CLICK_EVENT_ID));
+ }
+
+ // Actions
+ if (actionManager != null) {
+ actionManager.handleActions(variables, this);
+ }
+
+ if (variables.containsKey(VRoot.FRAGMENT_VARIABLE)) {
+ String fragment = (String) variables.get(VRoot.FRAGMENT_VARIABLE);
+ getPage().setFragment(fragment, true);
+ }
+
+ if (variables.containsKey("height") || variables.containsKey("width")) {
+ getPage().setBrowserWindowSize((Integer) variables.get("width"),
+ (Integer) variables.get("height"));
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.ComponentContainer#getComponentIterator()
+ */
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ // TODO could directly create some kind of combined iterator instead of
+ // creating a new ArrayList
+ ArrayList<Component> components = new ArrayList<Component>();
+
+ if (getContent() != null) {
+ components.add(getContent());
+ }
+
+ components.addAll(windows);
+
+ return components.iterator();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.ComponentContainer#getComponentCount()
+ */
+ @Override
+ public int getComponentCount() {
+ return windows.size() + (getContent() == null ? 0 : 1);
+ }
+
+ /**
+ * Sets the application to which this root is assigned. It is not legal to
+ * change the application once it has been set nor to set a
+ * <code>null</code> application.
+ * <p>
+ * This method is mainly intended for internal use by the framework.
+ * </p>
+ *
+ * @param application
+ * the application to set
+ *
+ * @throws IllegalStateException
+ * if the application has already been set
+ *
+ * @see #getApplication()
+ */
+ public void setApplication(Application application) {
+ if ((application == null) == (this.application == null)) {
+ throw new IllegalStateException("Application has already been set");
+ } else {
+ this.application = application;
+ }
+
+ if (application != null) {
+ attach();
+ } else {
+ detach();
+ }
+ }
+
+ /**
+ * Sets the id of this root within its application. The root id is used to
+ * route requests to the right root.
+ * <p>
+ * This method is mainly intended for internal use by the framework.
+ * </p>
+ *
+ * @param rootId
+ * the id of this root
+ *
+ * @throws IllegalStateException
+ * if the root id has already been set
+ *
+ * @see #getRootId()
+ */
+ public void setRootId(int rootId) {
+ if (this.rootId != -1) {
+ throw new IllegalStateException("Root id has already been defined");
+ }
+ this.rootId = rootId;
+ }
+
+ /**
+ * Gets the id of the root, used to identify this root within its
+ * application when processing requests. The root id should be present in
+ * every request to the server that originates from this root.
+ * {@link Application#getRootForRequest(WrappedRequest)} uses this id to
+ * find the route to which the request belongs.
+ *
+ * @return
+ */
+ public int getRootId() {
+ return rootId;
+ }
+
+ /**
+ * Adds a window as a subwindow inside this root. To open a new browser
+ * window or tab, you should instead use {@link open(Resource)} with an url
+ * pointing to this application and ensure
+ * {@link Application#getRoot(WrappedRequest)} returns an appropriate root
+ * for the request.
+ *
+ * @param window
+ * @throws IllegalArgumentException
+ * if the window is already added to an application
+ * @throws NullPointerException
+ * if the given <code>Window</code> is <code>null</code>.
+ */
+ public void addWindow(Window window) throws IllegalArgumentException,
+ NullPointerException {
+
+ if (window == null) {
+ throw new NullPointerException("Argument must not be null");
+ }
+
+ if (window.getApplication() != null) {
+ throw new IllegalArgumentException(
+ "Window is already attached to an application.");
+ }
+
+ attachWindow(window);
+ }
+
+ /**
+ * Helper method to attach a window.
+ *
+ * @param w
+ * the window to add
+ */
+ private void attachWindow(Window w) {
+ windows.add(w);
+ w.setParent(this);
+ requestRepaint();
+ }
+
+ /**
+ * Remove the given subwindow from this root.
+ *
+ * Since Vaadin 6.5, {@link CloseListener}s are called also when explicitly
+ * removing a window by calling this method.
+ *
+ * Since Vaadin 6.5, returns a boolean indicating if the window was removed
+ * or not.
+ *
+ * @param window
+ * Window to be removed.
+ * @return true if the subwindow was removed, false otherwise
+ */
+ public boolean removeWindow(Window window) {
+ if (!windows.remove(window)) {
+ // Window window is not a subwindow of this root.
+ return false;
+ }
+ window.setParent(null);
+ window.fireClose();
+ requestRepaint();
+
+ return true;
+ }
+
+ /**
+ * Gets all the windows added to this root.
+ *
+ * @return an unmodifiable collection of windows
+ */
+ public Collection<Window> getWindows() {
+ return Collections.unmodifiableCollection(windows);
+ }
+
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ /**
+ * Component that should be focused after the next repaint. Null if no focus
+ * change should take place.
+ */
+ private Focusable pendingFocus;
+
+ private boolean resizeLazy = false;
+
+ /**
+ * This method is used by Component.Focusable objects to request focus to
+ * themselves. Focus renders must be handled at window level (instead of
+ * Component.Focusable) due we want the last focused component to be focused
+ * in client too. Not the one that is rendered last (the case we'd get if
+ * implemented in Focusable only).
+ *
+ * To focus component from Vaadin application, use Focusable.focus(). See
+ * {@link Focusable}.
+ *
+ * @param focusable
+ * to be focused on next paint
+ */
+ public void setFocusedComponent(Focusable focusable) {
+ pendingFocus = focusable;
+ requestRepaint();
+ }
+
+ /**
+ * Scrolls any component between the component and root to a suitable
+ * position so the component is visible to the user. The given component
+ * must belong to this root.
+ *
+ * @param component
+ * the component to be scrolled into view
+ * @throws IllegalArgumentException
+ * if {@code component} does not belong to this root
+ */
+ public void scrollIntoView(Component component)
+ throws IllegalArgumentException {
+ if (component.getRoot() != this) {
+ throw new IllegalArgumentException(
+ "The component where to scroll must belong to this root.");
+ }
+ scrollIntoView = component;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the content of this root. The content is a component container that
+ * serves as the outermost item of the visual contents of this root.
+ *
+ * @return a component container to use as content
+ *
+ * @see #setContent(ComponentContainer)
+ * @see #createDefaultLayout()
+ */
+ public ComponentContainer getContent() {
+ return (ComponentContainer) getState().getContent();
+ }
+
+ /**
+ * Helper method to create the default content layout that is used if no
+ * content has not been explicitly defined.
+ *
+ * @return a newly created layout
+ */
+ private static VerticalLayout createDefaultLayout() {
+ VerticalLayout layout = new VerticalLayout();
+ layout.setMargin(true);
+ return layout;
+ }
+
+ /**
+ * Sets the content of this root. The content is a component container that
+ * serves as the outermost item of the visual contents of this root. If no
+ * content has been set, a {@link VerticalLayout} with margins enabled will
+ * be used by default - see {@link #createDefaultLayout()}. The content can
+ * also be set in a constructor.
+ *
+ * @return a component container to use as content
+ *
+ * @see #Root(ComponentContainer)
+ * @see #createDefaultLayout()
+ */
+ public void setContent(ComponentContainer content) {
+ if (content == null) {
+ content = createDefaultLayout();
+ }
+
+ if (getState().getContent() != null) {
+ super.removeComponent((Component) getState().getContent());
+ }
+ getState().setContent(content);
+ if (content != null) {
+ super.addComponent(content);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Adds a component to this root. The component is not added directly to the
+ * root, but instead to the content container ({@link #getContent()}).
+ *
+ * @param component
+ * the component to add to this root
+ *
+ * @see #getContent()
+ */
+ @Override
+ public void addComponent(Component component) {
+ getContent().addComponent(component);
+ }
+
+ /**
+ * This implementation removes the component from the content container (
+ * {@link #getContent()}) instead of from the actual root.
+ */
+ @Override
+ public void removeComponent(Component component) {
+ getContent().removeComponent(component);
+ }
+
+ /**
+ * This implementation removes the components from the content container (
+ * {@link #getContent()}) instead of from the actual root.
+ */
+ @Override
+ public void removeAllComponents() {
+ getContent().removeAllComponents();
+ }
+
+ /**
+ * Internal initialization method, should not be overridden. This method is
+ * not declared as final because that would break compatibility with e.g.
+ * CDI.
+ *
+ * @param request
+ * the initialization request
+ */
+ public void doInit(WrappedRequest request) {
+ getPage().init(request);
+
+ // Call the init overridden by the application developer
+ init(request);
+ }
+
+ /**
+ * Initializes this root. This method is intended to be overridden by
+ * subclasses to build the view and configure non-component functionality.
+ * Performing the initialization in a constructor is not suggested as the
+ * state of the root is not properly set up when the constructor is invoked.
+ * <p>
+ * The {@link WrappedRequest} can be used to get information about the
+ * request that caused this root to be created. By default, the
+ * {@link BrowserDetails} will be available in the request. If the browser
+ * details are not required, loading the application in the browser can take
+ * some shortcuts giving a faster initial rendering. This can be indicated
+ * by adding the {@link EagerInit} annotation to the Root class.
+ * </p>
+ *
+ * @param request
+ * the wrapped request that caused this root to be created
+ */
+ protected abstract void init(WrappedRequest request);
+
+ /**
+ * Sets the thread local for the current root. This method is used by the
+ * framework to set the current application whenever a new request is
+ * processed and it is cleared when the request has been processed.
+ * <p>
+ * The application developer can also use this method to define the current
+ * root outside the normal request handling, e.g. when initiating custom
+ * background threads.
+ * </p>
+ *
+ * @param root
+ * the root to register as the current root
+ *
+ * @see #getCurrent()
+ * @see ThreadLocal
+ */
+ public static void setCurrent(Root root) {
+ currentRoot.set(root);
+ }
+
+ /**
+ * Gets the currently used root. The current root is automatically defined
+ * when processing requests to the server. In other cases, (e.g. from
+ * background threads), the current root is not automatically defined.
+ *
+ * @return the current root instance if available, otherwise
+ * <code>null</code>
+ *
+ * @see #setCurrent(Root)
+ */
+ public static Root getCurrent() {
+ return currentRoot.get();
+ }
+
+ public void setScrollTop(int scrollTop) {
+ throw new RuntimeException("Not yet implemented");
+ }
+
+ @Override
+ protected ActionManager getActionManager() {
+ if (actionManager == null) {
+ actionManager = new ActionManager(this);
+ }
+ return actionManager;
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void addAction(
+ T action) {
+ getActionManager().addAction(action);
+ }
+
+ @Override
+ public <T extends Action & com.vaadin.event.Action.Listener> void removeAction(
+ T action) {
+ if (actionManager != null) {
+ actionManager.removeAction(action);
+ }
+ }
+
+ @Override
+ public void addActionHandler(Handler actionHandler) {
+ getActionManager().addActionHandler(actionHandler);
+ }
+
+ @Override
+ public void removeActionHandler(Handler actionHandler) {
+ if (actionManager != null) {
+ actionManager.removeActionHandler(actionHandler);
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * Default value: <code>false</code>
+ *
+ * @param resizeLazy
+ * true to use a delay before recalculating sizes, false to
+ * calculate immediately.
+ */
+ public void setResizeLazy(boolean resizeLazy) {
+ this.resizeLazy = resizeLazy;
+ requestRepaint();
+ }
+
+ /**
+ * Checks whether lazy resize is enabled.
+ *
+ * @return <code>true</code> if lazy resize is enabled, <code>false</code>
+ * if lazy resize is not enabled
+ */
+ public boolean isResizeLazy() {
+ return resizeLazy;
+ }
+
+ /**
+ * Add a click listener to the Root. The listener is called whenever the
+ * user clicks inside the Root. Also when the click targets a component
+ * inside the Root, provided the targeted component does not prevent the
+ * click event from propagating.
+ *
+ * Use {@link #removeListener(ClickListener)} to remove the listener.
+ *
+ * @param listener
+ * The listener to add
+ */
+ public void addListener(ClickListener listener) {
+ addListener(CLICK_EVENT_ID, ClickEvent.class, listener,
+ ClickListener.clickMethod);
+ }
+
+ /**
+ * Remove a click listener from the Root. The listener should earlier have
+ * been added using {@link #addListener(ClickListener)}.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeListener(ClickListener listener) {
+ removeListener(CLICK_EVENT_ID, ClickEvent.class, listener);
+ }
+
+ @Override
+ public boolean isConnectorEnabled() {
+ // TODO How can a Root be invisible? What does it mean?
+ return isVisible() && isEnabled();
+ }
+
+ public ConnectorTracker getConnectorTracker() {
+ return connectorTracker;
+ }
+
+ public Page getPage() {
+ return page;
+ }
+
+ /**
+ * Setting the caption of a Root is not supported. To set the title of the
+ * HTML page, use Page.setTitle
+ *
+ * @deprecated as of 7.0.0, use {@link Page#setTitle(String)}
+ */
+ @Override
+ @Deprecated
+ public void setCaption(String caption) {
+ throw new IllegalStateException(
+ "You can not set the title of a Root. To set the title of the HTML page, use Page.setTitle");
+ }
+
+ /**
+ * Shows a notification message on the middle of the root. The message
+ * automatically disappears ("humanized message").
+ *
+ * Care should be taken to to avoid XSS vulnerabilities as the caption is
+ * rendered as html.
+ *
+ * @see #showNotification(Notification)
+ * @see Notification
+ *
+ * @param caption
+ * The message
+ *
+ * @deprecated As of 7.0, use Notification.show instead but be aware that
+ * Notification.show does not allow HTML.
+ */
+ @Deprecated
+ public void showNotification(String caption) {
+ Notification notification = new Notification(caption);
+ notification.setHtmlContentAllowed(true);// Backwards compatibility
+ getPage().showNotification(notification);
+ }
+
+ /**
+ * Shows a notification message the root. The position and behavior of the
+ * message depends on the type, which is one of the basic types defined in
+ * {@link Notification}, for instance Notification.TYPE_WARNING_MESSAGE.
+ *
+ * Care should be taken to to avoid XSS vulnerabilities as the caption is
+ * rendered as html.
+ *
+ * @see #showNotification(Notification)
+ * @see Notification
+ *
+ * @param caption
+ * The message
+ * @param type
+ * The message type
+ *
+ * @deprecated As of 7.0, use Notification.show instead but be aware that
+ * Notification.show does not allow HTML.
+ */
+ @Deprecated
+ public void showNotification(String caption, int type) {
+ Notification notification = new Notification(caption, type);
+ notification.setHtmlContentAllowed(true);// Backwards compatibility
+ getPage().showNotification(notification);
+ }
+
+ /**
+ * Shows a notification consisting of a bigger caption and a smaller
+ * description on the middle of the root. The message automatically
+ * disappears ("humanized message").
+ *
+ * Care should be taken to to avoid XSS vulnerabilities as the caption and
+ * description are rendered as html.
+ *
+ * @see #showNotification(Notification)
+ * @see Notification
+ *
+ * @param caption
+ * The caption of the message
+ * @param description
+ * The message description
+ *
+ * @deprecated As of 7.0, use new Notification(...).show(Page) instead but
+ * be aware that HTML by default not allowed.
+ */
+ @Deprecated
+ public void showNotification(String caption, String description) {
+ Notification notification = new Notification(caption, description);
+ notification.setHtmlContentAllowed(true);// Backwards compatibility
+ getPage().showNotification(notification);
+ }
+
+ /**
+ * Shows a notification consisting of a bigger caption and a smaller
+ * description. The position and behavior of the message depends on the
+ * type, which is one of the basic types defined in {@link Notification} ,
+ * for instance Notification.TYPE_WARNING_MESSAGE.
+ *
+ * Care should be taken to to avoid XSS vulnerabilities as the caption and
+ * description are rendered as html.
+ *
+ * @see #showNotification(Notification)
+ * @see Notification
+ *
+ * @param caption
+ * The caption of the message
+ * @param description
+ * The message description
+ * @param type
+ * The message type
+ *
+ * @deprecated As of 7.0, use new Notification(...).show(Page) instead but
+ * be aware that HTML by default not allowed.
+ */
+ @Deprecated
+ public void showNotification(String caption, String description, int type) {
+ Notification notification = new Notification(caption, description, type);
+ notification.setHtmlContentAllowed(true);// Backwards compatibility
+ getPage().showNotification(notification);
+ }
+
+ /**
+ * Shows a notification consisting of a bigger caption and a smaller
+ * description. The position and behavior of the message depends on the
+ * type, which is one of the basic types defined in {@link Notification} ,
+ * for instance Notification.TYPE_WARNING_MESSAGE.
+ *
+ * Care should be taken to avoid XSS vulnerabilities if html content is
+ * allowed.
+ *
+ * @see #showNotification(Notification)
+ * @see Notification
+ *
+ * @param caption
+ * The message caption
+ * @param description
+ * The message description
+ * @param type
+ * The type of message
+ * @param htmlContentAllowed
+ * Whether html in the caption and description should be
+ * displayed as html or as plain text
+ *
+ * @deprecated As of 7.0, use new Notification(...).show(Page).
+ */
+ @Deprecated
+ public void showNotification(String caption, String description, int type,
+ boolean htmlContentAllowed) {
+ getPage()
+ .showNotification(
+ new Notification(caption, description, type,
+ htmlContentAllowed));
+ }
+
+ /**
+ * Shows a notification message.
+ *
+ * @see Notification
+ * @see #showNotification(String)
+ * @see #showNotification(String, int)
+ * @see #showNotification(String, String)
+ * @see #showNotification(String, String, int)
+ *
+ * @param notification
+ * The notification message to show
+ *
+ * @deprecated As of 7.0, use Notification.show instead
+ */
+ @Deprecated
+ public void showNotification(Notification notification) {
+ getPage().showNotification(notification);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Select.java b/server/src/com/vaadin/ui/Select.java
new file mode 100644
index 0000000000..f60935c64b
--- /dev/null
+++ b/server/src/com/vaadin/ui/Select.java
@@ -0,0 +1,803 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+
+/**
+ * <p>
+ * A class representing a selection of items the user has selected in a UI. The
+ * set of choices is presented as a set of {@link com.vaadin.data.Item}s in a
+ * {@link com.vaadin.data.Container}.
+ * </p>
+ *
+ * <p>
+ * A <code>Select</code> component may be in single- or multiselect mode.
+ * Multiselect mode means that more than one item can be selected
+ * simultaneously.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Select extends AbstractSelect implements AbstractSelect.Filtering,
+ FieldEvents.BlurNotifier, FieldEvents.FocusNotifier {
+
+ /**
+ * Holds value of property pageLength. 0 disables paging.
+ */
+ protected int pageLength = 10;
+
+ private int columns = 0;
+
+ // Current page when the user is 'paging' trough options
+ private int currentPage = -1;
+
+ private int filteringMode = FILTERINGMODE_STARTSWITH;
+
+ private String filterstring;
+ private String prevfilterstring;
+
+ /**
+ * Number of options that pass the filter, excluding the null item if any.
+ */
+ private int filteredSize;
+
+ /**
+ * Cache of filtered options, used only by the in-memory filtering system.
+ */
+ private List<Object> filteredOptions;
+
+ /**
+ * Flag to indicate that request repaint is called by filter request only
+ */
+ private boolean optionRequest;
+
+ /**
+ * True if the container is being filtered temporarily and item set change
+ * notifications should be suppressed.
+ */
+ private boolean filteringContainer;
+
+ /**
+ * Flag to indicate whether to scroll the selected item visible (select the
+ * page on which it is) when opening the popup or not. Only applies to
+ * single select mode.
+ *
+ * This requires finding the index of the item, which can be expensive in
+ * many large lazy loading containers.
+ */
+ private boolean scrollToSelectedItem = true;
+
+ /* Constructors */
+
+ /* Component methods */
+
+ public Select() {
+ super();
+ }
+
+ public Select(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public Select(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public Select(String caption) {
+ super(caption);
+ }
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (isMultiSelect()) {
+ // background compatibility hack. This object shouldn't be used for
+ // multiselect lists anymore (ListSelect instead). This fallbacks to
+ // a simpler paint method in super class.
+ super.paintContent(target);
+ // Fix for #4553
+ target.addAttribute("type", "legacy-multi");
+ return;
+ }
+
+ // clear caption change listeners
+ getCaptionChangeListener().clear();
+
+ // The tab ordering number
+ if (getTabIndex() != 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+
+ // If the field is modified, but not committed, set modified attribute
+ if (isModified()) {
+ target.addAttribute("modified", true);
+ }
+
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+
+ boolean needNullSelectOption = false;
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ needNullSelectOption = (getNullSelectionItemId() == null);
+ if (!needNullSelectOption) {
+ target.addAttribute("nullselectitem", true);
+ }
+ }
+
+ // Constructs selected keys array
+ String[] selectedKeys;
+ if (isMultiSelect()) {
+ selectedKeys = new String[((Set<?>) getValue()).size()];
+ } else {
+ selectedKeys = new String[(getValue() == null
+ && getNullSelectionItemId() == null ? 0 : 1)];
+ }
+
+ target.addAttribute("pagelength", pageLength);
+
+ target.addAttribute("filteringmode", getFilteringMode());
+
+ // Paints the options and create array of selected id keys
+ int keyIndex = 0;
+
+ target.startTag("options");
+
+ if (currentPage < 0) {
+ optionRequest = false;
+ currentPage = 0;
+ filterstring = "";
+ }
+
+ boolean nullFilteredOut = filterstring != null
+ && !"".equals(filterstring)
+ && filteringMode != FILTERINGMODE_OFF;
+ // null option is needed and not filtered out, even if not on current
+ // page
+ boolean nullOptionVisible = needNullSelectOption && !nullFilteredOut;
+
+ // first try if using container filters is possible
+ List<?> options = getOptionsWithFilter(nullOptionVisible);
+ if (null == options) {
+ // not able to use container filters, perform explicit in-memory
+ // filtering
+ options = getFilteredOptions();
+ filteredSize = options.size();
+ options = sanitetizeList(options, nullOptionVisible);
+ }
+
+ final boolean paintNullSelection = needNullSelectOption
+ && currentPage == 0 && !nullFilteredOut;
+
+ if (paintNullSelection) {
+ target.startTag("so");
+ target.addAttribute("caption", "");
+ target.addAttribute("key", "");
+ target.endTag("so");
+ }
+
+ final Iterator<?> i = options.iterator();
+ // Paints the available selection options from data source
+
+ while (i.hasNext()) {
+
+ final Object id = i.next();
+
+ if (!isNullSelectionAllowed() && id != null
+ && id.equals(getNullSelectionItemId()) && !isSelected(id)) {
+ continue;
+ }
+
+ // Gets the option attribute values
+ final String key = itemIdMapper.key(id);
+ final String caption = getItemCaption(id);
+ final Resource icon = getItemIcon(id);
+ getCaptionChangeListener().addNotifierForItem(id);
+
+ // Paints the option
+ target.startTag("so");
+ if (icon != null) {
+ target.addAttribute("icon", icon);
+ }
+ target.addAttribute("caption", caption);
+ if (id != null && id.equals(getNullSelectionItemId())) {
+ target.addAttribute("nullselection", true);
+ }
+ target.addAttribute("key", key);
+ if (isSelected(id) && keyIndex < selectedKeys.length) {
+ target.addAttribute("selected", true);
+ selectedKeys[keyIndex++] = key;
+ }
+ target.endTag("so");
+ }
+ target.endTag("options");
+
+ target.addAttribute("totalitems", size()
+ + (needNullSelectOption ? 1 : 0));
+ if (filteredSize > 0 || nullOptionVisible) {
+ target.addAttribute("totalMatches", filteredSize
+ + (nullOptionVisible ? 1 : 0));
+ }
+
+ // Paint variables
+ target.addVariable(this, "selected", selectedKeys);
+ if (isNewItemsAllowed()) {
+ target.addVariable(this, "newitem", "");
+ }
+
+ target.addVariable(this, "filter", filterstring);
+ target.addVariable(this, "page", currentPage);
+
+ currentPage = -1; // current page is always set by client
+
+ optionRequest = true;
+ }
+
+ /**
+ * Returns the filtered options for the current page using a container
+ * filter.
+ *
+ * As a size effect, {@link #filteredSize} is set to the total number of
+ * items passing the filter.
+ *
+ * The current container must be {@link Filterable} and {@link Indexed}, and
+ * the filtering mode must be suitable for container filtering (tested with
+ * {@link #canUseContainerFilter()}).
+ *
+ * Use {@link #getFilteredOptions()} and
+ * {@link #sanitetizeList(List, boolean)} if this is not the case.
+ *
+ * @param needNullSelectOption
+ * @return filtered list of options (may be empty) or null if cannot use
+ * container filters
+ */
+ protected List<?> getOptionsWithFilter(boolean needNullSelectOption) {
+ Container container = getContainerDataSource();
+
+ if (pageLength == 0) {
+ // no paging: return all items
+ filteredSize = container.size();
+ return new ArrayList<Object>(container.getItemIds());
+ }
+
+ if (!(container instanceof Filterable)
+ || !(container instanceof Indexed)
+ || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) {
+ return null;
+ }
+
+ Filterable filterable = (Filterable) container;
+
+ Filter filter = buildFilter(filterstring, filteringMode);
+
+ // adding and removing filters leads to extraneous item set
+ // change events from the underlying container, but the ComboBox does
+ // not process or propagate them based on the flag filteringContainer
+ if (filter != null) {
+ filteringContainer = true;
+ filterable.addContainerFilter(filter);
+ }
+
+ Indexed indexed = (Indexed) container;
+
+ int indexToEnsureInView = -1;
+
+ // if not an option request (item list when user changes page), go
+ // to page with the selected item after filtering if accepted by
+ // filter
+ Object selection = getValue();
+ if (isScrollToSelectedItem() && !optionRequest && !isMultiSelect()
+ && selection != null) {
+ // ensure proper page
+ indexToEnsureInView = indexed.indexOfId(selection);
+ }
+
+ filteredSize = container.size();
+ currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
+ indexToEnsureInView, filteredSize);
+ int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
+ filteredSize);
+ int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
+ filteredSize, first);
+
+ List<Object> options = new ArrayList<Object>();
+ for (int i = first; i <= last && i < filteredSize; ++i) {
+ options.add(indexed.getIdByIndex(i));
+ }
+
+ // to the outside, filtering should not be visible
+ if (filter != null) {
+ filterable.removeContainerFilter(filter);
+ filteringContainer = false;
+ }
+
+ return options;
+ }
+
+ /**
+ * Constructs a filter instance to use when using a Filterable container in
+ * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode.
+ *
+ * Note that the client side implementation expects the filter string to
+ * apply to the item caption string it sees, so changing the behavior of
+ * this method can cause problems.
+ *
+ * @param filterString
+ * @param filteringMode
+ * @return
+ */
+ protected Filter buildFilter(String filterString, int filteringMode) {
+ Filter filter = null;
+
+ if (null != filterString && !"".equals(filterString)) {
+ switch (filteringMode) {
+ case FILTERINGMODE_OFF:
+ break;
+ case FILTERINGMODE_STARTSWITH:
+ filter = new SimpleStringFilter(getItemCaptionPropertyId(),
+ filterString, true, true);
+ break;
+ case FILTERINGMODE_CONTAINS:
+ filter = new SimpleStringFilter(getItemCaptionPropertyId(),
+ filterString, true, false);
+ break;
+ }
+ }
+ return filter;
+ }
+
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ if (!filteringContainer) {
+ super.containerItemSetChange(event);
+ }
+ }
+
+ /**
+ * Makes correct sublist of given list of options.
+ *
+ * If paint is not an option request (affected by page or filter change),
+ * page will be the one where possible selection exists.
+ *
+ * Detects proper first and last item in list to return right page of
+ * options. Also, if the current page is beyond the end of the list, it will
+ * be adjusted.
+ *
+ * @param options
+ * @param needNullSelectOption
+ * flag to indicate if nullselect option needs to be taken into
+ * consideration
+ */
+ private List<?> sanitetizeList(List<?> options, boolean needNullSelectOption) {
+
+ if (pageLength != 0 && options.size() > pageLength) {
+
+ int indexToEnsureInView = -1;
+
+ // if not an option request (item list when user changes page), go
+ // to page with the selected item after filtering if accepted by
+ // filter
+ Object selection = getValue();
+ if (isScrollToSelectedItem() && !optionRequest && !isMultiSelect()
+ && selection != null) {
+ // ensure proper page
+ indexToEnsureInView = options.indexOf(selection);
+ }
+
+ int size = options.size();
+ currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
+ indexToEnsureInView, size);
+ int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
+ size);
+ int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
+ size, first);
+ return options.subList(first, last + 1);
+ } else {
+ return options;
+ }
+ }
+
+ /**
+ * Returns the index of the first item on the current page. The index is to
+ * the underlying (possibly filtered) contents. The null item, if any, does
+ * not have an index but takes up a slot on the first page.
+ *
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ * @return first item to show on the UI (index to the filtered list of
+ * options, not taking the null item into consideration if any)
+ */
+ private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption,
+ int size) {
+ // Not all options are visible, find out which ones are on the
+ // current "page".
+ int first = currentPage * pageLength;
+ if (needNullSelectOption && currentPage > 0) {
+ first--;
+ }
+ return first;
+ }
+
+ /**
+ * Returns the index of the last item on the current page. The index is to
+ * the underlying (possibly filtered) contents. If needNullSelectOption is
+ * true, the null item takes up the first slot on the first page,
+ * effectively reducing the first page size by one.
+ *
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ * @param first
+ * index in the filtered view of the first item of the page
+ * @return index in the filtered view of the last item on the page
+ */
+ private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption,
+ int size, int first) {
+ // page length usable for non-null items
+ int effectivePageLength = pageLength
+ - (needNullSelectOption && (currentPage == 0) ? 1 : 0);
+ return Math.min(size - 1, first + effectivePageLength - 1);
+ }
+
+ /**
+ * Adjusts the index of the current page if necessary: make sure the current
+ * page is not after the end of the contents, and optionally go to the page
+ * containg a specific item. There are no side effects but the adjusted page
+ * index is returned.
+ *
+ * @param page
+ * page number to use as the starting point
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param indexToEnsureInView
+ * index of an item that should be included on the page (in the
+ * data set, not counting the null item if any), -1 for none
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ */
+ private int adjustCurrentPage(int page, boolean needNullSelectOption,
+ int indexToEnsureInView, int size) {
+ if (indexToEnsureInView != -1) {
+ int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0))
+ / pageLength;
+ page = newPage;
+ }
+ // adjust the current page if beyond the end of the list
+ if (page * pageLength > size) {
+ page = (size + (needNullSelectOption ? 1 : 0)) / pageLength;
+ }
+ return page;
+ }
+
+ /**
+ * Filters the options in memory and returns the full filtered list.
+ *
+ * This can be less efficient than using container filters, so use
+ * {@link #getOptionsWithFilter(boolean)} if possible (filterable container
+ * and suitable item caption mode etc.).
+ *
+ * @return
+ */
+ protected List<?> getFilteredOptions() {
+ if (null == filterstring || "".equals(filterstring)
+ || FILTERINGMODE_OFF == filteringMode) {
+ prevfilterstring = null;
+ filteredOptions = new LinkedList<Object>(getItemIds());
+ return filteredOptions;
+ }
+
+ if (filterstring.equals(prevfilterstring)) {
+ return filteredOptions;
+ }
+
+ Collection<?> items;
+ if (prevfilterstring != null
+ && filterstring.startsWith(prevfilterstring)) {
+ items = filteredOptions;
+ } else {
+ items = getItemIds();
+ }
+ prevfilterstring = filterstring;
+
+ filteredOptions = new LinkedList<Object>();
+ for (final Iterator<?> it = items.iterator(); it.hasNext();) {
+ final Object itemId = it.next();
+ String caption = getItemCaption(itemId);
+ if (caption == null || caption.equals("")) {
+ continue;
+ } else {
+ caption = caption.toLowerCase();
+ }
+ switch (filteringMode) {
+ case FILTERINGMODE_CONTAINS:
+ if (caption.indexOf(filterstring) > -1) {
+ filteredOptions.add(itemId);
+ }
+ break;
+ case FILTERINGMODE_STARTSWITH:
+ default:
+ if (caption.startsWith(filterstring)) {
+ filteredOptions.add(itemId);
+ }
+ break;
+ }
+ }
+
+ return filteredOptions;
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Not calling super.changeVariables due the history of select
+ // component hierarchy
+
+ // Selection change
+ if (variables.containsKey("selected")) {
+ final String[] ka = (String[]) variables.get("selected");
+
+ if (isMultiSelect()) {
+ // Multiselect mode
+
+ // TODO Optimize by adding repaintNotNeeded whan applicaple
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> s = new LinkedList<Object>();
+ for (int i = 0; i < ka.length; i++) {
+ final Object id = itemIdMapper.get(ka[i]);
+ if (id != null && containsId(id)) {
+ s.add(id);
+ }
+ }
+
+ // Limits the deselection to the set of visible items
+ // (non-visible items can not be deselected)
+ final Collection<?> visible = getVisibleItemIds();
+ if (visible != null) {
+ @SuppressWarnings("unchecked")
+ Set<Object> newsel = (Set<Object>) getValue();
+ if (newsel == null) {
+ newsel = new HashSet<Object>();
+ } else {
+ newsel = new HashSet<Object>(newsel);
+ }
+ newsel.removeAll(visible);
+ newsel.addAll(s);
+ setValue(newsel, true);
+ }
+ } else {
+ // Single select mode
+ if (ka.length == 0) {
+
+ // Allows deselection only if the deselected item is visible
+ final Object current = getValue();
+ final Collection<?> visible = getVisibleItemIds();
+ if (visible != null && visible.contains(current)) {
+ setValue(null, true);
+ }
+ } else {
+ final Object id = itemIdMapper.get(ka[0]);
+ if (id != null && id.equals(getNullSelectionItemId())) {
+ setValue(null, true);
+ } else {
+ setValue(id, true);
+ }
+ }
+ }
+ }
+
+ String newFilter;
+ if ((newFilter = (String) variables.get("filter")) != null) {
+ // this is a filter request
+ currentPage = ((Integer) variables.get("page")).intValue();
+ filterstring = newFilter;
+ if (filterstring != null) {
+ filterstring = filterstring.toLowerCase();
+ }
+ optionRepaint();
+ } else if (isNewItemsAllowed()) {
+ // New option entered (and it is allowed)
+ final String newitem = (String) variables.get("newitem");
+ if (newitem != null && newitem.length() > 0) {
+ getNewItemHandler().addNewItem(newitem);
+ // rebuild list
+ filterstring = null;
+ prevfilterstring = null;
+ }
+ }
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+
+ }
+
+ @Override
+ public void requestRepaint() {
+ super.requestRepaint();
+ optionRequest = false;
+ prevfilterstring = filterstring;
+ filterstring = null;
+ }
+
+ private void optionRepaint() {
+ super.requestRepaint();
+ }
+
+ @Override
+ public void setFilteringMode(int filteringMode) {
+ this.filteringMode = filteringMode;
+ }
+
+ @Override
+ public int getFilteringMode() {
+ return filteringMode;
+ }
+
+ /**
+ * Note, one should use more generic setWidth(String) method instead of
+ * this. This now days actually converts columns to width with em css unit.
+ *
+ * Sets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ *
+ * @deprecated
+ *
+ * @param columns
+ * the number of columns to set.
+ */
+ @Deprecated
+ public void setColumns(int columns) {
+ if (columns < 0) {
+ columns = 0;
+ }
+ if (this.columns != columns) {
+ this.columns = columns;
+ setWidth(columns, Select.UNITS_EM);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * @deprecated see setter function
+ * @return
+ */
+ @Deprecated
+ public int getColumns() {
+ return columns;
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+
+ }
+
+ /**
+ * @deprecated use {@link ListSelect}, {@link OptionGroup} or
+ * {@link TwinColSelect} instead
+ * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean)
+ * @throws UnsupportedOperationException
+ * if trying to activate multiselect mode
+ */
+ @Deprecated
+ @Override
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect) {
+ throw new UnsupportedOperationException("Multiselect not supported");
+ }
+ }
+
+ /**
+ * @deprecated use {@link ListSelect}, {@link OptionGroup} or
+ * {@link TwinColSelect} instead
+ *
+ * @see com.vaadin.ui.AbstractSelect#isMultiSelect()
+ */
+ @Deprecated
+ @Override
+ public boolean isMultiSelect() {
+ return super.isMultiSelect();
+ }
+
+ /**
+ * Sets whether to scroll the selected item visible (directly open the page
+ * on which it is) when opening the combo box popup or not. Only applies to
+ * single select mode.
+ *
+ * This requires finding the index of the item, which can be expensive in
+ * many large lazy loading containers.
+ *
+ * @param scrollToSelectedItem
+ * true to find the page with the selected item when opening the
+ * selection popup
+ */
+ public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
+ this.scrollToSelectedItem = scrollToSelectedItem;
+ }
+
+ /**
+ * Returns true if the select should find the page with the selected item
+ * when opening the popup (single select combo box only).
+ *
+ * @see #setScrollToSelectedItem(boolean)
+ *
+ * @return true if the page with the selected item will be shown when
+ * opening the popup
+ */
+ public boolean isScrollToSelectedItem() {
+ return scrollToSelectedItem;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Slider.java b/server/src/com/vaadin/ui/Slider.java
new file mode 100644
index 0000000000..94afe4e2bd
--- /dev/null
+++ b/server/src/com/vaadin/ui/Slider.java
@@ -0,0 +1,372 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Map;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+
+/**
+ * A component for selecting a numerical value within a range.
+ *
+ * Example code: <code>
+ * class MyPlayer extends CustomComponent implements ValueChangeListener {
+ *
+ * Label volumeIndicator = new Label();
+ * Slider slider;
+ *
+ * public MyPlayer() {
+ * VerticalLayout vl = new VerticalLayout();
+ * setCompositionRoot(vl);
+ * slider = new Slider("Volume", 0, 100);
+ * slider.setImmediate(true);
+ * slider.setValue(new Double(50));
+ * vl.addComponent(slider);
+ * vl.addComponent(volumeIndicator);
+ * volumeIndicator.setValue("Current volume:" + 50.0);
+ * slider.addListener(this);
+ *
+ * }
+ *
+ * public void setVolume(double d) {
+ * volumeIndicator.setValue("Current volume: " + d);
+ * }
+ *
+ * public void valueChange(ValueChangeEvent event) {
+ * Double d = (Double) event.getProperty().getValue();
+ * setVolume(d.doubleValue());
+ * }
+ * }
+ *
+ * </code>
+ *
+ * @author Vaadin Ltd.
+ */
+public class Slider extends AbstractField<Double> implements Vaadin6Component {
+
+ public static final int ORIENTATION_HORIZONTAL = 0;
+
+ public static final int ORIENTATION_VERTICAL = 1;
+
+ /** Minimum value of slider */
+ private double min = 0;
+
+ /** Maximum value of slider */
+ private double max = 100;
+
+ /**
+ * Resolution, how many digits are considered relevant after the decimal
+ * point. Must be a non-negative value
+ */
+ private int resolution = 0;
+
+ /**
+ * Slider orientation (horizontal/vertical), defaults .
+ */
+ private int orientation = ORIENTATION_HORIZONTAL;
+
+ /**
+ * Default slider constructor. Sets all values to defaults and the slide
+ * handle at minimum value.
+ *
+ */
+ public Slider() {
+ super();
+ super.setValue(new Double(min));
+ }
+
+ /**
+ * Create a new slider with the caption given as parameter.
+ *
+ * The range of the slider is set to 0-100 and only integer values are
+ * allowed.
+ *
+ * @param caption
+ * The caption for this slider (e.g. "Volume").
+ */
+ public Slider(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Create a new slider with the given range and resolution.
+ *
+ * @param min
+ * The minimum value of the slider
+ * @param max
+ * The maximum value of the slider
+ * @param resolution
+ * The number of digits after the decimal point.
+ */
+ public Slider(double min, double max, int resolution) {
+ this();
+ setMin(min);
+ setMax(max);
+ setResolution(resolution);
+ }
+
+ /**
+ * Create a new slider with the given range that only allows integer values.
+ *
+ * @param min
+ * The minimum value of the slider
+ * @param max
+ * The maximum value of the slider
+ */
+ public Slider(int min, int max) {
+ this();
+ setMin(min);
+ setMax(max);
+ setResolution(0);
+ }
+
+ /**
+ * Create a new slider with the given caption and range that only allows
+ * integer values.
+ *
+ * @param caption
+ * The caption for the slider
+ * @param min
+ * The minimum value of the slider
+ * @param max
+ * The maximum value of the slider
+ */
+ public Slider(String caption, int min, int max) {
+ this(min, max);
+ setCaption(caption);
+ }
+
+ /**
+ * Gets the maximum slider value
+ *
+ * @return the largest value the slider can have
+ */
+ public double getMax() {
+ return max;
+ }
+
+ /**
+ * Set the maximum slider value. If the current value of the slider is
+ * larger than this, the value is set to the new maximum.
+ *
+ * @param max
+ * The new maximum slider value
+ */
+ public void setMax(double max) {
+ this.max = max;
+ if (getValue() > max) {
+ setValue(max);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Gets the minimum slider value
+ *
+ * @return the smallest value the slider can have
+ */
+ public double getMin() {
+ return min;
+ }
+
+ /**
+ * Set the minimum slider value. If the current value of the slider is
+ * smaller than this, the value is set to the new minimum.
+ *
+ * @param max
+ * The new minimum slider value
+ */
+ public void setMin(double min) {
+ this.min = min;
+ if (getValue() < min) {
+ setValue(min);
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Get the current orientation of the slider (horizontal or vertical).
+ *
+ * @return {@link #ORIENTATION_HORIZONTAL} or
+ * {@link #ORIENTATION_HORIZONTAL}
+ */
+ public int getOrientation() {
+ return orientation;
+ }
+
+ /**
+ * Set the orientation of the slider.
+ *
+ * @param The
+ * new orientation, either {@link #ORIENTATION_HORIZONTAL} or
+ * {@link #ORIENTATION_VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ this.orientation = orientation;
+ requestRepaint();
+ }
+
+ /**
+ * Get the current resolution of the slider. The resolution is the number of
+ * digits after the decimal point.
+ *
+ * @return resolution
+ */
+ public int getResolution() {
+ return resolution;
+ }
+
+ /**
+ * Set a new resolution for the slider. The resolution is the number of
+ * digits after the decimal point.
+ *
+ * @param resolution
+ */
+ public void setResolution(int resolution) {
+ if (resolution < 0) {
+ return;
+ }
+ this.resolution = resolution;
+ requestRepaint();
+ }
+
+ /**
+ * Sets the value of the slider.
+ *
+ * @param value
+ * The new value of the slider.
+ * @param repaintIsNotNeeded
+ * If true, client-side is not requested to repaint itself.
+ * @throws ValueOutOfBoundsException
+ * If the given value is not inside the range of the slider.
+ * @see #setMin(double) {@link #setMax(double)}
+ */
+ @Override
+ protected void setValue(Double value, boolean repaintIsNotNeeded) {
+ final double v = value.doubleValue();
+ double newValue;
+ if (resolution > 0) {
+ // Round up to resolution
+ newValue = (int) (v * Math.pow(10, resolution));
+ newValue = newValue / Math.pow(10, resolution);
+ if (min > newValue || max < newValue) {
+ throw new ValueOutOfBoundsException(value);
+ }
+ } else {
+ newValue = (int) v;
+ if (min > newValue || max < newValue) {
+ throw new ValueOutOfBoundsException(value);
+ }
+ }
+ super.setValue(newValue, repaintIsNotNeeded);
+ }
+
+ @Override
+ public void setValue(Object newFieldValue)
+ throws com.vaadin.data.Property.ReadOnlyException {
+ if (newFieldValue != null && newFieldValue instanceof Number
+ && !(newFieldValue instanceof Double)) {
+ // Support setting all types of Numbers
+ newFieldValue = ((Number) newFieldValue).doubleValue();
+ }
+
+ super.setValue(newFieldValue);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ target.addAttribute("min", min);
+ if (max > min) {
+ target.addAttribute("max", max);
+ } else {
+ target.addAttribute("max", min);
+ }
+ target.addAttribute("resolution", resolution);
+
+ if (resolution > 0) {
+ target.addVariable(this, "value", getValue().doubleValue());
+ } else {
+ target.addVariable(this, "value", getValue().intValue());
+ }
+
+ if (orientation == ORIENTATION_VERTICAL) {
+ target.addAttribute("vertical", true);
+ }
+
+ }
+
+ /**
+ * Invoked when the value of a variable has changed. Slider listeners are
+ * notified if the slider value has changed.
+ *
+ * @param source
+ * @param variables
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ if (variables.containsKey("value")) {
+ final Object value = variables.get("value");
+ final Double newValue = new Double(value.toString());
+ if (newValue != null && newValue != getValue()
+ && !newValue.equals(getValue())) {
+ try {
+ setValue(newValue, true);
+ } catch (final ValueOutOfBoundsException e) {
+ // Convert to nearest bound
+ double out = e.getValue().doubleValue();
+ if (out < min) {
+ out = min;
+ }
+ if (out > max) {
+ out = max;
+ }
+ super.setValue(new Double(out), false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Thrown when the value of the slider is about to be set to a value that is
+ * outside the valid range of the slider.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+ public class ValueOutOfBoundsException extends RuntimeException {
+
+ private final Double value;
+
+ /**
+ * Constructs an <code>ValueOutOfBoundsException</code> with the
+ * specified detail message.
+ *
+ * @param valueOutOfBounds
+ */
+ public ValueOutOfBoundsException(Double valueOutOfBounds) {
+ value = valueOutOfBounds;
+ }
+
+ /**
+ * Gets the value that is outside the valid range of the slider.
+ *
+ * @return the value that is out of bounds
+ */
+ public Double getValue() {
+ return value;
+ }
+
+ }
+
+ @Override
+ public Class<Double> getType() {
+ return Double.class;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/TabSheet.java b/server/src/com/vaadin/ui/TabSheet.java
new file mode 100644
index 0000000000..c52e9394c0
--- /dev/null
+++ b/server/src/com/vaadin/ui/TabSheet.java
@@ -0,0 +1,1328 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.BlurNotifier;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.FieldEvents.FocusNotifier;
+import com.vaadin.terminal.ErrorMessage;
+import com.vaadin.terminal.KeyMapper;
+import com.vaadin.terminal.LegacyPaint;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector;
+import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheet;
+import com.vaadin.ui.Component.Focusable;
+import com.vaadin.ui.themes.Reindeer;
+import com.vaadin.ui.themes.Runo;
+
+/**
+ * TabSheet component.
+ *
+ * Tabs are typically identified by the component contained on the tab (see
+ * {@link ComponentContainer}), and tab metadata (including caption, icon,
+ * visibility, enabledness, closability etc.) is kept in separate {@link Tab}
+ * instances.
+ *
+ * Tabs added with {@link #addComponent(Component)} get the caption and the icon
+ * of the component at the time when the component is created, and these are not
+ * automatically updated after tab creation.
+ *
+ * A tab sheet can have multiple tab selection listeners and one tab close
+ * handler ({@link CloseHandler}), which by default removes the tab from the
+ * TabSheet.
+ *
+ * The {@link TabSheet} can be styled with the .v-tabsheet, .v-tabsheet-tabs and
+ * .v-tabsheet-content styles. Themes may also have pre-defined variations of
+ * the tab sheet presentation, such as {@link Reindeer#TABSHEET_BORDERLESS},
+ * {@link Runo#TABSHEET_SMALL} and several other styles in {@link Reindeer}.
+ *
+ * The current implementation does not load the tabs to the UI before the first
+ * time they are shown, but this may change in future releases.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class TabSheet extends AbstractComponentContainer implements Focusable,
+ FocusNotifier, BlurNotifier, Vaadin6Component {
+
+ /**
+ * List of component tabs (tab contents). In addition to being on this list,
+ * there is a {@link Tab} object in tabs for each tab with meta-data about
+ * the tab.
+ */
+ private final ArrayList<Component> components = new ArrayList<Component>();
+
+ /**
+ * Map containing information related to the tabs (caption, icon etc).
+ */
+ private final HashMap<Component, Tab> tabs = new HashMap<Component, Tab>();
+
+ /**
+ * Selected tab content component.
+ */
+ private Component selected = null;
+
+ /**
+ * Mapper between server-side component instances (tab contents) and keys
+ * given to the client that identify tabs.
+ */
+ private final KeyMapper<Component> keyMapper = new KeyMapper<Component>();
+
+ /**
+ * When true, the tab selection area is not displayed to the user.
+ */
+ private boolean tabsHidden;
+
+ /**
+ * Handler to be called when a tab is closed.
+ */
+ private CloseHandler closeHandler;
+
+ private int tabIndex;
+
+ /**
+ * Constructs a new Tabsheet. Tabsheet is immediate by default, and the
+ * default close handler removes the tab being closed.
+ */
+ public TabSheet() {
+ super();
+ // expand horizontally by default
+ setWidth(100, UNITS_PERCENTAGE);
+ setImmediate(true);
+ setCloseHandler(new CloseHandler() {
+
+ @Override
+ public void onTabClose(TabSheet tabsheet, Component c) {
+ tabsheet.removeComponent(c);
+ }
+ });
+ }
+
+ /**
+ * Gets the component container iterator for going through all the
+ * components (tab contents).
+ *
+ * @return the unmodifiable Iterator of the tab content components
+ */
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ return Collections.unmodifiableList(components).iterator();
+ }
+
+ /**
+ * Gets the number of contained components (tabs). Consistent with the
+ * iterator returned by {@link #getComponentIterator()}.
+ *
+ * @return the number of contained components
+ */
+
+ @Override
+ public int getComponentCount() {
+ return components.size();
+ }
+
+ /**
+ * Removes a component and its corresponding tab.
+ *
+ * If the tab was selected, the first eligible (visible and enabled)
+ * remaining tab is selected.
+ *
+ * @param c
+ * the component to be removed.
+ */
+
+ @Override
+ public void removeComponent(Component c) {
+ if (c != null && components.contains(c)) {
+ super.removeComponent(c);
+ keyMapper.remove(c);
+ components.remove(c);
+ tabs.remove(c);
+ if (c.equals(selected)) {
+ if (components.isEmpty()) {
+ setSelected(null);
+ } else {
+ // select the first enabled and visible tab, if any
+ updateSelection();
+ fireSelectedTabChange();
+ }
+ }
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Removes a {@link Tab} and the component associated with it, as previously
+ * added with {@link #addTab(Component)},
+ * {@link #addTab(Component, String, Resource)} or
+ * {@link #addComponent(Component)}.
+ * <p>
+ * If the tab was selected, the first eligible (visible and enabled)
+ * remaining tab is selected.
+ * </p>
+ *
+ * @see #addTab(Component)
+ * @see #addTab(Component, String, Resource)
+ * @see #addComponent(Component)
+ * @see #removeComponent(Component)
+ * @param tab
+ * the Tab to remove
+ */
+ public void removeTab(Tab tab) {
+ removeComponent(tab.getComponent());
+ }
+
+ /**
+ * Adds a new tab into TabSheet. Component caption and icon are copied to
+ * the tab metadata at creation time.
+ *
+ * @see #addTab(Component)
+ *
+ * @param c
+ * the component to be added.
+ */
+
+ @Override
+ public void addComponent(Component c) {
+ addTab(c);
+ }
+
+ /**
+ * Adds a new tab into TabSheet.
+ *
+ * The first tab added to a tab sheet is automatically selected and a tab
+ * selection event is fired.
+ *
+ * If the component is already present in the tab sheet, changes its caption
+ * and returns the corresponding (old) tab, preserving other tab metadata.
+ *
+ * @param c
+ * the component to be added onto tab - should not be null.
+ * @param caption
+ * the caption to be set for the component and used rendered in
+ * tab bar
+ * @return the created {@link Tab}
+ */
+ public Tab addTab(Component c, String caption) {
+ return addTab(c, caption, null);
+ }
+
+ /**
+ * Adds a new tab into TabSheet.
+ *
+ * The first tab added to a tab sheet is automatically selected and a tab
+ * selection event is fired.
+ *
+ * If the component is already present in the tab sheet, changes its caption
+ * and icon and returns the corresponding (old) tab, preserving other tab
+ * metadata.
+ *
+ * @param c
+ * the component to be added onto tab - should not be null.
+ * @param caption
+ * the caption to be set for the component and used rendered in
+ * tab bar
+ * @param icon
+ * the icon to be set for the component and used rendered in tab
+ * bar
+ * @return the created {@link Tab}
+ */
+ public Tab addTab(Component c, String caption, Resource icon) {
+ return addTab(c, caption, icon, components.size());
+ }
+
+ /**
+ * Adds a new tab into TabSheet.
+ *
+ * The first tab added to a tab sheet is automatically selected and a tab
+ * selection event is fired.
+ *
+ * If the component is already present in the tab sheet, changes its caption
+ * and icon and returns the corresponding (old) tab, preserving other tab
+ * metadata like the position.
+ *
+ * @param c
+ * the component to be added onto tab - should not be null.
+ * @param caption
+ * the caption to be set for the component and used rendered in
+ * tab bar
+ * @param icon
+ * the icon to be set for the component and used rendered in tab
+ * bar
+ * @param position
+ * the position at where the the tab should be added.
+ * @return the created {@link Tab}
+ */
+ public Tab addTab(Component c, String caption, Resource icon, int position) {
+ if (c == null) {
+ return null;
+ } else if (tabs.containsKey(c)) {
+ Tab tab = tabs.get(c);
+ tab.setCaption(caption);
+ tab.setIcon(icon);
+ return tab;
+ } else {
+ components.add(position, c);
+
+ Tab tab = new TabSheetTabImpl(caption, icon);
+
+ tabs.put(c, tab);
+ if (selected == null) {
+ setSelected(c);
+ fireSelectedTabChange();
+ }
+ super.addComponent(c);
+ requestRepaint();
+ return tab;
+ }
+ }
+
+ /**
+ * Adds a new tab into TabSheet. Component caption and icon are copied to
+ * the tab metadata at creation time.
+ *
+ * If the tab sheet already contains the component, its tab is returned.
+ *
+ * @param c
+ * the component to be added onto tab - should not be null.
+ * @return the created {@link Tab}
+ */
+ public Tab addTab(Component c) {
+ return addTab(c, components.size());
+ }
+
+ /**
+ * Adds a new tab into TabSheet. Component caption and icon are copied to
+ * the tab metadata at creation time.
+ *
+ * If the tab sheet already contains the component, its tab is returned.
+ *
+ * @param c
+ * the component to be added onto tab - should not be null.
+ * @param position
+ * The position where the tab should be added
+ * @return the created {@link Tab}
+ */
+ public Tab addTab(Component c, int position) {
+ if (c == null) {
+ return null;
+ } else if (tabs.containsKey(c)) {
+ return tabs.get(c);
+ } else {
+ return addTab(c, c.getCaption(), c.getIcon(), position);
+ }
+ }
+
+ /**
+ * Moves all components from another container to this container. The
+ * components are removed from the other container.
+ *
+ * If the source container is a {@link TabSheet}, component captions and
+ * icons are copied from it.
+ *
+ * @param source
+ * the container components are removed from.
+ */
+
+ @Override
+ public void moveComponentsFrom(ComponentContainer source) {
+ for (final Iterator<Component> i = source.getComponentIterator(); i
+ .hasNext();) {
+ final Component c = i.next();
+ String caption = null;
+ Resource icon = null;
+ if (TabSheet.class.isAssignableFrom(source.getClass())) {
+ caption = ((TabSheet) source).getTabCaption(c);
+ icon = ((TabSheet) source).getTabIcon(c);
+ }
+ source.removeComponent(c);
+ addTab(c, caption, icon);
+
+ }
+ }
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the paint target
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ if (areTabsHidden()) {
+ target.addAttribute("hidetabs", true);
+ }
+
+ if (tabIndex != 0) {
+ target.addAttribute("tabindex", tabIndex);
+ }
+
+ target.startTag("tabs");
+
+ for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ Tab tab = tabs.get(component);
+
+ target.startTag("tab");
+ if (!tab.isEnabled() && tab.isVisible()) {
+ target.addAttribute(
+ TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED, true);
+ }
+
+ if (!tab.isVisible()) {
+ target.addAttribute("hidden", true);
+ }
+
+ if (tab.isClosable()) {
+ target.addAttribute("closable", true);
+ }
+
+ // tab icon, caption and description, but used via
+ // VCaption.updateCaption(uidl)
+ final Resource icon = tab.getIcon();
+ if (icon != null) {
+ target.addAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON,
+ icon);
+ }
+ final String caption = tab.getCaption();
+ if (caption != null && caption.length() > 0) {
+ target.addAttribute(
+ TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION, caption);
+ }
+ ErrorMessage tabError = tab.getComponentError();
+ if (tabError != null) {
+ target.addAttribute(
+ TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE,
+ tabError.getFormattedHtmlMessage());
+ }
+ final String description = tab.getDescription();
+ if (description != null) {
+ target.addAttribute(
+ TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION,
+ description);
+ }
+
+ final String styleName = tab.getStyleName();
+ if (styleName != null && styleName.length() != 0) {
+ target.addAttribute(VTabsheet.TAB_STYLE_NAME, styleName);
+ }
+
+ target.addAttribute("key", keyMapper.key(component));
+ if (component.equals(selected)) {
+ target.addAttribute("selected", true);
+ LegacyPaint.paint(component, target);
+ }
+ target.endTag("tab");
+ }
+
+ target.endTag("tabs");
+
+ if (selected != null) {
+ target.addVariable(this, "selected", keyMapper.key(selected));
+ }
+
+ }
+
+ /**
+ * Are the tab selection parts ("tabs") hidden.
+ *
+ * @return true if the tabs are hidden in the UI
+ */
+ public boolean areTabsHidden() {
+ return tabsHidden;
+ }
+
+ /**
+ * Hides or shows the tab selection parts ("tabs").
+ *
+ * @param tabsHidden
+ * true if the tabs should be hidden
+ */
+ public void hideTabs(boolean tabsHidden) {
+ this.tabsHidden = tabsHidden;
+ requestRepaint();
+ }
+
+ /**
+ * Gets tab caption. The tab is identified by the tab content component.
+ *
+ * @param c
+ * the component in the tab
+ * @deprecated Use {@link #getTab(Component)} and {@link Tab#getCaption()}
+ * instead.
+ */
+ @Deprecated
+ public String getTabCaption(Component c) {
+ Tab info = tabs.get(c);
+ if (info == null) {
+ return "";
+ } else {
+ return info.getCaption();
+ }
+ }
+
+ /**
+ * Sets tab caption. The tab is identified by the tab content component.
+ *
+ * @param c
+ * the component in the tab
+ * @param caption
+ * the caption to set.
+ * @deprecated Use {@link #getTab(Component)} and
+ * {@link Tab#setCaption(String)} instead.
+ */
+ @Deprecated
+ public void setTabCaption(Component c, String caption) {
+ Tab info = tabs.get(c);
+ if (info != null) {
+ info.setCaption(caption);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the icon for a tab. The tab is identified by the tab content
+ * component.
+ *
+ * @param c
+ * the component in the tab
+ * @deprecated Use {@link #getTab(Component)} and {@link Tab#getIcon()}
+ * instead.
+ */
+ @Deprecated
+ public Resource getTabIcon(Component c) {
+ Tab info = tabs.get(c);
+ if (info == null) {
+ return null;
+ } else {
+ return info.getIcon();
+ }
+ }
+
+ /**
+ * Sets icon for the given component. The tab is identified by the tab
+ * content component.
+ *
+ * @param c
+ * the component in the tab
+ * @param icon
+ * the icon to set
+ * @deprecated Use {@link #getTab(Component)} and
+ * {@link Tab#setIcon(Resource)} instead.
+ */
+ @Deprecated
+ public void setTabIcon(Component c, Resource icon) {
+ Tab info = tabs.get(c);
+ if (info != null) {
+ info.setIcon(icon);
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Returns the {@link Tab} (metadata) for a component. The {@link Tab}
+ * object can be used for setting caption,icon, etc for the tab.
+ *
+ * @param c
+ * the component
+ * @return The tab instance associated with the given component, or null if
+ * the tabsheet does not contain the component.
+ */
+ public Tab getTab(Component c) {
+ return tabs.get(c);
+ }
+
+ /**
+ * Returns the {@link Tab} (metadata) for a component. The {@link Tab}
+ * object can be used for setting caption,icon, etc for the tab.
+ *
+ * @param position
+ * the position of the tab
+ * @return The tab in the given position, or null if the position is out of
+ * bounds.
+ */
+ public Tab getTab(int position) {
+ if (position >= 0 && position < getComponentCount()) {
+ return getTab(components.get(position));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Sets the selected tab. The tab is identified by the tab content
+ * component. Does nothing if the tabsheet doesn't contain the component.
+ *
+ * @param c
+ */
+ public void setSelectedTab(Component c) {
+ if (c != null && components.contains(c) && !c.equals(selected)) {
+ setSelected(c);
+ updateSelection();
+ fireSelectedTabChange();
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Sets the selected tab in the TabSheet. Ensures that the selected tab is
+ * repainted if needed.
+ *
+ * @param c
+ * The new selection or null for no selection
+ */
+ private void setSelected(Component c) {
+ selected = c;
+ // Repaint of the selected component is needed as only the selected
+ // component is communicated to the client. Otherwise this will be a
+ // "cached" update even though the client knows nothing about the
+ // connector
+ if (selected instanceof ComponentContainer) {
+ ((ComponentContainer) selected).requestRepaintAll();
+ } else if (selected instanceof Table) {
+ // Workaround until there's a generic way of telling a component
+ // that there is no client side state to rely on. See #8642
+ ((Table) selected).refreshRowCache();
+ } else if (selected != null) {
+ selected.requestRepaint();
+ }
+
+ }
+
+ /**
+ * Sets the selected tab. The tab is identified by the corresponding
+ * {@link Tab Tab} instance. Does nothing if the tabsheet doesn't contain
+ * the given tab.
+ *
+ * @param tab
+ */
+ public void setSelectedTab(Tab tab) {
+ if (tab != null) {
+ setSelectedTab(tab.getComponent());
+ }
+ }
+
+ /**
+ * Sets the selected tab, identified by its position. Does nothing if the
+ * position is out of bounds.
+ *
+ * @param position
+ */
+ public void setSelectedTab(int position) {
+ setSelectedTab(getTab(position));
+ }
+
+ /**
+ * Checks if the current selection is valid, and updates the selection if
+ * the previously selected component is not visible and enabled. The first
+ * visible and enabled tab is selected if the current selection is empty or
+ * invalid.
+ *
+ * This method does not fire tab change events, but the caller should do so
+ * if appropriate.
+ *
+ * @return true if selection was changed, false otherwise
+ */
+ private boolean updateSelection() {
+ Component originalSelection = selected;
+ for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ Tab tab = tabs.get(component);
+
+ /*
+ * If we have no selection, if the current selection is invisible or
+ * if the current selection is disabled (but the whole component is
+ * not) we select this tab instead
+ */
+ Tab selectedTabInfo = null;
+ if (selected != null) {
+ selectedTabInfo = tabs.get(selected);
+ }
+ if (selected == null || selectedTabInfo == null
+ || !selectedTabInfo.isVisible()
+ || !selectedTabInfo.isEnabled()) {
+
+ // The current selection is not valid so we need to change
+ // it
+ if (tab.isEnabled() && tab.isVisible()) {
+ setSelected(component);
+ break;
+ } else {
+ /*
+ * The current selection is not valid but this tab cannot be
+ * selected either.
+ */
+ setSelected(null);
+ }
+ }
+ }
+ return originalSelection != selected;
+ }
+
+ /**
+ * Gets the selected tab content component.
+ *
+ * @return the selected tab contents
+ */
+ public Component getSelectedTab() {
+ return selected;
+ }
+
+ // inherits javadoc
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ if (variables.containsKey("selected")) {
+ setSelectedTab(keyMapper.get((String) variables.get("selected")));
+ }
+ if (variables.containsKey("close")) {
+ final Component tab = keyMapper
+ .get((String) variables.get("close"));
+ if (tab != null) {
+ closeHandler.onTabClose(this, tab);
+ }
+ }
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+ }
+
+ /**
+ * Replaces a component (tab content) with another. This can be used to
+ * change tab contents or to rearrange tabs. The tab position and some
+ * metadata are preserved when moving components within the same
+ * {@link TabSheet}.
+ *
+ * If the oldComponent is not present in the tab sheet, the new one is added
+ * at the end.
+ *
+ * If the oldComponent is already in the tab sheet but the newComponent
+ * isn't, the old tab is replaced with a new one, and the caption and icon
+ * of the old one are copied to the new tab.
+ *
+ * If both old and new components are present, their positions are swapped.
+ *
+ * {@inheritDoc}
+ */
+
+ @Override
+ public void replaceComponent(Component oldComponent, Component newComponent) {
+
+ if (selected == oldComponent) {
+ // keep selection w/o selectedTabChange event
+ setSelected(newComponent);
+ }
+
+ Tab newTab = tabs.get(newComponent);
+ Tab oldTab = tabs.get(oldComponent);
+
+ // Gets the locations
+ int oldLocation = -1;
+ int newLocation = -1;
+ int location = 0;
+ for (final Iterator<Component> i = components.iterator(); i.hasNext();) {
+ final Component component = i.next();
+
+ if (component == oldComponent) {
+ oldLocation = location;
+ }
+ if (component == newComponent) {
+ newLocation = location;
+ }
+
+ location++;
+ }
+
+ if (oldLocation == -1) {
+ addComponent(newComponent);
+ } else if (newLocation == -1) {
+ removeComponent(oldComponent);
+ newTab = addTab(newComponent, oldLocation);
+ // Copy all relevant metadata to the new tab (#8793)
+ // TODO Should reuse the old tab instance instead?
+ copyTabMetadata(oldTab, newTab);
+ } else {
+ components.set(oldLocation, newComponent);
+ components.set(newLocation, oldComponent);
+
+ // Tab associations are not changed, but metadata is swapped between
+ // the instances
+ // TODO Should reassociate the instances instead?
+ Tab tmp = new TabSheetTabImpl(null, null);
+ copyTabMetadata(newTab, tmp);
+ copyTabMetadata(oldTab, newTab);
+ copyTabMetadata(tmp, oldTab);
+
+ requestRepaint();
+ }
+
+ }
+
+ /* Click event */
+
+ private static final Method SELECTED_TAB_CHANGE_METHOD;
+ static {
+ try {
+ SELECTED_TAB_CHANGE_METHOD = SelectedTabChangeListener.class
+ .getDeclaredMethod("selectedTabChange",
+ new Class[] { SelectedTabChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in TabSheet");
+ }
+ }
+
+ /**
+ * Selected tab change event. This event is sent when the selected (shown)
+ * tab in the tab sheet is changed.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public class SelectedTabChangeEvent extends Component.Event {
+
+ /**
+ * New instance of selected tab change event
+ *
+ * @param source
+ * the Source of the event.
+ */
+ public SelectedTabChangeEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * TabSheet where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public TabSheet getTabSheet() {
+ return (TabSheet) getSource();
+ }
+ }
+
+ /**
+ * Selected tab change event listener. The listener is called whenever
+ * another tab is selected, including when adding the first tab to a
+ * tabsheet.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface SelectedTabChangeListener extends Serializable {
+
+ /**
+ * Selected (shown) tab in tab sheet has has been changed.
+ *
+ * @param event
+ * the selected tab change event.
+ */
+ public void selectedTabChange(SelectedTabChangeEvent event);
+ }
+
+ /**
+ * Adds a tab selection listener
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(SelectedTabChangeListener listener) {
+ addListener(SelectedTabChangeEvent.class, listener,
+ SELECTED_TAB_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a tab selection listener
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(SelectedTabChangeListener listener) {
+ removeListener(SelectedTabChangeEvent.class, listener,
+ SELECTED_TAB_CHANGE_METHOD);
+ }
+
+ /**
+ * Sends an event that the currently selected tab has changed.
+ */
+ protected void fireSelectedTabChange() {
+ fireEvent(new SelectedTabChangeEvent(this));
+ }
+
+ /**
+ * Tab meta-data for a component in a {@link TabSheet}.
+ *
+ * The meta-data includes the tab caption, icon, visibility and enabledness,
+ * closability, description (tooltip) and an optional component error shown
+ * in the tab.
+ *
+ * Tabs are identified by the component contained on them in most cases, and
+ * the meta-data can be obtained with {@link TabSheet#getTab(Component)}.
+ */
+ public interface Tab extends Serializable {
+ /**
+ * Returns the visible status for the tab. An invisible tab is not shown
+ * in the tab bar and cannot be selected.
+ *
+ * @return true for visible, false for hidden
+ */
+ public boolean isVisible();
+
+ /**
+ * Sets the visible status for the tab. An invisible tab is not shown in
+ * the tab bar and cannot be selected, selection is changed
+ * automatically when there is an attempt to select an invisible tab.
+ *
+ * @param visible
+ * true for visible, false for hidden
+ */
+ public void setVisible(boolean visible);
+
+ /**
+ * Returns the closability status for the tab.
+ *
+ * @return true if the tab is allowed to be closed by the end user,
+ * false for not allowing closing
+ */
+ public boolean isClosable();
+
+ /**
+ * Sets the closability status for the tab. A closable tab can be closed
+ * by the user through the user interface. This also controls if a close
+ * button is shown to the user or not.
+ * <p>
+ * Note! Currently only supported by TabSheet, not Accordion.
+ * </p>
+ *
+ * @param visible
+ * true if the end user is allowed to close the tab, false
+ * for not allowing to close. Should default to false.
+ */
+ public void setClosable(boolean closable);
+
+ /**
+ * Returns the enabled status for the tab. A disabled tab is shown as
+ * such in the tab bar and cannot be selected.
+ *
+ * @return true for enabled, false for disabled
+ */
+ public boolean isEnabled();
+
+ /**
+ * Sets the enabled status for the tab. A disabled tab is shown as such
+ * in the tab bar and cannot be selected.
+ *
+ * @param enabled
+ * true for enabled, false for disabled
+ */
+ public void setEnabled(boolean enabled);
+
+ /**
+ * Sets the caption for the tab.
+ *
+ * @param caption
+ * the caption to set
+ */
+ public void setCaption(String caption);
+
+ /**
+ * Gets the caption for the tab.
+ */
+ public String getCaption();
+
+ /**
+ * Gets the icon for the tab.
+ */
+ public Resource getIcon();
+
+ /**
+ * Sets the icon for the tab.
+ *
+ * @param icon
+ * the icon to set
+ */
+ public void setIcon(Resource icon);
+
+ /**
+ * Gets the description for the tab. The description can be used to
+ * briefly describe the state of the tab to the user, and is typically
+ * shown as a tooltip when hovering over the tab.
+ *
+ * @return the description for the tab
+ */
+ public String getDescription();
+
+ /**
+ * Sets the description for the tab. The description can be used to
+ * briefly describe the state of the tab to the user, and is typically
+ * shown as a tooltip when hovering over the tab.
+ *
+ * @param description
+ * the new description string for the tab.
+ */
+ public void setDescription(String description);
+
+ /**
+ * Sets an error indicator to be shown in the tab. This can be used e.g.
+ * to communicate to the user that there is a problem in the contents of
+ * the tab.
+ *
+ * @see AbstractComponent#setComponentError(ErrorMessage)
+ *
+ * @param componentError
+ * error message or null for none
+ */
+ public void setComponentError(ErrorMessage componentError);
+
+ /**
+ * Gets the current error message shown for the tab.
+ *
+ * TODO currently not sent to the client
+ *
+ * @see AbstractComponent#setComponentError(ErrorMessage)
+ */
+ public ErrorMessage getComponentError();
+
+ /**
+ * Get the component related to the Tab
+ */
+ public Component getComponent();
+
+ /**
+ * Sets a style name for the tab. The style name will be rendered as a
+ * HTML class name, which can be used in a CSS definition.
+ *
+ * <pre>
+ * Tab tab = tabsheet.addTab(tabContent, &quot;Tab text&quot;);
+ * tab.setStyleName(&quot;mystyle&quot;);
+ * </pre>
+ * <p>
+ * The used style name will be prefixed with "
+ * {@code v-tabsheet-tabitemcell-}". For example, if you give a tab the
+ * style "{@code mystyle}", the tab will get a "
+ * {@code v-tabsheet-tabitemcell-mystyle}" style. You could then style
+ * the component with:
+ * </p>
+ *
+ * <pre>
+ * .v-tabsheet-tabitemcell-mystyle {font-style: italic;}
+ * </pre>
+ *
+ * <p>
+ * This method will trigger a {@link RepaintRequestEvent} on the
+ * TabSheet to which the Tab belongs.
+ * </p>
+ *
+ * @param styleName
+ * the new style to be set for tab
+ * @see #getStyleName()
+ */
+ public void setStyleName(String styleName);
+
+ /**
+ * Gets the user-defined CSS style name of the tab. Built-in style names
+ * defined in Vaadin or GWT are not returned.
+ *
+ * @return the style name or of the tab
+ * @see #setStyleName(String)
+ */
+ public String getStyleName();
+ }
+
+ /**
+ * TabSheet's implementation of {@link Tab} - tab metadata.
+ */
+ public class TabSheetTabImpl implements Tab {
+
+ private String caption = "";
+ private Resource icon = null;
+ private boolean enabled = true;
+ private boolean visible = true;
+ private boolean closable = false;
+ private String description = null;
+ private ErrorMessage componentError = null;
+ private String styleName;
+
+ public TabSheetTabImpl(String caption, Resource icon) {
+ if (caption == null) {
+ caption = "";
+ }
+ this.caption = caption;
+ this.icon = icon;
+ }
+
+ /**
+ * Returns the tab caption. Can never be null.
+ */
+
+ @Override
+ public String getCaption() {
+ return caption;
+ }
+
+ @Override
+ public void setCaption(String caption) {
+ this.caption = caption;
+ requestRepaint();
+ }
+
+ @Override
+ public Resource getIcon() {
+ return icon;
+ }
+
+ @Override
+ public void setIcon(Resource icon) {
+ this.icon = icon;
+ requestRepaint();
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (updateSelection()) {
+ fireSelectedTabChange();
+ }
+ requestRepaint();
+ }
+
+ @Override
+ public boolean isVisible() {
+ return visible;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ if (updateSelection()) {
+ fireSelectedTabChange();
+ }
+ requestRepaint();
+ }
+
+ @Override
+ public boolean isClosable() {
+ return closable;
+ }
+
+ @Override
+ public void setClosable(boolean closable) {
+ this.closable = closable;
+ requestRepaint();
+ }
+
+ public void close() {
+
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.description = description;
+ requestRepaint();
+ }
+
+ @Override
+ public ErrorMessage getComponentError() {
+ return componentError;
+ }
+
+ @Override
+ public void setComponentError(ErrorMessage componentError) {
+ this.componentError = componentError;
+ requestRepaint();
+ }
+
+ @Override
+ public Component getComponent() {
+ for (Map.Entry<Component, Tab> entry : tabs.entrySet()) {
+ if (entry.getValue() == this) {
+ return entry.getKey();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ requestRepaint();
+ }
+
+ @Override
+ public String getStyleName() {
+ return styleName;
+ }
+ }
+
+ /**
+ * CloseHandler is used to process tab closing events. Default behavior is
+ * to remove the tab from the TabSheet.
+ *
+ * @author Jouni Koivuviita / Vaadin Ltd.
+ * @since 6.2.0
+ *
+ */
+ public interface CloseHandler extends Serializable {
+
+ /**
+ * Called when a user has pressed the close icon of a tab in the client
+ * side widget.
+ *
+ * @param tabsheet
+ * the TabSheet to which the tab belongs to
+ * @param tabContent
+ * the component that corresponds to the tab whose close
+ * button was clicked
+ */
+ void onTabClose(final TabSheet tabsheet, final Component tabContent);
+ }
+
+ /**
+ * Provide a custom {@link CloseHandler} for this TabSheet if you wish to
+ * perform some additional tasks when a user clicks on a tabs close button,
+ * e.g. show a confirmation dialogue before removing the tab.
+ *
+ * To remove the tab, if you provide your own close handler, you must call
+ * {@link #removeComponent(Component)} yourself.
+ *
+ * The default CloseHandler for TabSheet will only remove the tab.
+ *
+ * @param handler
+ */
+ public void setCloseHandler(CloseHandler handler) {
+ closeHandler = handler;
+ }
+
+ /**
+ * Sets the position of the tab.
+ *
+ * @param tab
+ * The tab
+ * @param position
+ * The new position of the tab
+ */
+ public void setTabPosition(Tab tab, int position) {
+ int oldPosition = getTabPosition(tab);
+ components.remove(oldPosition);
+ components.add(position, tab.getComponent());
+ requestRepaint();
+ }
+
+ /**
+ * Gets the position of the tab
+ *
+ * @param tab
+ * The tab
+ * @return
+ */
+ public int getTabPosition(Tab tab) {
+ return components.indexOf(tab.getComponent());
+ }
+
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ @Override
+ public int getTabIndex() {
+ return tabIndex;
+ }
+
+ @Override
+ public void setTabIndex(int tabIndex) {
+ this.tabIndex = tabIndex;
+ requestRepaint();
+ }
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+
+ }
+
+ @Override
+ public boolean isComponentVisible(Component childComponent) {
+ return childComponent == getSelectedTab();
+ }
+
+ /**
+ * Copies properties from one Tab to another.
+ *
+ * @param from
+ * The tab whose data to copy.
+ * @param to
+ * The tab to which copy the data.
+ */
+ private static void copyTabMetadata(Tab from, Tab to) {
+ to.setCaption(from.getCaption());
+ to.setIcon(from.getIcon());
+ to.setDescription(from.getDescription());
+ to.setVisible(from.isVisible());
+ to.setEnabled(from.isEnabled());
+ to.setClosable(from.isClosable());
+ to.setStyleName(from.getStyleName());
+ to.setComponentError(from.getComponentError());
+ }
+}
diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java
new file mode 100644
index 0000000000..39b7fb7473
--- /dev/null
+++ b/server/src/com/vaadin/ui/Table.java
@@ -0,0 +1,5449 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.ContainerOrderedWrapper;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.terminal.KeyMapper;
+import com.vaadin.terminal.LegacyPaint;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable;
+
+/**
+ * <p>
+ * <code>Table</code> is used for representing data or components in a pageable
+ * and selectable table.
+ * </p>
+ *
+ * <p>
+ * Scalability of the Table is largely dictated by the container. A table does
+ * not have a limit for the number of items and is just as fast with hundreds of
+ * thousands of items as with just a few. The current GWT implementation with
+ * scrolling however limits the number of rows to around 500000, depending on
+ * the browser and the pixel height of rows.
+ * </p>
+ *
+ * <p>
+ * Components in a Table will not have their caption nor icon rendered.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings({ "deprecation" })
+public class Table extends AbstractSelect implements Action.Container,
+ Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource,
+ DropTarget, HasComponents {
+
+ private transient Logger logger = null;
+
+ /**
+ * Modes that Table support as drag sourse.
+ */
+ public enum TableDragMode {
+ /**
+ * Table does not start drag and drop events. HTM5 style events started
+ * by browser may still happen.
+ */
+ NONE,
+ /**
+ * Table starts drag with a one row only.
+ */
+ ROW,
+ /**
+ * Table drags selected rows, if drag starts on a selected rows. Else it
+ * starts like in ROW mode. Note, that in Transferable there will still
+ * be only the row on which the drag started, other dragged rows need to
+ * be checked from the source Table.
+ */
+ MULTIROW
+ }
+
+ protected static final int CELL_KEY = 0;
+
+ protected static final int CELL_HEADER = 1;
+
+ protected static final int CELL_ICON = 2;
+
+ protected static final int CELL_ITEMID = 3;
+
+ protected static final int CELL_GENERATED_ROW = 4;
+
+ protected static final int CELL_FIRSTCOL = 5;
+
+ public enum Align {
+ /**
+ * Left column alignment. <b>This is the default behaviour. </b>
+ */
+ LEFT("b"),
+
+ /**
+ * Center column alignment.
+ */
+ CENTER("c"),
+
+ /**
+ * Right column alignment.
+ */
+ RIGHT("e");
+
+ private String alignment;
+
+ private Align(String alignment) {
+ this.alignment = alignment;
+ }
+
+ @Override
+ public String toString() {
+ return alignment;
+ }
+
+ public Align convertStringToAlign(String string) {
+ if (string == null) {
+ return null;
+ }
+ if (string.equals("b")) {
+ return Align.LEFT;
+ } else if (string.equals("c")) {
+ return Align.CENTER;
+ } else if (string.equals("e")) {
+ return Align.RIGHT;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link Align#LEFT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_LEFT = Align.LEFT;
+
+ /**
+ * @deprecated from 7.0, use {@link Align#CENTER} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_CENTER = Align.CENTER;
+
+ /**
+ * @deprecated from 7.0, use {@link Align#RIGHT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_RIGHT = Align.RIGHT;
+
+ public enum ColumnHeaderMode {
+ /**
+ * Column headers are hidden.
+ */
+ HIDDEN,
+ /**
+ * Property ID:s are used as column headers.
+ */
+ ID,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}.
+ */
+ EXPLICIT,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}. If a header is not specified for
+ * a given property, its property id is used instead.
+ * <p>
+ * <b>This is the default behavior. </b>
+ */
+ EXPLICIT_DEFAULTS_ID
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link ColumnHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN;
+
+ /**
+ * @deprecated from 7.0, use {@link ColumnHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID;
+
+ /**
+ * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT;
+
+ /**
+ * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ public enum RowHeaderMode {
+ /**
+ * Row caption mode: The row headers are hidden. <b>This is the default
+ * mode. </b>
+ */
+ HIDDEN(null),
+ /**
+ * Row caption mode: Items Id-objects toString is used as row caption.
+ */
+ ID(ItemCaptionMode.ID),
+ /**
+ * Row caption mode: Item-objects toString is used as row caption.
+ */
+ ITEM(ItemCaptionMode.ITEM),
+ /**
+ * Row caption mode: Index of the item is used as item caption. The
+ * index mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX(ItemCaptionMode.INDEX),
+ /**
+ * Row caption mode: Item captions are explicitly specified, but if the
+ * caption is missing, the item id objects <code>toString()</code> is
+ * used instead.
+ */
+ EXPLICIT_DEFAULTS_ID(ItemCaptionMode.EXPLICIT_DEFAULTS_ID),
+ /**
+ * Row caption mode: Item captions are explicitly specified.
+ */
+ EXPLICIT(ItemCaptionMode.EXPLICIT),
+ /**
+ * Row caption mode: Only icons are shown, the captions are hidden.
+ */
+ ICON_ONLY(ItemCaptionMode.ICON_ONLY),
+ /**
+ * Row caption mode: Item captions are read from property specified with
+ * {@link #setItemCaptionPropertyId(Object)}.
+ */
+ PROPERTY(ItemCaptionMode.PROPERTY);
+
+ ItemCaptionMode mode;
+
+ private RowHeaderMode(ItemCaptionMode mode) {
+ this.mode = mode;
+ }
+
+ public ItemCaptionMode getItemCaptionMode() {
+ return mode;
+ }
+ }
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#ITEM} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#INDEX} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#ICON_ONLY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY;
+
+ /**
+ * @deprecated from 7.0, use {@link RowHeaderMode#PROPERTY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY;
+
+ /**
+ * The default rate that table caches rows for smooth scrolling.
+ */
+ private static final double CACHE_RATE_DEFAULT = 2;
+
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+ private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() {
+ };
+
+ /* Private table extensions to Select */
+
+ /**
+ * True if column collapsing is allowed.
+ */
+ private boolean columnCollapsingAllowed = false;
+
+ /**
+ * True if reordering of columns is allowed on the client side.
+ */
+ private boolean columnReorderingAllowed = false;
+
+ /**
+ * Keymapper for column ids.
+ */
+ private final KeyMapper<Object> columnIdMap = new KeyMapper<Object>();
+
+ /**
+ * Holds visible column propertyIds - in order.
+ */
+ private LinkedList<Object> visibleColumns = new LinkedList<Object>();
+
+ /**
+ * Holds noncollapsible columns.
+ */
+ private HashSet<Object> noncollapsibleColumns = new HashSet<Object>();
+
+ /**
+ * Holds propertyIds of currently collapsed columns.
+ */
+ private final HashSet<Object> collapsedColumns = new HashSet<Object>();
+
+ /**
+ * Holds headers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>();
+
+ /**
+ * Holds footers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnFooters = new HashMap<Object, String>();
+
+ /**
+ * Holds icons for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Holds alignments for visible columns (by propertyId).
+ */
+ private HashMap<Object, Align> columnAlignments = new HashMap<Object, Align>();
+
+ /**
+ * Holds column widths in pixels (Integer) or expand ratios (Float) for
+ * visible columns (by propertyId).
+ */
+ private final HashMap<Object, Object> columnWidths = new HashMap<Object, Object>();
+
+ /**
+ * Holds column generators
+ */
+ private final HashMap<Object, ColumnGenerator> columnGenerators = new LinkedHashMap<Object, ColumnGenerator>();
+
+ /**
+ * Holds value of property pageLength. 0 disables paging.
+ */
+ private int pageLength = 15;
+
+ /**
+ * Id the first item on the current page.
+ */
+ private Object currentPageFirstItemId = null;
+
+ /**
+ * Index of the first item on the current page.
+ */
+ private int currentPageFirstItemIndex = 0;
+
+ /**
+ * Holds value of property selectable.
+ */
+ private boolean selectable = false;
+
+ /**
+ * Holds value of property columnHeaderMode.
+ */
+ private ColumnHeaderMode columnHeaderMode = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Holds value of property rowHeaderMode.
+ */
+ private RowHeaderMode rowHeaderMode = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Should the Table footer be visible?
+ */
+ private boolean columnFootersVisible = false;
+
+ /**
+ * Page contents buffer used in buffered mode.
+ */
+ private Object[][] pageBuffer = null;
+
+ /**
+ * Set of properties listened - the list is kept to release the listeners
+ * later.
+ */
+ private HashSet<Property<?>> listenedProperties = null;
+
+ /**
+ * Set of visible components - the is used for needsRepaint calculation.
+ */
+ private HashSet<Component> visibleComponents = null;
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ * Table cell editor factory.
+ */
+ private TableFieldFactory fieldFactory = DefaultFieldFactory.get();
+
+ /**
+ * Is table editable.
+ */
+ private boolean editable = false;
+
+ /**
+ * Current sorting direction.
+ */
+ private boolean sortAscending = true;
+
+ /**
+ * Currently table is sorted on this propertyId.
+ */
+ private Object sortContainerPropertyId = null;
+
+ /**
+ * Is table sorting by the user enabled.
+ */
+ private boolean sortEnabled = true;
+
+ /**
+ * Number of rows explicitly requested by the client to be painted on next
+ * paint. This is -1 if no request by the client is made. Painting the
+ * component will automatically reset this to -1.
+ */
+ private int reqRowsToPaint = -1;
+
+ /**
+ * Index of the first rows explicitly requested by the client to be painted.
+ * This is -1 if no request by the client is made. Painting the component
+ * will automatically reset this to -1.
+ */
+ private int reqFirstRowToPaint = -1;
+
+ private int firstToBeRenderedInClient = -1;
+
+ private int lastToBeRenderedInClient = -1;
+
+ private boolean isContentRefreshesEnabled = true;
+
+ private int pageBufferFirstIndex;
+
+ private boolean containerChangeToBeRendered = false;
+
+ /**
+ * Table cell specific style generator
+ */
+ private CellStyleGenerator cellStyleGenerator = null;
+
+ /**
+ * Table cell specific tooltip generator
+ */
+ private ItemDescriptionGenerator itemDescriptionGenerator;
+
+ /*
+ * EXPERIMENTAL feature: will tell the client to re-calculate column widths
+ * if set to true. Currently no setter: extend to enable.
+ */
+ protected boolean alwaysRecalculateColumnWidths = false;
+
+ private double cacheRate = CACHE_RATE_DEFAULT;
+
+ private TableDragMode dragMode = TableDragMode.NONE;
+
+ private DropHandler dropHandler;
+
+ private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ private boolean rowCacheInvalidated;
+
+ private RowGenerator rowGenerator = null;
+
+ private final Map<Field<?>, Property<?>> associatedProperties = new HashMap<Field<?>, Property<?>>();
+
+ private boolean painted = false;
+
+ private HashMap<Object, Converter<String, Object>> propertyValueConverters = new HashMap<Object, Converter<String, Object>>();
+
+ /**
+ * Set to true if the client-side should be informed that the key mapper has
+ * been reset so it can avoid sending back references to keys that are no
+ * longer present.
+ */
+ private boolean keyMapperReset;
+
+ /* Table constructors */
+
+ /**
+ * Creates a new empty table.
+ */
+ public Table() {
+ setRowHeaderMode(ROW_HEADER_MODE_HIDDEN);
+ }
+
+ /**
+ * Creates a new empty table with caption.
+ *
+ * @param caption
+ */
+ public Table(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new table with caption and connect it to a Container.
+ *
+ * @param caption
+ * @param dataSource
+ */
+ public Table(String caption, Container dataSource) {
+ this();
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /* Table functionality */
+
+ /**
+ * Gets the array of visible column id:s, including generated columns.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ * </p>
+ *
+ * @return an array of currently visible propertyIds and generated column
+ * ids.
+ */
+ public Object[] getVisibleColumns() {
+ if (visibleColumns == null) {
+ return null;
+ }
+ return visibleColumns.toArray();
+ }
+
+ /**
+ * Sets the array of visible column property id:s.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ * </p>
+ *
+ * @param visibleColumns
+ * the Array of shown property id:s.
+ */
+ public void setVisibleColumns(Object[] visibleColumns) {
+
+ // Visible columns must exist
+ if (visibleColumns == null) {
+ throw new NullPointerException(
+ "Can not set visible columns to null value");
+ }
+
+ // TODO add error check that no duplicate identifiers exist
+
+ // Checks that the new visible columns contains no nulls and properties
+ // exist
+ final Collection<?> properties = getContainerPropertyIds();
+ for (int i = 0; i < visibleColumns.length; i++) {
+ if (visibleColumns[i] == null) {
+ throw new NullPointerException("Ids must be non-nulls");
+ } else if (!properties.contains(visibleColumns[i])
+ && !columnGenerators.containsKey(visibleColumns[i])) {
+ throw new IllegalArgumentException(
+ "Ids must exist in the Container or as a generated column , missing id: "
+ + visibleColumns[i]);
+ }
+ }
+
+ // If this is called before the constructor is finished, it might be
+ // uninitialized
+ final LinkedList<Object> newVC = new LinkedList<Object>();
+ for (int i = 0; i < visibleColumns.length; i++) {
+ newVC.add(visibleColumns[i]);
+ }
+
+ // Removes alignments, icons and headers from hidden columns
+ if (this.visibleColumns != null) {
+ boolean disabledHere = disableContentRefreshing();
+ try {
+ for (final Iterator<Object> i = this.visibleColumns.iterator(); i
+ .hasNext();) {
+ final Object col = i.next();
+ if (!newVC.contains(col)) {
+ setColumnHeader(col, null);
+ setColumnAlignment(col, (Align) null);
+ setColumnIcon(col, null);
+ }
+ }
+ } finally {
+ if (disabledHere) {
+ enableContentRefreshing(false);
+ }
+ }
+ }
+
+ this.visibleColumns = newVC;
+
+ // Assures visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given my the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString().
+ * </p>
+ *
+ * @return the Array of column headers.
+ */
+ public String[] getColumnHeaders() {
+ if (columnHeaders == null) {
+ return null;
+ }
+ final String[] headers = new String[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ headers[i] = getColumnHeader(it.next());
+ }
+ return headers;
+ }
+
+ /**
+ * Sets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given my the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString() outputs when rendering.
+ * </p>
+ *
+ * @param columnHeaders
+ * the Array of column headers that match the
+ * {@link #getVisibleColumns()} method.
+ */
+ public void setColumnHeaders(String[] columnHeaders) {
+
+ if (columnHeaders.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the headers array must match the number of visible columns");
+ }
+
+ this.columnHeaders.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnHeaders.length; i++) {
+ this.columnHeaders.put(it.next(), columnHeaders[i]);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given my the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ * </p>
+ *
+ * @return the Array of icons that match the {@link #getVisibleColumns()}.
+ */
+ public Resource[] getColumnIcons() {
+ if (columnIcons == null) {
+ return null;
+ }
+ final Resource[] icons = new Resource[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ icons[i] = columnIcons.get(it.next());
+ }
+
+ return icons;
+ }
+
+ /**
+ * Sets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given my the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ * </p>
+ *
+ * @param columnIcons
+ * the Array of icons that match the {@link #getVisibleColumns()}
+ * .
+ */
+ public void setColumnIcons(Resource[] columnIcons) {
+
+ if (columnIcons.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the icons array must match the number of visible columns");
+ }
+
+ this.columnIcons.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnIcons.length; i++) {
+ this.columnIcons.put(it.next(), columnIcons[i]);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the array of column alignments.
+ *
+ * <p>
+ * The items in the array must match the properties identified by
+ * {@link #getVisibleColumns()}. The possible values for the alignments
+ * include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}: any null values are
+ * rendered as align lefts.
+ * </p>
+ *
+ * @return the Column alignments array.
+ */
+ public Align[] getColumnAlignments() {
+ if (columnAlignments == null) {
+ return null;
+ }
+ final Align[] alignments = new Align[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ alignments[i] = getColumnAlignment(it.next());
+ }
+
+ return alignments;
+ }
+
+ /**
+ * Sets the column alignments.
+ *
+ * <p>
+ * The amount of items in the array must match the amount of properties
+ * identified by {@link #getVisibleColumns()}. The possible values for the
+ * alignments include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}
+ * </p>
+ *
+ * @param columnAlignments
+ * the Column alignments array.
+ */
+ public void setColumnAlignments(Align... columnAlignments) {
+
+ if (columnAlignments.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the alignments array must match the number of visible columns");
+ }
+
+ // Resets the alignments
+ final HashMap<Object, Align> newCA = new HashMap<Object, Align>();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext() && i < columnAlignments.length; i++) {
+ newCA.put(it.next(), columnAlignments[i]);
+ }
+ this.columnAlignments = newCA;
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets columns width (in pixels). Theme may not necessary respect very
+ * small or very big values. Setting width to -1 (default) means that theme
+ * will make decision of width.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used. See @link {@link #setColumnExpandRatio(Object, float)}.
+ *
+ * @param propertyId
+ * colunmns property id
+ * @param width
+ * width to be reserved for colunmns content
+ * @since 4.0.3
+ */
+ public void setColumnWidth(Object propertyId, int width) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to store the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ if (width < 0) {
+ columnWidths.remove(propertyId);
+ } else {
+ columnWidths.put(propertyId, Integer.valueOf(width));
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Sets the column expand ratio for given column.
+ * <p>
+ * Expand ratios can be defined to customize the way how excess space is
+ * divided among columns. Table can have excess space if it has its width
+ * defined and there is horizontally more space than columns consume
+ * naturally. Excess space is the space that is not used by columns with
+ * explicit width (see {@link #setColumnWidth(Object, int)}) or with natural
+ * width (no width nor expand ratio).
+ *
+ * <p>
+ * By default (without expand ratios) the excess space is divided
+ * proportionally to columns natural widths.
+ *
+ * <p>
+ * Only expand ratios of visible columns are used in final calculations.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used.
+ *
+ * <p>
+ * A column with expand ratio is considered to be minimum width by default
+ * (if no excess space exists). The minimum width is defined by terminal
+ * implementation.
+ *
+ * <p>
+ * If terminal implementation supports re-sizable columns the column becomes
+ * fixed width column if users resizes the column.
+ *
+ * @param propertyId
+ * columns property id
+ * @param expandRatio
+ * the expandRatio used to divide excess space for this column
+ */
+ public void setColumnExpandRatio(Object propertyId, float expandRatio) {
+ if (expandRatio < 0) {
+ columnWidths.remove(propertyId);
+ } else {
+ columnWidths.put(propertyId, new Float(expandRatio));
+ }
+ }
+
+ public float getColumnExpandRatio(Object propertyId) {
+ final Object width = columnWidths.get(propertyId);
+ if (width == null || !(width instanceof Float)) {
+ return -1;
+ }
+ final Float value = (Float) width;
+ return value.floatValue();
+
+ }
+
+ /**
+ * Gets the pixel width of column
+ *
+ * @param propertyId
+ * @return width of column or -1 when value not set
+ */
+ public int getColumnWidth(Object propertyId) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to retrieve the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ final Object width = columnWidths.get(propertyId);
+ if (width == null || !(width instanceof Integer)) {
+ return -1;
+ }
+ final Integer value = (Integer) width;
+ return value.intValue();
+ }
+
+ /**
+ * Gets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging.
+ * </p>
+ *
+ * @return the Length of one page.
+ */
+ public int getPageLength() {
+ return pageLength;
+ }
+
+ /**
+ * Sets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging. The page length defaults to 15.
+ * </p>
+ *
+ * <p>
+ * If Table has width set ({@link #setWidth(float, int)} ) the client side
+ * may update the page length automatically the correct value.
+ * </p>
+ *
+ * @param pageLength
+ * the length of one page.
+ */
+ public void setPageLength(int pageLength) {
+ if (pageLength >= 0 && this.pageLength != pageLength) {
+ this.pageLength = pageLength;
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * This method adjusts a possible caching mechanism of table implementation.
+ *
+ * <p>
+ * Table component may fetch and render some rows outside visible area. With
+ * complex tables (for example containing layouts and components), the
+ * client side may become unresponsive. Setting the value lower, UI will
+ * become more responsive. With higher values scrolling in client will hit
+ * server less frequently.
+ *
+ * <p>
+ * The amount of cached rows will be cacheRate multiplied with pageLength (
+ * {@link #setPageLength(int)} both below and above visible area..
+ *
+ * @param cacheRate
+ * a value over 0 (fastest rendering time). Higher value will
+ * cache more rows on server (smoother scrolling). Default value
+ * is 2.
+ */
+ public void setCacheRate(double cacheRate) {
+ if (cacheRate < 0) {
+ throw new IllegalArgumentException(
+ "cacheRate cannot be less than zero");
+ }
+ if (this.cacheRate != cacheRate) {
+ this.cacheRate = cacheRate;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * @see #setCacheRate(double)
+ *
+ * @return the current cache rate value
+ */
+ public double getCacheRate() {
+ return cacheRate;
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public Object getCurrentPageFirstItemId() {
+
+ // Priorise index over id if indexes are supported
+ if (items instanceof Container.Indexed) {
+ final int index = getCurrentPageFirstItemIndex();
+ Object id = null;
+ if (index >= 0 && index < size()) {
+ id = getIdByIndex(index);
+ }
+ if (id != null && !id.equals(currentPageFirstItemId)) {
+ currentPageFirstItemId = id;
+ }
+ }
+
+ // If there is no item id at all, use the first one
+ if (currentPageFirstItemId == null) {
+ currentPageFirstItemId = firstItemId();
+ }
+
+ return currentPageFirstItemId;
+ }
+
+ protected Object getIdByIndex(int index) {
+ return ((Container.Indexed) items).getIdByIndex(index);
+ }
+
+ /**
+ * Setter for property currentPageFirstItemId.
+ *
+ * @param currentPageFirstItemId
+ * the New value of property currentPageFirstItemId.
+ */
+ public void setCurrentPageFirstItemId(Object currentPageFirstItemId) {
+
+ // Gets the corresponding index
+ int index = -1;
+ if (items instanceof Container.Indexed) {
+ index = indexOfId(currentPageFirstItemId);
+ } else {
+ // If the table item container does not have index, we have to
+ // calculates the index by hand
+ Object id = firstItemId();
+ while (id != null && !id.equals(currentPageFirstItemId)) {
+ index++;
+ id = nextItemId(id);
+ }
+ if (id == null) {
+ index = -1;
+ }
+ }
+
+ // If the search for item index was successful
+ if (index >= 0) {
+ /*
+ * The table is not capable of displaying an item in the container
+ * as the first if there are not enough items following the selected
+ * item so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size() - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ if (index > maxIndex) {
+ // Note that we pass index, not maxIndex, letting
+ // setCurrentPageFirstItemIndex handle the situation.
+ setCurrentPageFirstItemIndex(index);
+ return;
+ }
+
+ this.currentPageFirstItemId = currentPageFirstItemId;
+ currentPageFirstItemIndex = index;
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+
+ }
+
+ protected int indexOfId(Object itemId) {
+ return ((Container.Indexed) items).indexOfId(itemId);
+ }
+
+ /**
+ * Gets the icon Resource for the specified column.
+ *
+ * @param propertyId
+ * the propertyId indentifying the column.
+ * @return the icon for the specified column; null if the column has no icon
+ * set, or if the column is not visible.
+ */
+ public Resource getColumnIcon(Object propertyId) {
+ return columnIcons.get(propertyId);
+ }
+
+ /**
+ * Sets the icon Resource for the specified column.
+ * <p>
+ * Throws IllegalArgumentException if the specified column is not visible.
+ * </p>
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param icon
+ * the icon Resource to set.
+ */
+ public void setColumnIcon(Object propertyId, Resource icon) {
+
+ if (icon == null) {
+ columnIcons.remove(propertyId);
+ } else {
+ columnIcons.put(propertyId, icon);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the header for the specified column.
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @return the header for the specified column if it has one.
+ */
+ public String getColumnHeader(Object propertyId) {
+ if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) {
+ return null;
+ }
+
+ String header = columnHeaders.get(propertyId);
+ if ((header == null && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID)
+ || getColumnHeaderMode() == ColumnHeaderMode.ID) {
+ header = propertyId.toString();
+ }
+
+ return header;
+ }
+
+ /**
+ * Sets the column header for the specified column;
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param header
+ * the header to set.
+ */
+ public void setColumnHeader(Object propertyId, String header) {
+
+ if (header == null) {
+ columnHeaders.remove(propertyId);
+ } else {
+ columnHeaders.put(propertyId, header);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the specified column's alignment.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return the specified column's alignment if it as one; null otherwise.
+ */
+ public Align getColumnAlignment(Object propertyId) {
+ final Align a = columnAlignments.get(propertyId);
+ return a == null ? Align.LEFT : a;
+ }
+
+ /**
+ * Sets the specified column's alignment.
+ *
+ * <p>
+ * Throws IllegalArgumentException if the alignment is not one of the
+ * following: {@link Align#LEFT}, {@link Align#CENTER} or
+ * {@link Align#RIGHT}
+ * </p>
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param alignment
+ * the desired alignment.
+ */
+ public void setColumnAlignment(Object propertyId, Align alignment) {
+ if (alignment == null || alignment == Align.LEFT) {
+ columnAlignments.remove(propertyId);
+ } else {
+ columnAlignments.put(propertyId, alignment);
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Checks if the specified column is collapsed.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return true if the column is collapsed; false otherwise;
+ */
+ public boolean isColumnCollapsed(Object propertyId) {
+ return collapsedColumns != null
+ && collapsedColumns.contains(propertyId);
+ }
+
+ /**
+ * Sets whether the specified column is collapsed or not.
+ *
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsed
+ * the desired collapsedness.
+ * @throws IllegalStateException
+ * if column collapsing is not allowed
+ */
+ public void setColumnCollapsed(Object propertyId, boolean collapsed)
+ throws IllegalStateException {
+ if (!isColumnCollapsingAllowed()) {
+ throw new IllegalStateException("Column collapsing not allowed!");
+ }
+ if (collapsed && noncollapsibleColumns.contains(propertyId)) {
+ throw new IllegalStateException("The column is noncollapsible!");
+ }
+
+ if (collapsed) {
+ collapsedColumns.add(propertyId);
+ } else {
+ collapsedColumns.remove(propertyId);
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if column collapsing is allowed.
+ *
+ * @return true if columns can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsingAllowed() {
+ return columnCollapsingAllowed;
+ }
+
+ /**
+ * Sets whether column collapsing is allowed or not.
+ *
+ * @param collapsingAllowed
+ * specifies whether column collapsing is allowed.
+ */
+ public void setColumnCollapsingAllowed(boolean collapsingAllowed) {
+ columnCollapsingAllowed = collapsingAllowed;
+
+ if (!collapsingAllowed) {
+ collapsedColumns.clear();
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets whether the given column is collapsible. Note that collapsible
+ * columns can only be actually collapsed (via UI or with
+ * {@link #setColumnCollapsed(Object, boolean) setColumnCollapsed()}) if
+ * {@link #isColumnCollapsingAllowed()} is true. By default all columns are
+ * collapsible.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsible
+ * true if the column should be collapsible, false otherwise.
+ */
+ public void setColumnCollapsible(Object propertyId, boolean collapsible) {
+ if (collapsible) {
+ noncollapsibleColumns.remove(propertyId);
+ } else {
+ noncollapsibleColumns.add(propertyId);
+ collapsedColumns.remove(propertyId);
+ }
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if the given column is collapsible. Note that even if this method
+ * returns <code>true</code>, the column can only be actually collapsed (via
+ * UI or with {@link #setColumnCollapsed(Object, boolean)
+ * setColumnCollapsed()}) if {@link #isColumnCollapsingAllowed()} is also
+ * true.
+ *
+ * @return true if the column can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsible(Object propertyId) {
+ return !noncollapsibleColumns.contains(propertyId);
+ }
+
+ /**
+ * Checks if column reordering is allowed.
+ *
+ * @return true if columns can be reordered; false otherwise.
+ */
+ public boolean isColumnReorderingAllowed() {
+ return columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether column reordering is allowed or not.
+ *
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed.
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (columnReorderingAllowed != this.columnReorderingAllowed) {
+ this.columnReorderingAllowed = columnReorderingAllowed;
+ requestRepaint();
+ }
+ }
+
+ /*
+ * Arranges visible columns according to given columnOrder. Silently ignores
+ * colimnId:s that are not visible columns, and keeps the internal order of
+ * visible columns left out of the ordering (trailing). Silently does
+ * nothing if columnReordering is not allowed.
+ */
+ private void setColumnOrder(Object[] columnOrder) {
+ if (columnOrder == null || !isColumnReorderingAllowed()) {
+ return;
+ }
+ final LinkedList<Object> newOrder = new LinkedList<Object>();
+ for (int i = 0; i < columnOrder.length; i++) {
+ if (columnOrder[i] != null
+ && visibleColumns.contains(columnOrder[i])) {
+ visibleColumns.remove(columnOrder[i]);
+ newOrder.add(columnOrder[i]);
+ }
+ }
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext();) {
+ final Object columnId = it.next();
+ if (!newOrder.contains(columnId)) {
+ newOrder.add(columnId);
+ }
+ }
+ visibleColumns = newOrder;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public int getCurrentPageFirstItemIndex() {
+ return currentPageFirstItemIndex;
+ }
+
+ void setCurrentPageFirstItemIndex(int newIndex, boolean needsPageBufferReset) {
+
+ if (newIndex < 0) {
+ newIndex = 0;
+ }
+
+ /*
+ * minimize Container.size() calls which may be expensive. For example
+ * it may cause sql query.
+ */
+ final int size = size();
+
+ /*
+ * The table is not capable of displaying an item in the container as
+ * the first if there are not enough items following the selected item
+ * so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ /*
+ * FIXME #7607 Take somehow into account the case where we want to
+ * scroll to the bottom so that the last row is completely visible even
+ * if (table height) / (row height) is not an integer. Reverted the
+ * original fix because of #8662 regression.
+ */
+ if (newIndex > maxIndex) {
+ newIndex = maxIndex;
+ }
+
+ // Refresh first item id
+ if (items instanceof Container.Indexed) {
+ try {
+ currentPageFirstItemId = getIdByIndex(newIndex);
+ } catch (final IndexOutOfBoundsException e) {
+ currentPageFirstItemId = null;
+ }
+ currentPageFirstItemIndex = newIndex;
+ } else {
+
+ // For containers not supporting indexes, we must iterate the
+ // container forwards / backwards
+ // next available item forward or backward
+
+ currentPageFirstItemId = firstItemId();
+
+ // Go forwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = size - 1;
+ }
+
+ // Go backwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex > newIndex
+ && !isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex--;
+ currentPageFirstItemId = prevItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = 0;
+ }
+
+ // Go forwards once more
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If for some reason we do hit border again, override
+ // the user index request
+ if (isLastId(currentPageFirstItemId)) {
+ newIndex = currentPageFirstItemIndex = size - 1;
+ }
+ }
+ if (needsPageBufferReset) {
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * Setter for property currentPageFirstItem.
+ *
+ * @param newIndex
+ * the New value of property currentPageFirstItem.
+ */
+ public void setCurrentPageFirstItemIndex(int newIndex) {
+ setCurrentPageFirstItemIndex(newIndex, true);
+ }
+
+ /**
+ * Getter for property pageBuffering.
+ *
+ * @deprecated functionality is not needed in ajax rendering model
+ *
+ * @return the Value of property pageBuffering.
+ */
+ @Deprecated
+ public boolean isPageBufferingEnabled() {
+ return true;
+ }
+
+ /**
+ * Setter for property pageBuffering.
+ *
+ * @deprecated functionality is not needed in ajax rendering model
+ *
+ * @param pageBuffering
+ * the New value of property pageBuffering.
+ */
+ @Deprecated
+ public void setPageBufferingEnabled(boolean pageBuffering) {
+
+ }
+
+ /**
+ * Getter for property selectable.
+ *
+ * <p>
+ * The table is not selectable by default.
+ * </p>
+ *
+ * @return the Value of property selectable.
+ */
+ public boolean isSelectable() {
+ return selectable;
+ }
+
+ /**
+ * Setter for property selectable.
+ *
+ * <p>
+ * The table is not selectable by default.
+ * </p>
+ *
+ * @param selectable
+ * the New value of property selectable.
+ */
+ public void setSelectable(boolean selectable) {
+ if (this.selectable != selectable) {
+ this.selectable = selectable;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Getter for property columnHeaderMode.
+ *
+ * @return the Value of property columnHeaderMode.
+ */
+ public ColumnHeaderMode getColumnHeaderMode() {
+ return columnHeaderMode;
+ }
+
+ /**
+ * Setter for property columnHeaderMode.
+ *
+ * @param columnHeaderMode
+ * the New value of property columnHeaderMode.
+ */
+ public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) {
+ if (columnHeaderMode == null) {
+ throw new IllegalArgumentException(
+ "Column header mode can not be null");
+ }
+ if (columnHeaderMode != this.columnHeaderMode) {
+ this.columnHeaderMode = columnHeaderMode;
+ requestRepaint();
+ }
+
+ }
+
+ /**
+ * Refreshes the rows in the internal cache. Only if
+ * {@link #resetPageBuffer()} is called before this then all values are
+ * guaranteed to be recreated.
+ */
+ protected void refreshRenderedCells() {
+ if (getParent() == null) {
+ return;
+ }
+
+ if (!isContentRefreshesEnabled) {
+ return;
+ }
+
+ // Collects the basic facts about the table page
+ final int pagelen = getPageLength();
+ int rows, totalRows;
+ rows = totalRows = size();
+ int firstIndex = Math
+ .min(getCurrentPageFirstItemIndex(), totalRows - 1);
+ if (rows > 0 && firstIndex >= 0) {
+ rows -= firstIndex;
+ }
+ if (pagelen > 0 && pagelen < rows) {
+ rows = pagelen;
+ }
+
+ // If "to be painted next" variables are set, use them
+ if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) {
+ rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1;
+ }
+ if (firstToBeRenderedInClient >= 0) {
+ if (firstToBeRenderedInClient < totalRows) {
+ firstIndex = firstToBeRenderedInClient;
+ } else {
+ firstIndex = totalRows - 1;
+ }
+ } else {
+ // initial load
+
+ // #8805 send one extra row in the beginning in case a partial
+ // row is shown on the UI
+ if (firstIndex > 0) {
+ firstIndex = firstIndex - 1;
+ rows = rows + 1;
+ }
+ firstToBeRenderedInClient = firstIndex;
+ }
+ if (totalRows > 0) {
+ if (rows + firstIndex > totalRows) {
+ rows = totalRows - firstIndex;
+ }
+ } else {
+ rows = 0;
+ }
+
+ // Saves the results to internal buffer
+ pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true);
+
+ if (rows > 0) {
+ pageBufferFirstIndex = firstIndex;
+ }
+
+ setRowCacheInvalidated(true);
+ requestRepaint();
+ }
+
+ /**
+ * Requests that the Table should be repainted as soon as possible.
+ *
+ * Note that a {@code Table} does not necessarily repaint its contents when
+ * this method has been called. See {@link #refreshRowCache()} for forcing
+ * an update of the contents.
+ */
+
+ @Override
+ public void requestRepaint() {
+ // Overridden only for javadoc
+ super.requestRepaint();
+ }
+
+ @Override
+ public void requestRepaintAll() {
+ super.requestRepaintAll();
+
+ // Avoid sending a partial repaint (#8714)
+ refreshRowCache();
+ }
+
+ private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) {
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int totalRows = size();
+ int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex;
+
+ /*
+ * firstIndexInPageBuffer is the first row to be removed. "rows" rows
+ * after that should be removed. If the page buffer does not contain
+ * that many rows, we only remove the rows that actually are in the page
+ * buffer.
+ */
+ if (firstIndexInPageBuffer + rows > totalCachedRows) {
+ rows = totalCachedRows - firstIndexInPageBuffer;
+ }
+
+ /*
+ * Unregister components that will no longer be in the page buffer to
+ * make sure that no components leak.
+ */
+ unregisterComponentsAndPropertiesInRows(firstIndex, rows);
+
+ /*
+ * The number of rows that should be in the cache after this operation
+ * is done (pageBuffer currently contains the expanded items).
+ */
+ int newCachedRowCount = totalCachedRows;
+ if (newCachedRowCount + pageBufferFirstIndex > totalRows) {
+ newCachedRowCount = totalRows - pageBufferFirstIndex;
+ }
+
+ /*
+ * The index at which we should render the first row that does not come
+ * from the previous page buffer.
+ */
+ int firstAppendedRowInPageBuffer = totalCachedRows - rows;
+ int firstAppendedRow = firstAppendedRowInPageBuffer
+ + pageBufferFirstIndex;
+
+ /*
+ * Calculate the maximum number of new rows that we can add to the page
+ * buffer. Less than the rows we removed if the container does not
+ * contain that many items afterwards.
+ */
+ int maxRowsToRender = (totalRows - firstAppendedRow);
+ int rowsToAdd = rows;
+ if (rowsToAdd > maxRowsToRender) {
+ rowsToAdd = maxRowsToRender;
+ }
+
+ Object[][] cells = null;
+ if (rowsToAdd > 0) {
+ cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false);
+ }
+ /*
+ * Create the new cache buffer by copying the first rows from the old
+ * buffer, moving the following rows upwards and appending more rows if
+ * applicable.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][row];
+ }
+ for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) {
+ // Move the rows that were after the expanded rows
+ newPageBuffer[i][row] = pageBuffer[i][row + rows];
+ }
+ for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) {
+ // Add the newly rendered rows. Only used if rowsToAdd > 0
+ // (cells != null)
+ newPageBuffer[i][row] = cells[i][row
+ - firstAppendedRowInPageBuffer];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ }
+
+ private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex, int rows) {
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+ int cacheIx = firstIndex - pageBufferFirstIndex;
+ // update the new rows in the cache.
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int end = Math.min(cacheIx + rows, totalCachedRows);
+ for (int ix = cacheIx; ix < end; ix++) {
+ for (int i = 0; i < pageBuffer.length; i++) {
+ pageBuffer[i][ix] = cells[i][ix - cacheIx];
+ }
+ }
+ return cells;
+ }
+
+ /**
+ * @param firstIndex
+ * The position where new rows should be inserted
+ * @param rows
+ * The maximum number of rows that should be inserted at position
+ * firstIndex. Less rows will be inserted if the page buffer is
+ * too small.
+ * @return
+ */
+ private Object[][] getVisibleCellsInsertIntoCache(int firstIndex, int rows) {
+ getLogger().finest(
+ "Insert " + rows + " rows at index " + firstIndex
+ + " to existing page buffer requested");
+
+ // Page buffer must not become larger than pageLength*cacheRate before
+ // or after the current page
+ int minPageBufferIndex = getCurrentPageFirstItemIndex()
+ - (int) (getPageLength() * getCacheRate());
+ if (minPageBufferIndex < 0) {
+ minPageBufferIndex = 0;
+ }
+
+ int maxPageBufferIndex = getCurrentPageFirstItemIndex()
+ + (int) (getPageLength() * (1 + getCacheRate()));
+ int maxBufferSize = maxPageBufferIndex - minPageBufferIndex;
+
+ if (getPageLength() == 0) {
+ // If pageLength == 0 then all rows should be rendered
+ maxBufferSize = pageBuffer[0].length + rows;
+ }
+ /*
+ * Number of rows that were previously cached. This is not necessarily
+ * the same as pageLength if we do not have enough rows in the
+ * container.
+ */
+ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length;
+
+ /*
+ * firstIndexInPageBuffer is the offset in pageBuffer where the new rows
+ * will be inserted (firstIndex is the index in the whole table).
+ *
+ * E.g. scrolled down to row 1000: firstIndex==1010,
+ * pageBufferFirstIndex==1000 -> cacheIx==10
+ */
+ int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex;
+
+ /* If rows > size available in page buffer */
+ if (firstIndexInPageBuffer + rows > maxBufferSize) {
+ rows = maxBufferSize - firstIndexInPageBuffer;
+ }
+
+ /*
+ * "rows" rows will be inserted at firstIndex. Find out how many old
+ * rows fall outside the new buffer so we can unregister components in
+ * the cache.
+ */
+
+ /* All rows until the insertion point remain, always. */
+ int firstCacheRowToRemoveInPageBuffer = firstIndexInPageBuffer;
+
+ /*
+ * IF there is space remaining in the buffer after the rows have been
+ * inserted, we can keep more rows.
+ */
+ int numberOfOldRowsAfterInsertedRows = maxBufferSize
+ - firstIndexInPageBuffer - rows;
+ if (numberOfOldRowsAfterInsertedRows > 0) {
+ firstCacheRowToRemoveInPageBuffer += numberOfOldRowsAfterInsertedRows;
+ }
+
+ if (firstCacheRowToRemoveInPageBuffer <= currentlyCachedRowCount) {
+ /*
+ * Unregister all components that fall beyond the cache limits after
+ * inserting the new rows.
+ */
+ unregisterComponentsAndPropertiesInRows(
+ firstCacheRowToRemoveInPageBuffer + pageBufferFirstIndex,
+ currentlyCachedRowCount - firstCacheRowToRemoveInPageBuffer
+ + pageBufferFirstIndex);
+ }
+
+ // Calculate the new cache size
+ int newCachedRowCount = currentlyCachedRowCount;
+ if (maxBufferSize == 0 || currentlyCachedRowCount < maxBufferSize) {
+ newCachedRowCount = currentlyCachedRowCount + rows;
+ if (maxBufferSize > 0 && newCachedRowCount > maxBufferSize) {
+ newCachedRowCount = maxBufferSize;
+ }
+ }
+
+ /* Paint the new rows into a separate buffer */
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+
+ /*
+ * Create the new cache buffer and fill it with the data from the old
+ * buffer as well as the inserted rows.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][row];
+ }
+ for (int row = firstIndexInPageBuffer; row < firstIndexInPageBuffer
+ + rows; row++) {
+ // Copy the newly created rows
+ newPageBuffer[i][row] = cells[i][row - firstIndexInPageBuffer];
+ }
+ for (int row = firstIndexInPageBuffer + rows; row < newCachedRowCount; row++) {
+ // Move the old rows down below the newly inserted rows
+ newPageBuffer[i][row] = pageBuffer[i][row - rows];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ getLogger().finest(
+ "Page Buffer now contains "
+ + pageBuffer[CELL_ITEMID].length
+ + " rows ("
+ + pageBufferFirstIndex
+ + "-"
+ + (pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1) + ")");
+ return cells;
+ }
+
+ /**
+ * Render rows with index "firstIndex" to "firstIndex+rows-1" to a new
+ * buffer.
+ *
+ * Reuses values from the current page buffer if the rows are found there.
+ *
+ * @param firstIndex
+ * @param rows
+ * @param replaceListeners
+ * @return
+ */
+ private Object[][] getVisibleCellsNoCache(int firstIndex, int rows,
+ boolean replaceListeners) {
+ getLogger().finest(
+ "Render visible cells for rows " + firstIndex + "-"
+ + (firstIndex + rows - 1));
+ final Object[] colids = getVisibleColumns();
+ final int cols = colids.length;
+
+ HashSet<Property<?>> oldListenedProperties = listenedProperties;
+ HashSet<Component> oldVisibleComponents = visibleComponents;
+
+ if (replaceListeners) {
+ // initialize the listener collections, this should only be done if
+ // the entire cache is refreshed (through refreshRenderedCells)
+ listenedProperties = new HashSet<Property<?>>();
+ visibleComponents = new HashSet<Component>();
+ }
+
+ Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows];
+ if (rows == 0) {
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+ return cells;
+ }
+
+ // Gets the first item id
+ Object id;
+ if (items instanceof Container.Indexed) {
+ id = getIdByIndex(firstIndex);
+ } else {
+ id = firstItemId();
+ for (int i = 0; i < firstIndex; i++) {
+ id = nextItemId(id);
+ }
+ }
+
+ final RowHeaderMode headmode = getRowHeaderMode();
+ final boolean[] iscomponent = new boolean[cols];
+ for (int i = 0; i < cols; i++) {
+ iscomponent[i] = columnGenerators.containsKey(colids[i])
+ || Component.class.isAssignableFrom(getType(colids[i]));
+ }
+ int firstIndexNotInCache;
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ firstIndexNotInCache = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length;
+ } else {
+ firstIndexNotInCache = -1;
+ }
+
+ // Creates the page contents
+ int filledRows = 0;
+ for (int i = 0; i < rows && id != null; i++) {
+ cells[CELL_ITEMID][i] = id;
+ cells[CELL_KEY][i] = itemIdMapper.key(id);
+ if (headmode != ROW_HEADER_MODE_HIDDEN) {
+ switch (headmode) {
+ case INDEX:
+ cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + 1);
+ break;
+ default:
+ cells[CELL_HEADER][i] = getItemCaption(id);
+ }
+ cells[CELL_ICON][i] = getItemIcon(id);
+ }
+
+ GeneratedRow generatedRow = rowGenerator != null ? rowGenerator
+ .generateRow(this, id) : null;
+ cells[CELL_GENERATED_ROW][i] = generatedRow;
+
+ for (int j = 0; j < cols; j++) {
+ if (isColumnCollapsed(colids[j])) {
+ continue;
+ }
+ Property<?> p = null;
+ Object value = "";
+ boolean isGeneratedRow = generatedRow != null;
+ boolean isGeneratedColumn = columnGenerators
+ .containsKey(colids[j]);
+ boolean isGenerated = isGeneratedRow || isGeneratedColumn;
+
+ if (!isGenerated) {
+ p = getContainerProperty(id, colids[j]);
+ }
+
+ if (isGeneratedRow) {
+ if (generatedRow.isSpanColumns() && j > 0) {
+ value = null;
+ } else if (generatedRow.isSpanColumns() && j == 0
+ && generatedRow.getValue() instanceof Component) {
+ value = generatedRow.getValue();
+ } else if (generatedRow.getText().length > j) {
+ value = generatedRow.getText()[j];
+ }
+ } else {
+ // check in current pageBuffer already has row
+ int index = firstIndex + i;
+ if (p != null || isGenerated) {
+ int indexInOldBuffer = index - pageBufferFirstIndex;
+ if (index < firstIndexNotInCache
+ && index >= pageBufferFirstIndex
+ && pageBuffer[CELL_GENERATED_ROW][indexInOldBuffer] == null
+ && id.equals(pageBuffer[CELL_ITEMID][indexInOldBuffer])) {
+ // we already have data in our cache,
+ // recycle it instead of fetching it via
+ // getValue/getPropertyValue
+ value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer];
+ if (!isGeneratedColumn && iscomponent[j]
+ || !(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ if (isGeneratedColumn) {
+ ColumnGenerator cg = columnGenerators
+ .get(colids[j]);
+ value = cg.generateCell(this, id, colids[j]);
+ if (value != null
+ && !(value instanceof Component)
+ && !(value instanceof String)) {
+ // Avoid errors if a generator returns
+ // something
+ // other than a Component or a String
+ value = value.toString();
+ }
+ } else if (iscomponent[j]) {
+ value = p.getValue();
+ listenProperty(p, oldListenedProperties);
+ } else if (p != null) {
+ value = getPropertyValue(id, colids[j], p);
+ /*
+ * If returned value is Component (via
+ * fieldfactory or overridden getPropertyValue)
+ * we excpect it to listen property value
+ * changes. Otherwise if property emits value
+ * change events, table will start to listen
+ * them and refresh content when needed.
+ */
+ if (!(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ value = getPropertyValue(id, colids[j], null);
+ }
+ }
+ }
+ }
+
+ if (value instanceof Component) {
+ registerComponent((Component) value);
+ }
+ cells[CELL_FIRSTCOL + j][i] = value;
+ }
+
+ // Gets the next item id
+ if (items instanceof Container.Indexed) {
+ int index = firstIndex + i + 1;
+ if (index < size()) {
+ id = getIdByIndex(index);
+ } else {
+ id = null;
+ }
+ } else {
+ id = nextItemId(id);
+ }
+
+ filledRows++;
+ }
+
+ // Assures that all the rows of the cell-buffer are valid
+ if (filledRows != cells[0].length) {
+ final Object[][] temp = new Object[cells.length][filledRows];
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < filledRows; j++) {
+ temp[i][j] = cells[i][j];
+ }
+ }
+ cells = temp;
+ }
+
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+
+ return cells;
+ }
+
+ protected void registerComponent(Component component) {
+ getLogger().finest(
+ "Registered " + component.getClass().getSimpleName() + ": "
+ + component.getCaption());
+ if (component.getParent() != this) {
+ component.setParent(this);
+ }
+ visibleComponents.add(component);
+ }
+
+ private void listenProperty(Property<?> p,
+ HashSet<Property<?>> oldListenedProperties) {
+ if (p instanceof Property.ValueChangeNotifier) {
+ if (oldListenedProperties == null
+ || !oldListenedProperties.contains(p)) {
+ ((Property.ValueChangeNotifier) p).addListener(this);
+ }
+ /*
+ * register listened properties, so we can do proper cleanup to free
+ * memory. Essential if table has loads of data and it is used for a
+ * long time.
+ */
+ listenedProperties.add(p);
+
+ }
+ }
+
+ /**
+ * @param firstIx
+ * Index of the first row to process. Global index, not relative
+ * to page buffer.
+ * @param count
+ */
+ private void unregisterComponentsAndPropertiesInRows(int firstIx, int count) {
+ getLogger().finest(
+ "Unregistering components in rows " + firstIx + "-"
+ + (firstIx + count - 1));
+ Object[] colids = getVisibleColumns();
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ int bufSize = pageBuffer[CELL_ITEMID].length;
+ int ix = firstIx - pageBufferFirstIndex;
+ ix = ix < 0 ? 0 : ix;
+ if (ix < bufSize) {
+ count = count > bufSize - ix ? bufSize - ix : count;
+ for (int i = 0; i < count; i++) {
+ for (int c = 0; c < colids.length; c++) {
+ Object cellVal = pageBuffer[CELL_FIRSTCOL + c][i + ix];
+ if (cellVal instanceof Component
+ && visibleComponents.contains(cellVal)) {
+ visibleComponents.remove(cellVal);
+ unregisterComponent((Component) cellVal);
+ } else {
+ Property<?> p = getContainerProperty(
+ pageBuffer[CELL_ITEMID][i + ix], colids[c]);
+ if (p instanceof ValueChangeNotifier
+ && listenedProperties.contains(p)) {
+ listenedProperties.remove(p);
+ ((ValueChangeNotifier) p).removeListener(this);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to remove listeners and maintain correct component
+ * hierarchy. Detaches properties and components if those are no more
+ * rendered in client.
+ *
+ * @param oldListenedProperties
+ * set of properties that where listened in last render
+ * @param oldVisibleComponents
+ * set of components that where attached in last render
+ */
+ private void unregisterPropertiesAndComponents(
+ HashSet<Property<?>> oldListenedProperties,
+ HashSet<Component> oldVisibleComponents) {
+ if (oldVisibleComponents != null) {
+ for (final Iterator<Component> i = oldVisibleComponents.iterator(); i
+ .hasNext();) {
+ Component c = i.next();
+ if (!visibleComponents.contains(c)) {
+ unregisterComponent(c);
+ }
+ }
+ }
+
+ if (oldListenedProperties != null) {
+ for (final Iterator<Property<?>> i = oldListenedProperties
+ .iterator(); i.hasNext();) {
+ Property.ValueChangeNotifier o = (ValueChangeNotifier) i.next();
+ if (!listenedProperties.contains(o)) {
+ o.removeListener(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method cleans up a Component that has been generated when Table is
+ * in editable mode. The component needs to be detached from its parent and
+ * if it is a field, it needs to be detached from its property data source
+ * in order to allow garbage collection to take care of removing the unused
+ * component from memory.
+ *
+ * Override this method and getPropertyValue(Object, Object, Property) with
+ * custom logic if you need to deal with buffered fields.
+ *
+ * @see #getPropertyValue(Object, Object, Property)
+ *
+ * @param oldVisibleComponents
+ * a set of components that should be unregistered.
+ */
+ protected void unregisterComponent(Component component) {
+ getLogger().finest(
+ "Unregistered " + component.getClass().getSimpleName() + ": "
+ + component.getCaption());
+ component.setParent(null);
+ /*
+ * Also remove property data sources to unregister listeners keeping the
+ * fields in memory.
+ */
+ if (component instanceof Field) {
+ Field<?> field = (Field<?>) component;
+ Property<?> associatedProperty = associatedProperties
+ .remove(component);
+ if (associatedProperty != null
+ && field.getPropertyDataSource() == associatedProperty) {
+ // Remove the property data source only if it's the one we
+ // added in getPropertyValue
+ field.setPropertyDataSource(null);
+ }
+ }
+ }
+
+ /**
+ * Refreshes the current page contents.
+ *
+ * @deprecated should not need to be used
+ */
+ @Deprecated
+ public void refreshCurrentPage() {
+
+ }
+
+ /**
+ * Sets the row header mode.
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li>{@link #ROW_HEADER_MODE_HIDDEN}: The row captions are hidden.</li>
+ * <li>{@link #ROW_HEADER_MODE_ID}: Items Id-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_ITEM}: Item-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_PROPERTY}: Property set with
+ * {@link #setItemCaptionPropertyId(Object)} is used as row header.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID}: Items Id-objects
+ * <code>toString()</code> is used as row header. If caption is explicitly
+ * specified, it overrides the id-caption.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT}: The row headers must be explicitly
+ * specified.</li>
+ * <li>{@link #ROW_HEADER_MODE_INDEX}: The index of the item is used as row
+ * caption. The index mode can only be used with the containers implementing
+ * <code>Container.Indexed</code> interface.</li>
+ * </ul>
+ * The default value is {@link #ROW_HEADER_MODE_HIDDEN}
+ * </p>
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setRowHeaderMode(RowHeaderMode mode) {
+ if (mode != null) {
+ rowHeaderMode = mode;
+ if (mode != RowHeaderMode.HIDDEN) {
+ setItemCaptionMode(mode.getItemCaptionMode());
+ }
+ // Assures the visual refresh. No need to reset the page buffer
+ // before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Gets the row header mode.
+ *
+ * @return the Row header mode.
+ * @see #setRowHeaderMode(int)
+ */
+ public RowHeaderMode getRowHeaderMode() {
+ return rowHeaderMode;
+ }
+
+ /**
+ * Adds the new row to table and fill the visible cells (except generated
+ * columns) with given values.
+ *
+ * @param cells
+ * the Object array that is used for filling the visible cells
+ * new row. The types must be settable to visible column property
+ * types.
+ * @param itemId
+ * the Id the new row. If null, a new id is automatically
+ * assigned. If given, the table cant already have a item with
+ * given id.
+ * @return Returns item id for the new row. Returns null if operation fails.
+ */
+ public Object addItem(Object[] cells, Object itemId)
+ throws UnsupportedOperationException {
+
+ // remove generated columns from the list of columns being assigned
+ final LinkedList<Object> availableCols = new LinkedList<Object>();
+ for (Iterator<Object> it = visibleColumns.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (!columnGenerators.containsKey(id)) {
+ availableCols.add(id);
+ }
+ }
+ // Checks that a correct number of cells are given
+ if (cells.length != availableCols.size()) {
+ return null;
+ }
+
+ // Creates new item
+ Item item;
+ if (itemId == null) {
+ itemId = items.addItem();
+ if (itemId == null) {
+ return null;
+ }
+ item = items.getItem(itemId);
+ } else {
+ item = items.addItem(itemId);
+ }
+ if (item == null) {
+ return null;
+ }
+
+ // Fills the item properties
+ for (int i = 0; i < availableCols.size(); i++) {
+ item.getItemProperty(availableCols.get(i)).setValue(cells[i]);
+ }
+
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+
+ return itemId;
+ }
+
+ /**
+ * Discards and recreates the internal row cache. Call this if you make
+ * changes that affect the rows but the information about the changes are
+ * not automatically propagated to the Table.
+ * <p>
+ * Do not call this e.g. if you have updated the data model through a
+ * Property. These types of changes are automatically propagated to the
+ * Table.
+ * <p>
+ * A typical case when this is needed is if you update a generator (e.g.
+ * CellStyleGenerator) and want to ensure that the rows are redrawn with new
+ * styles.
+ * <p>
+ * <i>Note that calling this method is not cheap so avoid calling it
+ * unnecessarily.</i>
+ *
+ * @since 6.7.2
+ */
+ public void refreshRowCache() {
+ resetPageBuffer();
+ refreshRenderedCells();
+ }
+
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+
+ disableContentRefreshing();
+
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+
+ // Assures that the data source is ordered by making unordered
+ // containers ordered by wrapping them
+ if (newDataSource instanceof Container.Ordered) {
+ super.setContainerDataSource(newDataSource);
+ } else {
+ super.setContainerDataSource(new ContainerOrderedWrapper(
+ newDataSource));
+ }
+
+ // Resets page position
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+
+ // Resets column properties
+ if (collapsedColumns != null) {
+ collapsedColumns.clear();
+ }
+
+ // columnGenerators 'override' properties, don't add the same id twice
+ Collection<Object> col = new LinkedList<Object>();
+ for (Iterator<?> it = getContainerPropertyIds().iterator(); it
+ .hasNext();) {
+ Object id = it.next();
+ if (columnGenerators == null || !columnGenerators.containsKey(id)) {
+ col.add(id);
+ }
+ }
+ // generators added last
+ if (columnGenerators != null && columnGenerators.size() > 0) {
+ col.addAll(columnGenerators.keySet());
+ }
+
+ setVisibleColumns(col.toArray());
+
+ // Assure visual refresh
+ resetPageBuffer();
+
+ enableContentRefreshing(true);
+ }
+
+ /**
+ * Gets items ids from a range of key values
+ *
+ * @param startRowKey
+ * The start key
+ * @param endRowKey
+ * The end key
+ * @return
+ */
+ private LinkedHashSet<Object> getItemIdsInRange(Object itemId,
+ final int length) {
+ LinkedHashSet<Object> ids = new LinkedHashSet<Object>();
+ for (int i = 0; i < length; i++) {
+ assert itemId != null; // should not be null unless client-server
+ // are out of sync
+ ids.add(itemId);
+ itemId = nextItemId(itemId);
+ }
+ return ids;
+ }
+
+ /**
+ * Handles selection if selection is a multiselection
+ *
+ * @param variables
+ * The variables
+ */
+ private void handleSelectedItems(Map<String, Object> variables) {
+ final String[] ka = (String[]) variables.get("selected");
+ final String[] ranges = (String[]) variables.get("selectedRanges");
+
+ Set<Object> renderedButNotSelectedItemIds = getCurrentlyRenderedItemIds();
+
+ @SuppressWarnings("unchecked")
+ HashSet<Object> newValue = new LinkedHashSet<Object>(
+ (Collection<Object>) getValue());
+
+ if (variables.containsKey("clearSelections")) {
+ // the client side has instructed to swipe all previous selections
+ newValue.clear();
+ }
+
+ /*
+ * Then add (possibly some of them back) rows that are currently
+ * selected on the client side (the ones that the client side is aware
+ * of).
+ */
+ for (int i = 0; i < ka.length; i++) {
+ // key to id
+ final Object id = itemIdMapper.get(ka[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ requestRepaint();
+ } else if (id != null && containsId(id)) {
+ newValue.add(id);
+ renderedButNotSelectedItemIds.remove(id);
+ }
+ }
+
+ /* Add range items aka shift clicked multiselection areas */
+ if (ranges != null) {
+ for (String range : ranges) {
+ String[] split = range.split("-");
+ Object startItemId = itemIdMapper.get(split[0]);
+ int length = Integer.valueOf(split[1]);
+ LinkedHashSet<Object> itemIdsInRange = getItemIdsInRange(
+ startItemId, length);
+ newValue.addAll(itemIdsInRange);
+ renderedButNotSelectedItemIds.removeAll(itemIdsInRange);
+ }
+ }
+ /*
+ * finally clear all currently rendered rows (the ones that the client
+ * side counterpart is aware of) that the client didn't send as selected
+ */
+ newValue.removeAll(renderedButNotSelectedItemIds);
+
+ if (!isNullSelectionAllowed() && newValue.isEmpty()) {
+ // empty selection not allowed, keep old value
+ requestRepaint();
+ return;
+ }
+
+ setValue(newValue, true);
+
+ }
+
+ private Set<Object> getCurrentlyRenderedItemIds() {
+ HashSet<Object> ids = new HashSet<Object>();
+ if (pageBuffer != null) {
+ for (int i = 0; i < pageBuffer[CELL_ITEMID].length; i++) {
+ ids.add(pageBuffer[CELL_ITEMID][i]);
+ }
+ }
+ return ids;
+ }
+
+ /* Component basics */
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.Select#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ boolean clientNeedsContentRefresh = false;
+
+ handleClickEvent(variables);
+
+ handleColumnResizeEvent(variables);
+
+ handleColumnWidthUpdates(variables);
+
+ disableContentRefreshing();
+
+ if (!isSelectable() && variables.containsKey("selected")) {
+ // Not-selectable is a special case, AbstractSelect does not support
+ // TODO could be optimized.
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ /*
+ * The AbstractSelect cannot handle the multiselection properly, instead
+ * we handle it ourself
+ */
+ else if (isSelectable() && isMultiSelect()
+ && variables.containsKey("selected")
+ && multiSelectMode == MultiSelectMode.DEFAULT) {
+ handleSelectedItems(variables);
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ super.changeVariables(source, variables);
+
+ // Client might update the pagelength if Table height is fixed
+ if (variables.containsKey("pagelength")) {
+ // Sets pageLength directly to avoid repaint that setter causes
+ pageLength = (Integer) variables.get("pagelength");
+ }
+
+ // Page start index
+ if (variables.containsKey("firstvisible")) {
+ final Integer value = (Integer) variables.get("firstvisible");
+ if (value != null) {
+ setCurrentPageFirstItemIndex(value.intValue(), false);
+ }
+ }
+
+ // Sets requested firstrow and rows for the next paint
+ if (variables.containsKey("reqfirstrow")
+ || variables.containsKey("reqrows")) {
+
+ try {
+ firstToBeRenderedInClient = ((Integer) variables
+ .get("firstToBeRendered")).intValue();
+ lastToBeRenderedInClient = ((Integer) variables
+ .get("lastToBeRendered")).intValue();
+ } catch (Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not parse the first and/or last rows.", e);
+ }
+
+ // respect suggested rows only if table is not otherwise updated
+ // (row caches emptied by other event)
+ if (!containerChangeToBeRendered) {
+ Integer value = (Integer) variables.get("reqfirstrow");
+ if (value != null) {
+ reqFirstRowToPaint = value.intValue();
+ }
+ value = (Integer) variables.get("reqrows");
+ if (value != null) {
+ reqRowsToPaint = value.intValue();
+ // sanity check
+ if (reqFirstRowToPaint + reqRowsToPaint > size()) {
+ reqRowsToPaint = size() - reqFirstRowToPaint;
+ }
+ }
+ }
+ getLogger().finest(
+ "Client wants rows " + reqFirstRowToPaint + "-"
+ + (reqFirstRowToPaint + reqRowsToPaint - 1));
+ clientNeedsContentRefresh = true;
+ }
+
+ if (isSortEnabled()) {
+ // Sorting
+ boolean doSort = false;
+ if (variables.containsKey("sortcolumn")) {
+ final String colId = (String) variables.get("sortcolumn");
+ if (colId != null && !"".equals(colId) && !"null".equals(colId)) {
+ final Object id = columnIdMap.get(colId);
+ setSortContainerPropertyId(id, false);
+ doSort = true;
+ }
+ }
+ if (variables.containsKey("sortascending")) {
+ final boolean state = ((Boolean) variables.get("sortascending"))
+ .booleanValue();
+ if (state != sortAscending) {
+ setSortAscending(state, false);
+ doSort = true;
+ }
+ }
+ if (doSort) {
+ this.sort();
+ resetPageBuffer();
+ }
+ }
+
+ // Dynamic column hide/show and order
+ // Update visible columns
+ if (isColumnCollapsingAllowed()) {
+ if (variables.containsKey("collapsedcolumns")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("collapsedcolumns");
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext();) {
+ setColumnCollapsed(it.next(), false);
+ }
+ for (int i = 0; i < ids.length; i++) {
+ setColumnCollapsed(columnIdMap.get(ids[i].toString()),
+ true);
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column collapsing state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+ if (isColumnReorderingAllowed()) {
+ if (variables.containsKey("columnorder")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("columnorder");
+ // need a real Object[], ids can be a String[]
+ final Object[] idsTemp = new Object[ids.length];
+ for (int i = 0; i < ids.length; i++) {
+ idsTemp[i] = columnIdMap.get(ids[i].toString());
+ }
+ setColumnOrder(idsTemp);
+ if (hasListeners(ColumnReorderEvent.class)) {
+ fireEvent(new ColumnReorderEvent(this));
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column reordering state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+
+ enableContentRefreshing(clientNeedsContentRefresh);
+
+ // Actions
+ if (variables.containsKey("action")) {
+ final StringTokenizer st = new StringTokenizer(
+ (String) variables.get("action"), ",");
+ if (st.countTokens() == 2) {
+ final Object itemId = itemIdMapper.get(st.nextToken());
+ final Action action = actionMapper.get(st.nextToken());
+
+ if (action != null && (itemId == null || containsId(itemId))
+ && actionHandlers != null) {
+ for (Handler ah : actionHandlers) {
+ ah.handleAction(action, this, itemId);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Handles click event
+ *
+ * @param variables
+ */
+ private void handleClickEvent(Map<String, Object> variables) {
+
+ // Item click event
+ if (variables.containsKey("clickEvent")) {
+ String key = (String) variables.get("clickedKey");
+ Object itemId = itemIdMapper.get(key);
+ Object propertyId = null;
+ String colkey = (String) variables.get("clickedColKey");
+ // click is not necessary on a property
+ if (colkey != null) {
+ propertyId = columnIdMap.get(colkey);
+ }
+ MouseEventDetails evt = MouseEventDetails
+ .deSerialize((String) variables.get("clickEvent"));
+ Item item = getItem(itemId);
+ if (item != null) {
+ fireEvent(new ItemClickEvent(this, item, itemId, propertyId,
+ evt));
+ }
+ }
+
+ // Header click event
+ else if (variables.containsKey("headerClickEvent")) {
+
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("headerClickEvent"));
+
+ Object cid = variables.get("headerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new HeaderClickEvent(this, propertyId, details));
+ }
+
+ // Footer click event
+ else if (variables.containsKey("footerClickEvent")) {
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("footerClickEvent"));
+
+ Object cid = variables.get("footerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new FooterClickEvent(this, propertyId, details));
+ }
+ }
+
+ /**
+ * Handles the column resize event sent by the client.
+ *
+ * @param variables
+ */
+ private void handleColumnResizeEvent(Map<String, Object> variables) {
+ if (variables.containsKey("columnResizeEventColumn")) {
+ Object cid = variables.get("columnResizeEventColumn");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+
+ Object prev = variables.get("columnResizeEventPrev");
+ int previousWidth = -1;
+ if (prev != null) {
+ previousWidth = Integer.valueOf(prev.toString());
+ }
+
+ Object curr = variables.get("columnResizeEventCurr");
+ int currentWidth = -1;
+ if (curr != null) {
+ currentWidth = Integer.valueOf(curr.toString());
+ }
+
+ fireColumnResizeEvent(propertyId, previousWidth, currentWidth);
+ }
+ }
+ }
+
+ private void fireColumnResizeEvent(Object propertyId, int previousWidth,
+ int currentWidth) {
+ /*
+ * Update the sizes on the server side. If a column previously had a
+ * expand ratio and the user resized the column then the expand ratio
+ * will be turned into a static pixel size.
+ */
+ setColumnWidth(propertyId, currentWidth);
+
+ fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth,
+ currentWidth));
+ }
+
+ private void handleColumnWidthUpdates(Map<String, Object> variables) {
+ if (variables.containsKey("columnWidthUpdates")) {
+ String[] events = (String[]) variables.get("columnWidthUpdates");
+ for (String str : events) {
+ String[] eventDetails = str.split(":");
+ Object propertyId = columnIdMap.get(eventDetails[0]);
+ if (propertyId == null) {
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ int width = Integer.valueOf(eventDetails[1]);
+ setColumnWidth(propertyId, width);
+ }
+ }
+ }
+
+ /**
+ * Go to mode where content updates are not done. This is due we want to
+ * bypass expensive content for some reason (like when we know we may have
+ * other content changes on their way).
+ *
+ * @return true if content refresh flag was enabled prior this call
+ */
+ protected boolean disableContentRefreshing() {
+ boolean wasDisabled = isContentRefreshesEnabled;
+ isContentRefreshesEnabled = false;
+ return wasDisabled;
+ }
+
+ /**
+ * Go to mode where content content refreshing has effect.
+ *
+ * @param refreshContent
+ * true if content refresh needs to be done
+ */
+ protected void enableContentRefreshing(boolean refreshContent) {
+ isContentRefreshesEnabled = true;
+ if (refreshContent) {
+ refreshRenderedCells();
+ // Ensure that client gets a response
+ requestRepaint();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin.
+ * terminal.PaintTarget)
+ */
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ /*
+ * Body actions - Actions which has the target null and can be invoked
+ * by right clicking on the table body.
+ */
+ final Set<Action> actionSet = findAndPaintBodyActions(target);
+
+ final Object[][] cells = getVisibleCells();
+ int rows = findNumRowsToPaint(target, cells);
+
+ int total = size();
+ if (shouldHideNullSelectionItem()) {
+ total--;
+ rows--;
+ }
+
+ // Table attributes
+ paintTableAttributes(target, rows, total);
+
+ paintVisibleColumnOrder(target);
+
+ // Rows
+ if (isPartialRowUpdate() && painted && !target.isFullRepaint()) {
+ paintPartialRowUpdate(target, actionSet);
+ /*
+ * Send the page buffer indexes to ensure that the client side stays
+ * in sync. Otherwise we _might_ have the situation where the client
+ * side discards too few or too many rows, causing out of sync
+ * issues.
+ *
+ * This could probably be done for full repaints also to simplify
+ * the client side.
+ */
+ int pageBufferLastIndex = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1;
+ target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST,
+ pageBufferFirstIndex);
+ target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST,
+ pageBufferLastIndex);
+ } else if (target.isFullRepaint() || isRowCacheInvalidated()) {
+ paintRows(target, cells, actionSet);
+ setRowCacheInvalidated(false);
+ }
+
+ paintSorting(target);
+
+ resetVariablesAndPageBuffer(target);
+
+ // Actions
+ paintActions(target, actionSet);
+
+ paintColumnOrder(target);
+
+ // Available columns
+ paintAvailableColumns(target);
+
+ paintVisibleColumns(target);
+
+ if (keyMapperReset) {
+ keyMapperReset = false;
+ target.addAttribute(VScrollTable.ATTRIBUTE_KEY_MAPPER_RESET, true);
+ }
+
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+
+ painted = true;
+ }
+
+ private void setRowCacheInvalidated(boolean invalidated) {
+ rowCacheInvalidated = invalidated;
+ }
+
+ protected boolean isRowCacheInvalidated() {
+ return rowCacheInvalidated;
+ }
+
+ private void paintPartialRowUpdate(PaintTarget target, Set<Action> actionSet)
+ throws PaintException {
+ paintPartialRowUpdates(target, actionSet);
+ paintPartialRowAdditions(target, actionSet);
+ }
+
+ private void paintPartialRowUpdates(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstUpdatedItemIndex();
+ int count = getUpdatedRowCount();
+
+ target.startTag("urows");
+ target.addAttribute("firsturowix", firstIx);
+ target.addAttribute("numurows", count);
+
+ // Partial row updates bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsUpdateCacheRows(firstIx, count);
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("urows");
+ }
+
+ private void paintPartialRowAdditions(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstAddedItemIndex();
+ int count = getAddedRowCount();
+
+ target.startTag("prows");
+
+ if (!shouldHideAddedRows()) {
+ getLogger().finest(
+ "Paint rows for add. Index: " + firstIx + ", count: "
+ + count + ".");
+
+ // Partial row additions bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsInsertIntoCache(firstIx, count);
+ if (cells[0].length < count) {
+ // delete the rows below, since they will fall beyond the cache
+ // page.
+ target.addAttribute("delbelow", true);
+ count = cells[0].length;
+ }
+
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not
+ // allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ } else {
+ getLogger().finest(
+ "Paint rows for remove. Index: " + firstIx + ", count: "
+ + count + ".");
+ removeRowsFromCacheAndFillBottom(firstIx, count);
+ target.addAttribute("hide", true);
+ }
+
+ target.addAttribute("firstprowix", firstIx);
+ target.addAttribute("numprows", count);
+ target.endTag("prows");
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates and additions,
+ * which bypass the normal caching mechanism. This is useful for e.g.
+ * TreeTable.
+ *
+ * @return true if this update is a partial row update, false if not. For
+ * plain Table it is always false.
+ */
+ protected boolean isPartialRowUpdate() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return The index of the first added item. For plain Table it is always
+ * 0.
+ */
+ protected int getFirstAddedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return the number of rows to be added, starting at the index returned by
+ * {@link #getFirstAddedItemIndex()}. For plain Table it is always
+ * 0.
+ */
+ protected int getAddedRowCount() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable removing of rows, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for e.g.
+ * TreeTable, when you need to hide certain rows as a node is collapsed.
+ *
+ * This should return true if the rows pointed to by
+ * {@link #getFirstAddedItemIndex()} and {@link #getAddedRowCount()} should
+ * be hidden instead of added.
+ *
+ * @return whether the rows to add (see {@link #getFirstAddedItemIndex()}
+ * and {@link #getAddedRowCount()}) should be added or hidden. For
+ * plain Table it is always false.
+ */
+ protected boolean shouldHideAddedRows() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the index of the first item to be updated. For plain Table it is
+ * always 0.
+ */
+ protected int getFirstUpdatedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the number of rows to update, starting at the index returned by
+ * {@link #getFirstUpdatedItemIndex()}. For plain table it is always
+ * 0.
+ */
+ protected int getUpdatedRowCount() {
+ return 0;
+ }
+
+ private void paintTableAttributes(PaintTarget target, int rows, int total)
+ throws PaintException {
+ paintTabIndex(target);
+ paintDragMode(target);
+ paintSelectMode(target);
+
+ if (cacheRate != CACHE_RATE_DEFAULT) {
+ target.addAttribute("cr", cacheRate);
+ }
+
+ target.addAttribute("cols", getVisibleColumns().length);
+ target.addAttribute("rows", rows);
+
+ target.addAttribute("firstrow",
+ (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint
+ : firstToBeRenderedInClient));
+ target.addAttribute("totalrows", total);
+ if (getPageLength() != 0) {
+ target.addAttribute("pagelength", getPageLength());
+ }
+ if (areColumnHeadersEnabled()) {
+ target.addAttribute("colheaders", true);
+ }
+ if (rowHeadersAreEnabled()) {
+ target.addAttribute("rowheaders", true);
+ }
+
+ target.addAttribute("colfooters", columnFootersVisible);
+
+ // The cursors are only shown on pageable table
+ if (getCurrentPageFirstItemIndex() != 0 || getPageLength() > 0) {
+ target.addVariable(this, "firstvisible",
+ getCurrentPageFirstItemIndex());
+ }
+ }
+
+ /**
+ * Resets and paints "to be painted next" variables. Also reset pageBuffer
+ */
+ private void resetVariablesAndPageBuffer(PaintTarget target)
+ throws PaintException {
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ containerChangeToBeRendered = false;
+ target.addVariable(this, "reqrows", reqRowsToPaint);
+ target.addVariable(this, "reqfirstrow", reqFirstRowToPaint);
+ }
+
+ private boolean areColumnHeadersEnabled() {
+ return getColumnHeaderMode() != ColumnHeaderMode.HIDDEN;
+ }
+
+ private void paintVisibleColumns(PaintTarget target) throws PaintException {
+ target.startTag("visiblecolumns");
+ if (rowHeadersAreEnabled()) {
+ target.startTag("column");
+ target.addAttribute("cid", ROW_HEADER_COLUMN_KEY);
+ paintColumnWidth(target, ROW_HEADER_FAKE_PROPERTY_ID);
+ target.endTag("column");
+ }
+ final Collection<?> sortables = getSortableContainerPropertyIds();
+ for (Object colId : visibleColumns) {
+ if (colId != null) {
+ target.startTag("column");
+ target.addAttribute("cid", columnIdMap.key(colId));
+ final String head = getColumnHeader(colId);
+ target.addAttribute("caption", (head != null ? head : ""));
+ final String foot = getColumnFooter(colId);
+ target.addAttribute("fcaption", (foot != null ? foot : ""));
+ if (isColumnCollapsed(colId)) {
+ target.addAttribute("collapsed", true);
+ }
+ if (areColumnHeadersEnabled()) {
+ if (getColumnIcon(colId) != null) {
+ target.addAttribute("icon", getColumnIcon(colId));
+ }
+ if (sortables.contains(colId)) {
+ target.addAttribute("sortable", true);
+ }
+ }
+ if (!Align.LEFT.equals(getColumnAlignment(colId))) {
+ target.addAttribute("align", getColumnAlignment(colId)
+ .toString());
+ }
+ paintColumnWidth(target, colId);
+ target.endTag("column");
+ }
+ }
+ target.endTag("visiblecolumns");
+ }
+
+ private void paintAvailableColumns(PaintTarget target)
+ throws PaintException {
+ if (columnCollapsingAllowed) {
+ final HashSet<Object> collapsedCols = new HashSet<Object>();
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedCols.add(colId);
+ }
+ }
+ final String[] collapsedKeys = new String[collapsedCols.size()];
+ int nextColumn = 0;
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ }
+ target.addVariable(this, "collapsedcolumns", collapsedKeys);
+
+ final String[] noncollapsibleKeys = new String[noncollapsibleColumns
+ .size()];
+ nextColumn = 0;
+ for (Object colId : noncollapsibleColumns) {
+ noncollapsibleKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "noncollapsiblecolumns",
+ noncollapsibleKeys);
+ }
+
+ }
+
+ private void paintActions(PaintTarget target, final Set<Action> actionSet)
+ throws PaintException {
+ if (!actionSet.isEmpty()) {
+ target.addVariable(this, "action", "");
+ target.startTag("actions");
+ for (Action a : actionSet) {
+ target.startTag("action");
+ if (a.getCaption() != null) {
+ target.addAttribute("caption", a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ target.addAttribute("icon", a.getIcon());
+ }
+ target.addAttribute("key", actionMapper.key(a));
+ target.endTag("action");
+ }
+ target.endTag("actions");
+ }
+ }
+
+ private void paintColumnOrder(PaintTarget target) throws PaintException {
+ if (columnReorderingAllowed) {
+ final String[] colorder = new String[visibleColumns.size()];
+ int i = 0;
+ for (Object colId : visibleColumns) {
+ colorder[i++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "columnorder", colorder);
+ }
+ }
+
+ private void paintSorting(PaintTarget target) throws PaintException {
+ // Sorting
+ if (getContainerDataSource() instanceof Container.Sortable) {
+ target.addVariable(this, "sortcolumn",
+ columnIdMap.key(sortContainerPropertyId));
+ target.addVariable(this, "sortascending", sortAscending);
+ }
+ }
+
+ private void paintRows(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ target.startTag("rows");
+ // cells array contains all that are supposed to be visible on client,
+ // but we'll start from the one requested by client
+ int start = 0;
+ if (reqFirstRowToPaint != -1 && firstToBeRenderedInClient != -1) {
+ start = reqFirstRowToPaint - firstToBeRenderedInClient;
+ }
+ int end = cells[0].length;
+ if (reqRowsToPaint != -1) {
+ end = start + reqRowsToPaint;
+ }
+ // sanity check
+ if (lastToBeRenderedInClient != -1 && lastToBeRenderedInClient < end) {
+ end = lastToBeRenderedInClient + 1;
+ }
+ if (start > cells[CELL_ITEMID].length || start < 0) {
+ start = 0;
+ }
+ if (end > cells[CELL_ITEMID].length) {
+ end = cells[CELL_ITEMID].length;
+ }
+
+ for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("rows");
+ }
+
+ private boolean[] findCellsWithComponents() {
+ final boolean[] isComponent = new boolean[visibleColumns.size()];
+ int ix = 0;
+ for (Object columnId : visibleColumns) {
+ if (columnGenerators.containsKey(columnId)) {
+ isComponent[ix++] = true;
+ } else {
+ final Class<?> colType = getType(columnId);
+ isComponent[ix++] = colType != null
+ && Component.class.isAssignableFrom(colType);
+ }
+ }
+ return isComponent;
+ }
+
+ private void paintVisibleColumnOrder(PaintTarget target) {
+ // Visible column order
+ final ArrayList<String> visibleColOrder = new ArrayList<String>();
+ for (Object columnId : visibleColumns) {
+ if (!isColumnCollapsed(columnId)) {
+ visibleColOrder.add(columnIdMap.key(columnId));
+ }
+ }
+ target.addAttribute("vcolorder", visibleColOrder.toArray());
+ }
+
+ private Set<Action> findAndPaintBodyActions(PaintTarget target) {
+ Set<Action> actionSet = new LinkedHashSet<Action>();
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ // Getting actions for the null item, which in this case means
+ // the body item
+ final Action[] actions = ah.getActions(null, this);
+ if (actions != null) {
+ for (Action action : actions) {
+ actionSet.add(action);
+ keys.add(actionMapper.key(action));
+ }
+ }
+ }
+ target.addAttribute("alb", keys.toArray());
+ }
+ return actionSet;
+ }
+
+ private boolean shouldHideNullSelectionItem() {
+ return !isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && containsId(getNullSelectionItemId());
+ }
+
+ private int findNumRowsToPaint(PaintTarget target, final Object[][] cells)
+ throws PaintException {
+ int rows;
+ if (reqRowsToPaint >= 0) {
+ rows = reqRowsToPaint;
+ } else {
+ rows = cells[0].length;
+ if (alwaysRecalculateColumnWidths) {
+ // TODO experimental feature for now: tell the client to
+ // recalculate column widths.
+ // We'll only do this for paints that do not originate from
+ // table scroll/cache requests (i.e when reqRowsToPaint<0)
+ target.addAttribute("recalcWidths", true);
+ }
+ }
+ return rows;
+ }
+
+ private void paintSelectMode(PaintTarget target) throws PaintException {
+ if (multiSelectMode != MultiSelectMode.DEFAULT) {
+ target.addAttribute("multiselectmode", multiSelectMode.ordinal());
+ }
+ if (isSelectable()) {
+ target.addAttribute("selectmode", (isMultiSelect() ? "multi"
+ : "single"));
+ } else {
+ target.addAttribute("selectmode", "none");
+ }
+ if (!isNullSelectionAllowed()) {
+ target.addAttribute("nsa", false);
+ }
+
+ // selection support
+ // The select variable is only enabled if selectable
+ if (isSelectable()) {
+ target.addVariable(this, "selected", findSelectedKeys());
+ }
+ }
+
+ private String[] findSelectedKeys() {
+ LinkedList<String> selectedKeys = new LinkedList<String>();
+ if (isMultiSelect()) {
+ HashSet<?> sel = new HashSet<Object>((Set<?>) getValue());
+ Collection<?> vids = getVisibleItemIds();
+ for (Iterator<?> it = vids.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (sel.contains(id)) {
+ selectedKeys.add(itemIdMapper.key(id));
+ }
+ }
+ } else {
+ Object value = getValue();
+ if (value == null) {
+ value = getNullSelectionItemId();
+ }
+ if (value != null) {
+ selectedKeys.add(itemIdMapper.key(value));
+ }
+ }
+ return selectedKeys.toArray(new String[selectedKeys.size()]);
+ }
+
+ private void paintDragMode(PaintTarget target) throws PaintException {
+ if (dragMode != TableDragMode.NONE) {
+ target.addAttribute("dragmode", dragMode.ordinal());
+ }
+ }
+
+ private void paintTabIndex(PaintTarget target) throws PaintException {
+ // The tab ordering number
+ if (getTabIndex() > 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+ }
+
+ private void paintColumnWidth(PaintTarget target, final Object columnId)
+ throws PaintException {
+ if (columnWidths.containsKey(columnId)) {
+ if (getColumnWidth(columnId) > -1) {
+ target.addAttribute("width",
+ String.valueOf(getColumnWidth(columnId)));
+ } else {
+ target.addAttribute("er", getColumnExpandRatio(columnId));
+ }
+ }
+ }
+
+ private boolean rowHeadersAreEnabled() {
+ return getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN;
+ }
+
+ private void paintRow(PaintTarget target, final Object[][] cells,
+ final boolean iseditable, final Set<Action> actionSet,
+ final boolean[] iscomponent, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ target.startTag("tr");
+
+ paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId);
+
+ // cells
+ int currentColumn = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); currentColumn++) {
+ final Object columnId = it.next();
+ if (columnId == null || isColumnCollapsed(columnId)) {
+ continue;
+ }
+ /*
+ * For each cell, if a cellStyleGenerator is specified, get the
+ * specific style for the cell. If there is any, add it to the
+ * target.
+ */
+ if (cellStyleGenerator != null) {
+ String cellStyle = cellStyleGenerator
+ .getStyle(itemId, columnId);
+ if (cellStyle != null && !cellStyle.equals("")) {
+ target.addAttribute("style-" + columnIdMap.key(columnId),
+ cellStyle);
+ }
+ }
+
+ if ((iscomponent[currentColumn] || iseditable || cells[CELL_GENERATED_ROW][indexInRowbuffer] != null)
+ && Component.class.isInstance(cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer])) {
+ final Component c = (Component) cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer];
+ if (c == null) {
+ target.addText("");
+ paintCellTooltips(target, itemId, columnId);
+ } else {
+ LegacyPaint.paint(c, target);
+ }
+ } else {
+ target.addText((String) cells[CELL_FIRSTCOL + currentColumn][indexInRowbuffer]);
+ paintCellTooltips(target, itemId, columnId);
+ }
+ }
+
+ target.endTag("tr");
+ }
+
+ private void paintCellTooltips(PaintTarget target, Object itemId,
+ Object columnId) throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String itemDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, columnId);
+ if (itemDescription != null && !itemDescription.equals("")) {
+ target.addAttribute("descr-" + columnIdMap.key(columnId),
+ itemDescription);
+ }
+ }
+ }
+
+ private void paintRowTooltips(PaintTarget target, Object itemId)
+ throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String rowDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, null);
+ if (rowDescription != null && !rowDescription.equals("")) {
+ target.addAttribute("rowdescr", rowDescription);
+ }
+ }
+ }
+
+ private void paintRowAttributes(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ // tr attributes
+
+ paintRowIcon(target, cells, indexInRowbuffer);
+ paintRowHeader(target, cells, indexInRowbuffer);
+ paintGeneratedRowInfo(target, cells, indexInRowbuffer);
+ target.addAttribute("key",
+ Integer.parseInt(cells[CELL_KEY][indexInRowbuffer].toString()));
+
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+
+ // Actions
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ final Action[] aa = ah.getActions(itemId, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String key = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(key);
+ }
+ }
+ }
+ target.addAttribute("al", keys.toArray());
+ }
+
+ /*
+ * For each row, if a cellStyleGenerator is specified, get the specific
+ * style for the cell, using null as propertyId. If there is any, add it
+ * to the target.
+ */
+ if (cellStyleGenerator != null) {
+ String rowStyle = cellStyleGenerator.getStyle(itemId, null);
+ if (rowStyle != null && !rowStyle.equals("")) {
+ target.addAttribute("rowstyle", rowStyle);
+ }
+ }
+
+ paintRowTooltips(target, itemId);
+
+ paintRowAttributes(target, itemId);
+ }
+
+ private void paintGeneratedRowInfo(PaintTarget target, Object[][] cells,
+ int indexInRowBuffer) throws PaintException {
+ GeneratedRow generatedRow = (GeneratedRow) cells[CELL_GENERATED_ROW][indexInRowBuffer];
+ if (generatedRow != null) {
+ target.addAttribute("gen_html", generatedRow.isHtmlContentAllowed());
+ target.addAttribute("gen_span", generatedRow.isSpanColumns());
+ target.addAttribute("gen_widget",
+ generatedRow.getValue() instanceof Component);
+ }
+ }
+
+ protected void paintRowHeader(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()) {
+ if (cells[CELL_HEADER][indexInRowbuffer] != null) {
+ target.addAttribute("caption",
+ (String) cells[CELL_HEADER][indexInRowbuffer]);
+ }
+ }
+
+ }
+
+ protected void paintRowIcon(PaintTarget target, final Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()
+ && cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ /**
+ * A method where extended Table implementations may add their custom
+ * attributes for rows.
+ *
+ * @param target
+ * @param itemId
+ */
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+
+ }
+
+ /**
+ * Gets the cached visible table contents.
+ *
+ * @return the cached visible table contents.
+ */
+ private Object[][] getVisibleCells() {
+ if (pageBuffer == null) {
+ refreshRenderedCells();
+ }
+ return pageBuffer;
+ }
+
+ /**
+ * Gets the value of property.
+ *
+ * By default if the table is editable the fieldFactory is used to create
+ * editors for table cells. Otherwise formatPropertyValue is used to format
+ * the value representation.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be presented.
+ * @return Object Either formatted value or Component for field.
+ * @see #setTableFieldFactory(TableFieldFactory)
+ */
+ protected Object getPropertyValue(Object rowId, Object colId,
+ Property property) {
+ if (isEditable() && fieldFactory != null) {
+ final Field<?> f = fieldFactory.createField(
+ getContainerDataSource(), rowId, colId, this);
+ if (f != null) {
+ // Remember that we have made this association so we can remove
+ // it when the component is removed
+ associatedProperties.put(f, property);
+ bindPropertyToField(rowId, colId, property, f);
+ return f;
+ }
+ }
+
+ return formatPropertyValue(rowId, colId, property);
+ }
+
+ /**
+ * Binds an item property to a field generated by TableFieldFactory. The
+ * default behavior is to bind property straight to Field. If
+ * Property.Viewer type property (e.g. PropertyFormatter) is already set for
+ * field, the property is bound to that Property.Viewer.
+ *
+ * @param rowId
+ * @param colId
+ * @param property
+ * @param field
+ * @since 6.7.3
+ */
+ protected void bindPropertyToField(Object rowId, Object colId,
+ Property property, Field field) {
+ // check if field has a property that is Viewer set. In that case we
+ // expect developer has e.g. PropertyFormatter that he wishes to use and
+ // assign the property to the Viewer instead.
+ boolean hasFilterProperty = field.getPropertyDataSource() != null
+ && (field.getPropertyDataSource() instanceof Property.Viewer);
+ if (hasFilterProperty) {
+ ((Property.Viewer) field.getPropertyDataSource())
+ .setPropertyDataSource(property);
+ } else {
+ field.setPropertyDataSource(property);
+ }
+ }
+
+ /**
+ * Formats table cell property values. By default the property.toString()
+ * and return a empty string for null properties.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be formatted.
+ * @return the String representation of property and its value.
+ * @since 3.1
+ */
+ protected String formatPropertyValue(Object rowId, Object colId,
+ Property<?> property) {
+ if (property == null) {
+ return "";
+ }
+ Converter<String, Object> converter = null;
+
+ if (hasConverter(colId)) {
+ converter = getConverter(colId);
+ } else {
+ ConverterUtil.getConverter(String.class, property.getType(),
+ getApplication());
+ }
+ Object value = property.getValue();
+ if (converter != null) {
+ return converter.convertToPresentation(value, getLocale());
+ }
+ return (null != value) ? value.toString() : "";
+ }
+
+ /* Action container */
+
+ /**
+ * Registers a new action handler for this container
+ *
+ * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void addActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ }
+ }
+
+ /**
+ * Removes a previously registered action handler for the contents of this
+ * container.
+ *
+ * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ actionHandlers.remove(actionHandler);
+
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ actionHandlers = null;
+ actionMapper = null;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ /* Property value change listening support */
+
+ /**
+ * Notifies this listener that the Property's value has changed.
+ *
+ * Also listens changes in rendered items to refresh content area.
+ *
+ * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent)
+ */
+
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ if (event.getProperty() == this
+ || event.getProperty() == getPropertyDataSource()) {
+ super.valueChange(event);
+ } else {
+ refreshRowCache();
+ containerChangeToBeRendered = true;
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Clears the current page buffer. Call this before
+ * {@link #refreshRenderedCells()} to ensure that all content is updated
+ * from the properties.
+ */
+ protected void resetPageBuffer() {
+ firstToBeRenderedInClient = -1;
+ lastToBeRenderedInClient = -1;
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ pageBuffer = null;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+
+ @Override
+ public void attach() {
+ super.attach();
+
+ refreshRenderedCells();
+ }
+
+ /**
+ * Notifies the component that it is detached from the application
+ *
+ * @see com.vaadin.ui.Component#detach()
+ */
+
+ @Override
+ public void detach() {
+ super.detach();
+ }
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * @see com.vaadin.data.Container#removeAllItems()
+ */
+
+ @Override
+ public boolean removeAllItems() {
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+ return super.removeAllItems();
+ }
+
+ /**
+ * Removes the Item identified by <code>ItemId</code> from the Container.
+ *
+ * @see com.vaadin.data.Container#removeItem(Object)
+ */
+
+ @Override
+ public boolean removeItem(Object itemId) {
+ final Object nextItemId = nextItemId(itemId);
+ final boolean ret = super.removeItem(itemId);
+ if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) {
+ currentPageFirstItemId = nextItemId;
+ }
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return ret;
+ }
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ *
+ * @see com.vaadin.data.Container#removeContainerProperty(Object)
+ */
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ // If a visible property is removed, remove the corresponding column
+ visibleColumns.remove(propertyId);
+ columnAlignments.remove(propertyId);
+ columnIcons.remove(propertyId);
+ columnHeaders.remove(propertyId);
+ columnFooters.remove(propertyId);
+
+ return super.removeContainerProperty(propertyId);
+ }
+
+ /**
+ * Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the proprty.
+ * @param type
+ * the class of the property.
+ * @param defaultValue
+ * the default value given for all existing items.
+ * @see com.vaadin.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ boolean visibleColAdded = false;
+ if (!visibleColumns.contains(propertyId)) {
+ visibleColumns.add(propertyId);
+ visibleColAdded = true;
+ }
+
+ if (!super.addContainerProperty(propertyId, type, defaultValue)) {
+ if (visibleColAdded) {
+ visibleColumns.remove(propertyId);
+ }
+ return false;
+ }
+ if (!(items instanceof Container.PropertySetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return true;
+ }
+
+ /**
+ * Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the proprty
+ * @param type
+ * the class of the property
+ * @param defaultValue
+ * the default value given for all existing items
+ * @param columnHeader
+ * the Explicit header of the column. If explicit header is not
+ * needed, this should be set null.
+ * @param columnIcon
+ * the Icon of the column. If icon is not needed, this should be
+ * set null.
+ * @param columnAlignment
+ * the Alignment of the column. Null implies align left.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported.
+ * @see com.vaadin.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue, String columnHeader, Resource columnIcon,
+ Align columnAlignment) throws UnsupportedOperationException {
+ if (!this.addContainerProperty(propertyId, type, defaultValue)) {
+ return false;
+ }
+ setColumnAlignment(propertyId, columnAlignment);
+ setColumnHeader(propertyId, columnHeader);
+ setColumnIcon(propertyId, columnIcon);
+ return true;
+ }
+
+ /**
+ * Adds a generated column to the Table.
+ * <p>
+ * A generated column is a column that exists only in the Table, not as a
+ * property in the underlying Container. It shows up just as a regular
+ * column.
+ * </p>
+ * <p>
+ * A generated column will override a property with the same id, so that the
+ * generated column is shown instead of the column representing the
+ * property. Note that getContainerProperty() will still get the real
+ * property.
+ * </p>
+ * <p>
+ * Table will not listen to value change events from properties overridden
+ * by generated columns. If the content of your generated column depends on
+ * properties that are not directly visible in the table, attach value
+ * change listener to update the content on all depended properties.
+ * Otherwise your UI might not get updated as expected.
+ * </p>
+ * <p>
+ * Also note that getVisibleColumns() will return the generated columns,
+ * while getContainerPropertyIds() will not.
+ * </p>
+ *
+ * @param id
+ * the id of the column to be added
+ * @param generatedColumn
+ * the {@link ColumnGenerator} to use for this column
+ */
+ public void addGeneratedColumn(Object id, ColumnGenerator generatedColumn) {
+ if (generatedColumn == null) {
+ throw new IllegalArgumentException(
+ "Can not add null as a GeneratedColumn");
+ }
+ if (columnGenerators.containsKey(id)) {
+ throw new IllegalArgumentException(
+ "Can not add the same GeneratedColumn twice, id:" + id);
+ } else {
+ columnGenerators.put(id, generatedColumn);
+ /*
+ * add to visible column list unless already there (overriding
+ * column from DS)
+ */
+ if (!visibleColumns.contains(id)) {
+ visibleColumns.add(id);
+ }
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * Returns the ColumnGenerator used to generate the given column.
+ *
+ * @param columnId
+ * The id of the generated column
+ * @return The ColumnGenerator used for the given columnId or null.
+ */
+ public ColumnGenerator getColumnGenerator(Object columnId)
+ throws IllegalArgumentException {
+ return columnGenerators.get(columnId);
+ }
+
+ /**
+ * Removes a generated column previously added with addGeneratedColumn.
+ *
+ * @param columnId
+ * id of the generated column to remove
+ * @return true if the column could be removed (existed in the Table)
+ */
+ public boolean removeGeneratedColumn(Object columnId) {
+ if (columnGenerators.containsKey(columnId)) {
+ columnGenerators.remove(columnId);
+ // remove column from visibleColumns list unless it exists in
+ // container (generator previously overrode this column)
+ if (!items.getContainerPropertyIds().contains(columnId)) {
+ visibleColumns.remove(columnId);
+ }
+ refreshRowCache();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns item identifiers of the items which are currently rendered on the
+ * client.
+ * <p>
+ * Note, that some due to historical reasons the name of the method is bit
+ * misleading. Some items may be partly or totally out of the viewport of
+ * the table's scrollable area. Actually detecting rows which can be
+ * actually seen by the end user may be problematic due to the client server
+ * architecture. Using {@link #getCurrentPageFirstItemId()} combined with
+ * {@link #getPageLength()} may produce good enough estimates in some
+ * situations.
+ *
+ * @see com.vaadin.ui.Select#getVisibleItemIds()
+ */
+
+ @Override
+ public Collection<?> getVisibleItemIds() {
+
+ final LinkedList<Object> visible = new LinkedList<Object>();
+
+ final Object[][] cells = getVisibleCells();
+ // may be null if the table has not been rendered yet (e.g. not attached
+ // to a layout)
+ if (null != cells) {
+ for (int i = 0; i < cells[CELL_ITEMID].length; i++) {
+ visible.add(cells[CELL_ITEMID][i]);
+ }
+ }
+
+ return visible;
+ }
+
+ /**
+ * Container datasource item set change. Table must flush its buffers on
+ * change.
+ *
+ * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent)
+ */
+
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ super.containerItemSetChange(event);
+
+ // super method clears the key map, must inform client about this to
+ // avoid getting invalid keys back (#8584)
+ keyMapperReset = true;
+
+ // ensure that page still has first item in page, ignore buffer refresh
+ // (forced in this method)
+ setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false);
+ refreshRowCache();
+ }
+
+ /**
+ * Container datasource property set change. Table must flush its buffers on
+ * change.
+ *
+ * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent)
+ */
+
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ disableContentRefreshing();
+ super.containerPropertySetChange(event);
+
+ // sanitetize visibleColumns. note that we are not adding previously
+ // non-existing properties as columns
+ Collection<?> containerPropertyIds = getContainerDataSource()
+ .getContainerPropertyIds();
+
+ LinkedList<Object> newVisibleColumns = new LinkedList<Object>(
+ visibleColumns);
+ for (Iterator<Object> iterator = newVisibleColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id) || columnGenerators
+ .containsKey(id))) {
+ iterator.remove();
+ }
+ }
+ setVisibleColumns(newVisibleColumns.toArray());
+ // same for collapsed columns
+ for (Iterator<Object> iterator = collapsedColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id) || columnGenerators
+ .containsKey(id))) {
+ iterator.remove();
+ }
+ }
+
+ resetPageBuffer();
+ enableContentRefreshing(true);
+ }
+
+ /**
+ * Adding new items is not supported.
+ *
+ * @throws UnsupportedOperationException
+ * if set to true.
+ * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean)
+ */
+
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Gets the ID of the Item following the Item that corresponds to itemId.
+ *
+ * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return ((Container.Ordered) items).nextItemId(itemId);
+ }
+
+ /**
+ * Gets the ID of the Item preceding the Item that corresponds to the
+ * itemId.
+ *
+ * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return ((Container.Ordered) items).prevItemId(itemId);
+ }
+
+ /**
+ * Gets the ID of the first Item in the Container.
+ *
+ * @see com.vaadin.data.Container.Ordered#firstItemId()
+ */
+
+ @Override
+ public Object firstItemId() {
+ return ((Container.Ordered) items).firstItemId();
+ }
+
+ /**
+ * Gets the ID of the last Item in the Container.
+ *
+ * @see com.vaadin.data.Container.Ordered#lastItemId()
+ */
+
+ @Override
+ public Object lastItemId() {
+ return ((Container.Ordered) items).lastItemId();
+ }
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the first Item in
+ * the Container.
+ *
+ * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return ((Container.Ordered) items).isFirstId(itemId);
+ }
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the last Item in
+ * the Container.
+ *
+ * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return ((Container.Ordered) items).isLastId(itemId);
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ Object itemId = ((Container.Ordered) items)
+ .addItemAfter(previousItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return itemId;
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ Item item = ((Container.Ordered) items).addItemAfter(previousItemId,
+ newItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return item;
+ }
+
+ /**
+ * Sets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The TableFieldFactory is only used if the Table is editable. By default
+ * the DefaultFieldFactory is used.
+ *
+ * @param fieldFactory
+ * the field factory to set.
+ * @see #isEditable
+ * @see DefaultFieldFactory
+ */
+ public void setTableFieldFactory(TableFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The FieldFactory is only used if the Table is editable.
+ *
+ * @return TableFieldFactory used to create the Field instances.
+ * @see #isEditable
+ */
+ public TableFieldFactory getTableFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Is table editable.
+ *
+ * If table is editable a editor of type Field is created for each table
+ * cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementins the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @return true if table is editable, false oterwise.
+ * @see Field
+ * @see FieldFactory
+ *
+ */
+ public boolean isEditable() {
+ return editable;
+ }
+
+ /**
+ * Sets the editable property.
+ *
+ * If table is editable a editor of type Field is created for each table
+ * cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementins the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @param editable
+ * true if table should be editable by user.
+ * @see Field
+ * @see FieldFactory
+ *
+ */
+ public void setEditable(boolean editable) {
+ this.editable = editable;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Sorts the table.
+ *
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ *
+ */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending)
+ throws UnsupportedOperationException {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ final int pageIndex = getCurrentPageFirstItemIndex();
+ ((Container.Sortable) c).sort(propertyId, ascending);
+ setCurrentPageFirstItemIndex(pageIndex);
+ refreshRowCache();
+
+ } else if (c != null) {
+ throw new UnsupportedOperationException(
+ "Underlying Data does not allow sorting");
+ }
+ }
+
+ /**
+ * Sorts the table by currently selected sorting column.
+ *
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ */
+ public void sort() {
+ if (getSortContainerPropertyId() == null) {
+ return;
+ }
+ sort(new Object[] { sortContainerPropertyId },
+ new boolean[] { sortAscending });
+ }
+
+ /**
+ * Gets the container property IDs, which can be used to sort the item.
+ * <p>
+ * Note that the {@link #isSortEnabled()} state affects what this method
+ * returns. Disabling sorting causes this method to always return an empty
+ * collection.
+ * </p>
+ *
+ * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable && isSortEnabled()) {
+ return ((Container.Sortable) c).getSortableContainerPropertyIds();
+ } else {
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ /**
+ * Gets the currently sorted column property ID.
+ *
+ * @return the Container property id of the currently sorted column.
+ */
+ public Object getSortContainerPropertyId() {
+ return sortContainerPropertyId;
+ }
+
+ /**
+ * Sets the currently sorted column property id.
+ *
+ * @param propertyId
+ * the Container property id of the currently sorted column.
+ */
+ public void setSortContainerPropertyId(Object propertyId) {
+ setSortContainerPropertyId(propertyId, true);
+ }
+
+ /**
+ * Internal method to set currently sorted column property id. With doSort
+ * flag actual sorting may be bypassed.
+ *
+ * @param propertyId
+ * @param doSort
+ */
+ private void setSortContainerPropertyId(Object propertyId, boolean doSort) {
+ if ((sortContainerPropertyId != null && !sortContainerPropertyId
+ .equals(propertyId))
+ || (sortContainerPropertyId == null && propertyId != null)) {
+ sortContainerPropertyId = propertyId;
+
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is the table currently sorted in ascending order.
+ *
+ * @return <code>true</code> if ascending, <code>false</code> if descending.
+ */
+ public boolean isSortAscending() {
+ return sortAscending;
+ }
+
+ /**
+ * Sets the table in ascending order.
+ *
+ * @param ascending
+ * <code>true</code> if ascending, <code>false</code> if
+ * descending.
+ */
+ public void setSortAscending(boolean ascending) {
+ setSortAscending(ascending, true);
+ }
+
+ /**
+ * Internal method to set sort ascending. With doSort flag actual sort can
+ * be bypassed.
+ *
+ * @param ascending
+ * @param doSort
+ */
+ private void setSortAscending(boolean ascending, boolean doSort) {
+ if (sortAscending != ascending) {
+ sortAscending = ascending;
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is sorting disabled altogether.
+ *
+ * True iff no sortable columns are given even in the case where data source
+ * would support this.
+ *
+ * @return True iff sorting is disabled.
+ * @deprecated Use {@link #isSortEnabled()} instead
+ */
+ @Deprecated
+ public boolean isSortDisabled() {
+ return !isSortEnabled();
+ }
+
+ /**
+ * Checks if sorting is enabled.
+ *
+ * @return true if sorting by the user is allowed, false otherwise
+ */
+ public boolean isSortEnabled() {
+ return sortEnabled;
+ }
+
+ /**
+ * Disables the sorting by the user altogether.
+ *
+ * @param sortDisabled
+ * True iff sorting is disabled.
+ * @deprecated Use {@link #setSortEnabled(boolean)} instead
+ */
+ @Deprecated
+ public void setSortDisabled(boolean sortDisabled) {
+ setSortEnabled(!sortDisabled);
+ }
+
+ /**
+ * Enables or disables sorting.
+ * <p>
+ * Setting this to false disallows sorting by the user. It is still possible
+ * to call {@link #sort()}.
+ * </p>
+ *
+ * @param sortEnabled
+ * true to allow the user to sort the table, false to disallow it
+ */
+ public void setSortEnabled(boolean sortEnabled) {
+ if (this.sortEnabled != sortEnabled) {
+ this.sortEnabled = sortEnabled;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Used to create "generated columns"; columns that exist only in the Table,
+ * not in the underlying Container. Implement this interface and pass it to
+ * Table.addGeneratedColumn along with an id for the column to be generated.
+ *
+ */
+ public interface ColumnGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell in a generated column needs to be
+ * generated.
+ *
+ * @param source
+ * the source Table
+ * @param itemId
+ * the itemId (aka rowId) for the of the cell to be generated
+ * @param columnId
+ * the id for the generated column (as specified in
+ * addGeneratedColumn)
+ * @return A {@link Component} that should be rendered in the cell or a
+ * {@link String} that should be displayed in the cell. Other
+ * return values are not supported.
+ */
+ public abstract Object generateCell(Table source, Object itemId,
+ Object columnId);
+ }
+
+ /**
+ * Set cell style generator for Table.
+ *
+ * @param cellStyleGenerator
+ * New cell style generator or null to remove generator.
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the style generators
+ refreshRenderedCells();
+
+ }
+
+ /**
+ * Get the current cell style generator.
+ *
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Allow to define specific style on cells (and rows) contents. Implements
+ * this interface and pass it to Table.setCellStyleGenerator. Row styles are
+ * generated when porpertyId is null. The CSS class name that will be added
+ * to the cell content is <tt>v-table-cell-content-[style name]</tt>, and
+ * the row style will be <tt>v-table-row-[style name]</tt>.
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell (and row) is painted.
+ *
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row style
+ * @return The style name to add to this cell or row. (the CSS class
+ * name will be v-table-cell-content-[style name], or
+ * v-table-row-[style name] for rows)
+ */
+ public abstract String getStyle(Object itemId, Object propertyId);
+ }
+
+ @Override
+ public void addListener(ItemClickListener listener) {
+ addListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ public void removeListener(ItemClickListener listener) {
+ removeListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ // Identical to AbstractCompoenentContainer.setEnabled();
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (getParent() != null && !getParent().isEnabled()) {
+ // some ancestor still disabled, don't update children
+ return;
+ } else {
+ requestRepaintAll();
+ }
+ }
+
+ /**
+ * Sets the drag start mode of the Table. Drag start mode controls how Table
+ * behaves as a drag source.
+ *
+ * @param newDragMode
+ */
+ public void setDragMode(TableDragMode newDragMode) {
+ dragMode = newDragMode;
+ requestRepaint();
+ }
+
+ /**
+ * @return the current start mode of the Table. Drag start mode controls how
+ * Table behaves as a drag source.
+ */
+ public TableDragMode getDragMode() {
+ return dragMode;
+ }
+
+ /**
+ * Concrete implementation of {@link DataBoundTransferable} for data
+ * transferred from a table.
+ *
+ * @see {@link DataBoundTransferable}.
+ *
+ * @since 6.3
+ */
+ public class TableTransferable extends DataBoundTransferable {
+
+ protected TableTransferable(Map<String, Object> rawVariables) {
+ super(Table.this, rawVariables);
+ Object object = rawVariables.get("itemId");
+ if (object != null) {
+ setData("itemId", itemIdMapper.get((String) object));
+ }
+ object = rawVariables.get("propertyId");
+ if (object != null) {
+ setData("propertyId", columnIdMap.get((String) object));
+ }
+ }
+
+ @Override
+ public Object getItemId() {
+ return getData("itemId");
+ }
+
+ @Override
+ public Object getPropertyId() {
+ return getData("propertyId");
+ }
+
+ @Override
+ public Table getSourceComponent() {
+ return (Table) super.getSourceComponent();
+ }
+
+ }
+
+ @Override
+ public TableTransferable getTransferable(Map<String, Object> rawVariables) {
+ TableTransferable transferable = new TableTransferable(rawVariables);
+ return transferable;
+ }
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ @Override
+ public AbstractSelectTargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new AbstractSelectTargetDetails(clientVariables);
+ }
+
+ /**
+ * Sets the behavior of how the multi-select mode should behave when the
+ * table is both selectable and in multi-select mode.
+ * <p>
+ * Note, that on some clients the mode may not be respected. E.g. on touch
+ * based devices CTRL/SHIFT base selection method is invalid, so touch based
+ * browsers always use the {@link MultiSelectMode#SIMPLE}.
+ *
+ * @param mode
+ * The select mode of the table
+ */
+ public void setMultiSelectMode(MultiSelectMode mode) {
+ multiSelectMode = mode;
+ requestRepaint();
+ }
+
+ /**
+ * Returns the select mode in which multi-select is used.
+ *
+ * @return The multi select mode
+ */
+ public MultiSelectMode getMultiSelectMode() {
+ return multiSelectMode;
+ }
+
+ /**
+ * Lazy loading accept criterion for Table. Accepted target rows are loaded
+ * from server once per drag and drop operation. Developer must override one
+ * method that decides on which rows the currently dragged data can be
+ * dropped.
+ *
+ * <p>
+ * Initially pretty much no data is sent to client. On first required
+ * criterion check (per drag request) the client side data structure is
+ * initialized from server and no subsequent requests requests are needed
+ * during that drag and drop operation.
+ */
+ public static abstract class TableDropCriterion extends ServerSideCriterion {
+
+ private Table table;
+
+ private Set<Object> allowedItemIds;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.ServerSideCriterion#getIdentifier
+ * ()
+ */
+
+ @Override
+ protected String getIdentifier() {
+ return TableDropCriterion.class.getCanonicalName();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accepts(com.vaadin
+ * .event.dd.DragAndDropEvent)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ table = (Table) dragEvent.getTargetDetails().getTarget();
+ Collection<?> visibleItemIds = table.getVisibleItemIds();
+ allowedItemIds = getAllowedItemIds(dragEvent, table,
+ (Collection<Object>) visibleItemIds);
+
+ return allowedItemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#paintResponse(
+ * com.vaadin.terminal.PaintTarget)
+ */
+
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ /*
+ * send allowed nodes to client so subsequent requests can be
+ * avoided
+ */
+ Object[] array = allowedItemIds.toArray();
+ for (int i = 0; i < array.length; i++) {
+ String key = table.itemIdMapper.key(array[i]);
+ array[i] = key;
+ }
+ target.addAttribute("allowedIds", array);
+ }
+
+ /**
+ * @param dragEvent
+ * @param table
+ * the table for which the allowed item identifiers are
+ * defined
+ * @param visibleItemIds
+ * the list of currently rendered item identifiers, accepted
+ * item id's need to be detected only for these visible items
+ * @return the set of identifiers for items on which the dragEvent will
+ * be accepted
+ */
+ protected abstract Set<Object> getAllowedItemIds(
+ DragAndDropEvent dragEvent, Table table,
+ Collection<Object> visibleItemIds);
+
+ }
+
+ /**
+ * Click event fired when clicking on the Table headers. The event includes
+ * a reference the the Table the event originated from, the property id of
+ * the column which header was pressed and details about the mouse event
+ * itself.
+ */
+ public static class HeaderClickEvent extends ClickEvent {
+ public static final Method HEADER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ HEADER_CLICK_METHOD = HeaderClickListener.class
+ .getDeclaredMethod("headerClick",
+ new Class[] { HeaderClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ public HeaderClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Gets the property id of the column which header was pressed
+ *
+ * @return The column propety id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /**
+ * Click event fired when clicking on the Table footers. The event includes
+ * a reference the the Table the event originated from, the property id of
+ * the column which header was pressed and details about the mouse event
+ * itself.
+ */
+ public static class FooterClickEvent extends ClickEvent {
+ public static final Method FOOTER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ FOOTER_CLICK_METHOD = FooterClickListener.class
+ .getDeclaredMethod("footerClick",
+ new Class[] { FooterClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the component
+ * @param propertyId
+ * The propertyId of the column
+ * @param details
+ * The mouse details of the click
+ */
+ public FooterClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Gets the property id of the column which header was pressed
+ *
+ * @return The column propety id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /**
+ * Interface for the listener for column header mouse click events. The
+ * headerClick method is called when the user presses a header column cell.
+ */
+ public interface HeaderClickListener extends Serializable {
+
+ /**
+ * Called when a user clicks a header column cell
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void headerClick(HeaderClickEvent event);
+ }
+
+ /**
+ * Interface for the listener for column footer mouse click events. The
+ * footerClick method is called when the user presses a footer column cell.
+ */
+ public interface FooterClickListener extends Serializable {
+
+ /**
+ * Called when a user clicks a footer column cell
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void footerClick(FooterClickEvent event);
+ }
+
+ /**
+ * Adds a header click listener which handles the click events when the user
+ * clicks on a column header cell in the Table.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ * </p>
+ *
+ * @param listener
+ * The handler which should handle the header click events.
+ */
+ public void addListener(HeaderClickListener listener) {
+ addListener(VScrollTable.HEADER_CLICK_EVENT_ID, HeaderClickEvent.class,
+ listener, HeaderClickEvent.HEADER_CLICK_METHOD);
+ }
+
+ /**
+ * Removes a header click listener
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeListener(HeaderClickListener listener) {
+ removeListener(VScrollTable.HEADER_CLICK_EVENT_ID,
+ HeaderClickEvent.class, listener);
+ }
+
+ /**
+ * Adds a footer click listener which handles the click events when the user
+ * clicks on a column footer cell in the Table.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ * </p>
+ *
+ * @param listener
+ * The handler which should handle the footer click events.
+ */
+ public void addListener(FooterClickListener listener) {
+ addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, FooterClickEvent.class,
+ listener, FooterClickEvent.FOOTER_CLICK_METHOD);
+ }
+
+ /**
+ * Removes a footer click listener
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeListener(FooterClickListener listener) {
+ removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID,
+ FooterClickEvent.class, listener);
+ }
+
+ /**
+ * Gets the footer caption beneath the rows
+ *
+ * @param propertyId
+ * The propertyId of the column *
+ * @return The caption of the footer or NULL if not set
+ */
+ public String getColumnFooter(Object propertyId) {
+ return columnFooters.get(propertyId);
+ }
+
+ /**
+ * Sets the column footer caption. The column footer caption is the text
+ * displayed beneath the column if footers have been set visible.
+ *
+ * @param propertyId
+ * The properyId of the column
+ *
+ * @param footer
+ * The caption of the footer
+ */
+ public void setColumnFooter(Object propertyId, String footer) {
+ if (footer == null) {
+ columnFooters.remove(propertyId);
+ } else {
+ columnFooters.put(propertyId, footer);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Sets the footer visible in the bottom of the table.
+ * <p>
+ * The footer can be used to add column related data like sums to the bottom
+ * of the Table using setColumnFooter(Object propertyId, String footer).
+ * </p>
+ *
+ * @param visible
+ * Should the footer be visible
+ */
+ public void setFooterVisible(boolean visible) {
+ if (visible != columnFootersVisible) {
+ columnFootersVisible = visible;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Is the footer currently visible?
+ *
+ * @return Returns true if visible else false
+ */
+ public boolean isFooterVisible() {
+ return columnFootersVisible;
+ }
+
+ /**
+ * This event is fired when a column is resized. The event contains the
+ * columns property id which was fired, the previous width of the column and
+ * the width of the column after the resize.
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+ public static final Method COLUMN_RESIZE_METHOD;
+
+ static {
+ try {
+ COLUMN_RESIZE_METHOD = ColumnResizeListener.class
+ .getDeclaredMethod("columnResize",
+ new Class[] { ColumnResizeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ private final int previousWidth;
+ private final int currentWidth;
+ private final Object columnPropertyId;
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the event
+ * @param propertyId
+ * The columns property id
+ * @param previous
+ * The width in pixels of the column before the resize event
+ * @param current
+ * The width in pixels of the column after the resize event
+ */
+ public ColumnResizeEvent(Component source, Object propertyId,
+ int previous, int current) {
+ super(source);
+ previousWidth = previous;
+ currentWidth = current;
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Get the column property id of the column that was resized.
+ *
+ * @return The column property id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+
+ /**
+ * Get the width in pixels of the column before the resize event
+ *
+ * @return Width in pixels
+ */
+ public int getPreviousWidth() {
+ return previousWidth;
+ }
+
+ /**
+ * Get the width in pixels of the column after the resize event
+ *
+ * @return Width in pixels
+ */
+ public int getCurrentWidth() {
+ return currentWidth;
+ }
+ }
+
+ /**
+ * Interface for listening to column resize events.
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /**
+ * This method is triggered when the column has been resized
+ *
+ * @param event
+ * The event which contains the column property id, the
+ * previous width of the column and the current width of the
+ * column
+ */
+ public void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * Adds a column resize listener to the Table. A column resize listener is
+ * called when a user resizes a columns width.
+ *
+ * @param listener
+ * The listener to attach to the Table
+ */
+ public void addListener(ColumnResizeListener listener) {
+ addListener(VScrollTable.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener,
+ ColumnResizeEvent.COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Removes a column resize listener from the Table.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeListener(ColumnResizeListener listener) {
+ removeListener(VScrollTable.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener);
+ }
+
+ /**
+ * This event is fired when a columns are reordered by the end user user.
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+ public static final Method METHOD;
+
+ static {
+ try {
+ METHOD = ColumnReorderListener.class.getDeclaredMethod(
+ "columnReorder",
+ new Class[] { ColumnReorderEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the event
+ */
+ public ColumnReorderEvent(Component source) {
+ super(source);
+ }
+
+ }
+
+ /**
+ * Interface for listening to column reorder events.
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /**
+ * This method is triggered when the column has been reordered
+ *
+ * @param event
+ */
+ public void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * Adds a column reorder listener to the Table. A column reorder listener is
+ * called when a user reorders columns.
+ *
+ * @param listener
+ * The listener to attach to the Table
+ */
+ public void addListener(ColumnReorderListener listener) {
+ addListener(VScrollTable.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD);
+ }
+
+ /**
+ * Removes a column reorder listener from the Table.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeListener(ColumnReorderListener listener) {
+ removeListener(VScrollTable.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener);
+ }
+
+ /**
+ * Set the item description generator which generates tooltips for cells and
+ * rows in the Table
+ *
+ * @param generator
+ * The generator to use or null to disable
+ */
+ public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) {
+ if (generator != itemDescriptionGenerator) {
+ itemDescriptionGenerator = generator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the descriptions
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Get the item description generator which generates tooltips for cells and
+ * rows in the Table.
+ */
+ public ItemDescriptionGenerator getItemDescriptionGenerator() {
+ return itemDescriptionGenerator;
+ }
+
+ /**
+ * Row generators can be used to replace certain items in a table with a
+ * generated string. The generator is called each time the table is
+ * rendered, which means that new strings can be generated each time.
+ *
+ * Row generators can be used for e.g. summary rows or grouping of items.
+ */
+ public interface RowGenerator extends Serializable {
+ /**
+ * Called for every row that is painted in the Table. Returning a
+ * GeneratedRow object will cause the row to be painted based on the
+ * contents of the GeneratedRow. A generated row is by default styled
+ * similarly to a header or footer row.
+ * <p>
+ * The GeneratedRow data object contains the text that should be
+ * rendered in the row. The itemId in the container thus works only as a
+ * placeholder.
+ * <p>
+ * If GeneratedRow.setSpanColumns(true) is used, there will be one
+ * String spanning all columns (use setText("Spanning text")). Otherwise
+ * you can define one String per visible column.
+ * <p>
+ * If GeneratedRow.setRenderAsHtml(true) is used, the strings can
+ * contain HTML markup, otherwise all strings will be rendered as text
+ * (the default).
+ * <p>
+ * A "v-table-generated-row" CSS class is added to all generated rows.
+ * For custom styling of a generated row you can combine a RowGenerator
+ * with a CellStyleGenerator.
+ * <p>
+ *
+ * @param table
+ * The Table that is being painted
+ * @param itemId
+ * The itemId for the row
+ * @return A GeneratedRow describing how the row should be painted or
+ * null to paint the row with the contents from the container
+ */
+ public GeneratedRow generateRow(Table table, Object itemId);
+ }
+
+ public static class GeneratedRow implements Serializable {
+ private boolean htmlContentAllowed = false;
+ private boolean spanColumns = false;
+ private String[] text = null;
+
+ /**
+ * Creates a new generated row. If only one string is passed in, columns
+ * are automatically spanned.
+ *
+ * @param text
+ */
+ public GeneratedRow(String... text) {
+ setHtmlContentAllowed(false);
+ setSpanColumns(text == null || text.length == 1);
+ setText(text);
+ }
+
+ /**
+ * Pass one String if spanColumns is used, one String for each visible
+ * column otherwise
+ */
+ public void setText(String... text) {
+ if (text == null || (text.length == 1 && text[0] == null)) {
+ text = new String[] { "" };
+ }
+ this.text = text;
+ }
+
+ protected String[] getText() {
+ return text;
+ }
+
+ protected Object getValue() {
+ return getText();
+ }
+
+ protected boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ /**
+ * If set to true, all strings passed to {@link #setText(String...)}
+ * will be rendered as HTML.
+ *
+ * @param htmlContentAllowed
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ }
+
+ protected boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ /**
+ * If set to true, only one string will be rendered, spanning the entire
+ * row.
+ *
+ * @param spanColumns
+ */
+ public void setSpanColumns(boolean spanColumns) {
+ this.spanColumns = spanColumns;
+ }
+ }
+
+ /**
+ * Assigns a row generator to the table. The row generator will be able to
+ * replace rows in the table when it is rendered.
+ *
+ * @param generator
+ * the new row generator
+ */
+ public void setRowGenerator(RowGenerator generator) {
+ rowGenerator = generator;
+ refreshRowCache();
+ }
+
+ /**
+ * @return the current row generator
+ */
+ public RowGenerator getRowGenerator() {
+ return rowGenerator;
+ }
+
+ /**
+ * Sets a converter for a property id.
+ * <p>
+ * The converter is used to format the the data for the given property id
+ * before displaying it in the table.
+ * </p>
+ *
+ * @param propertyId
+ * The propertyId to format using the converter
+ * @param converter
+ * The converter to use for the property id
+ */
+ public void setConverter(Object propertyId, Converter<String, ?> converter) {
+ if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException("PropertyId " + propertyId
+ + " must be in the container");
+ }
+ // FIXME: This check should be here but primitive types like Boolean
+ // formatter for boolean property must be handled
+
+ // if (!converter.getSourceType().isAssignableFrom(getType(propertyId)))
+ // {
+ // throw new IllegalArgumentException("Property type ("
+ // + getType(propertyId)
+ // + ") must match converter source type ("
+ // + converter.getSourceType() + ")");
+ // }
+ propertyValueConverters.put(propertyId,
+ (Converter<String, Object>) converter);
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if there is a converter set explicitly for the given property id.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return true if a converter has been set for the property id, false
+ * otherwise
+ */
+ protected boolean hasConverter(Object propertyId) {
+ return propertyValueConverters.containsKey(propertyId);
+ }
+
+ /**
+ * Returns the converter used to format the given propertyId.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return The converter used to format the propertyId or null if no
+ * converter has been set
+ */
+ public Converter<String, Object> getConverter(Object propertyId) {
+ return propertyValueConverters.get(propertyId);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ // We need to ensure that the rows are sent to the client when the
+ // Table is made visible if it has been rendered as invisible.
+ setRowCacheInvalidated(true);
+ }
+ super.setVisible(visible);
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ return getComponentIterator();
+ }
+
+ @Override
+ public Iterator<Component> getComponentIterator() {
+ if (visibleComponents == null) {
+ Collection<Component> empty = Collections.emptyList();
+ return empty.iterator();
+ }
+
+ return visibleComponents.iterator();
+ }
+
+ @Override
+ public boolean isComponentVisible(Component childComponent) {
+ return true;
+ }
+
+ private final Logger getLogger() {
+ if (logger == null) {
+ logger = Logger.getLogger(Table.class.getName());
+ }
+ return logger;
+ }
+}
diff --git a/server/src/com/vaadin/ui/TableFieldFactory.java b/server/src/com/vaadin/ui/TableFieldFactory.java
new file mode 100644
index 0000000000..6c9a641aa8
--- /dev/null
+++ b/server/src/com/vaadin/ui/TableFieldFactory.java
@@ -0,0 +1,45 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Container;
+
+/**
+ * Factory interface for creating new Field-instances based on Container
+ * (datasource), item id, property id and uiContext (the component responsible
+ * for displaying fields). Currently this interface is used by {@link Table},
+ * but might later be used by some other components for {@link Field}
+ * generation.
+ *
+ * <p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.0
+ * @see FormFieldFactory
+ */
+public interface TableFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the Container, item id, property id and the
+ * component responsible for displaying the field (most commonly
+ * {@link Table}).
+ *
+ * @param container
+ * the Container where the property belongs to.
+ * @param itemId
+ * the item Id.
+ * @param propertyId
+ * the Id of the property.
+ * @param uiContext
+ * the component where the field is presented.
+ * @return A field suitable for editing the specified data or null if the
+ * property should not be editable.
+ */
+ Field<?> createField(Container container, Object itemId, Object propertyId,
+ Component uiContext);
+
+}
diff --git a/server/src/com/vaadin/ui/TextArea.java b/server/src/com/vaadin/ui/TextArea.java
new file mode 100644
index 0000000000..d7837dd33f
--- /dev/null
+++ b/server/src/com/vaadin/ui/TextArea.java
@@ -0,0 +1,121 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.data.Property;
+import com.vaadin.shared.ui.textarea.TextAreaState;
+
+/**
+ * A text field that supports multi line editing.
+ */
+public class TextArea extends AbstractTextField {
+
+ /**
+ * Constructs an empty TextArea.
+ */
+ public TextArea() {
+ setValue("");
+ }
+
+ /**
+ * Constructs an empty TextArea with given caption.
+ *
+ * @param caption
+ * the caption for the field.
+ */
+ public TextArea(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a TextArea with given property data source.
+ *
+ * @param dataSource
+ * the data source for the field
+ */
+ public TextArea(Property dataSource) {
+ this();
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a TextArea with given caption and property data source.
+ *
+ * @param caption
+ * the caption for the field
+ * @param dataSource
+ * the data source for the field
+ */
+ public TextArea(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a TextArea with given caption and value.
+ *
+ * @param caption
+ * the caption for the field
+ * @param value
+ * the value for the field
+ */
+ public TextArea(String caption, String value) {
+ this(caption);
+ setValue(value);
+
+ }
+
+ @Override
+ public TextAreaState getState() {
+ return (TextAreaState) super.getState();
+ }
+
+ /**
+ * Sets the number of rows in the text area.
+ *
+ * @param rows
+ * the number of rows for this text area.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ getState().setRows(rows);
+ requestRepaint();
+ }
+
+ /**
+ * Gets the number of rows in the text area.
+ *
+ * @return number of explicitly set rows.
+ */
+ public int getRows() {
+ return getState().getRows();
+ }
+
+ /**
+ * Sets the text area's word-wrap mode on or off.
+ *
+ * @param wordwrap
+ * the boolean value specifying if the text area should be in
+ * word-wrap mode.
+ */
+ public void setWordwrap(boolean wordwrap) {
+ getState().setWordwrap(wordwrap);
+ requestRepaint();
+ }
+
+ /**
+ * Tests if the text area is in word-wrap mode.
+ *
+ * @return <code>true</code> if the component is in word-wrap mode,
+ * <code>false</code> if not.
+ */
+ public boolean isWordwrap() {
+ return getState().isWordwrap();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/TextField.java b/server/src/com/vaadin/ui/TextField.java
new file mode 100644
index 0000000000..567e9c1c10
--- /dev/null
+++ b/server/src/com/vaadin/ui/TextField.java
@@ -0,0 +1,92 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A text editor component that can be bound to any bindable Property. The text
+ * editor supports both multiline and single line modes, default is one-line
+ * mode.
+ * </p>
+ *
+ * <p>
+ * Since <code>TextField</code> extends <code>AbstractField</code> it implements
+ * the {@link com.vaadin.data.Buffered} interface. A <code>TextField</code> is
+ * in write-through mode by default, so
+ * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)} must be called
+ * to enable buffering.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class TextField extends AbstractTextField {
+
+ /**
+ * Constructs an empty <code>TextField</code> with no caption.
+ */
+ public TextField() {
+ setValue("");
+ }
+
+ /**
+ * Constructs an empty <code>TextField</code> with given caption.
+ *
+ * @param caption
+ * the caption <code>String</code> for the editor.
+ */
+ public TextField(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>TextField</code> that's bound to the specified
+ * <code>Property</code> and has no caption.
+ *
+ * @param dataSource
+ * the Property to be edited with this editor.
+ */
+ public TextField(Property dataSource) {
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a new <code>TextField</code> that's bound to the specified
+ * <code>Property</code> and has the given caption <code>String</code>.
+ *
+ * @param caption
+ * the caption <code>String</code> for the editor.
+ * @param dataSource
+ * the Property to be edited with this editor.
+ */
+ public TextField(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>TextField</code> with the given caption and
+ * initial text contents. The editor constructed this way will not be bound
+ * to a Property unless
+ * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)}
+ * is called to bind it.
+ *
+ * @param caption
+ * the caption <code>String</code> for the editor.
+ * @param value
+ * the initial text content of the editor.
+ */
+ public TextField(String caption, String value) {
+ setValue(value);
+ setCaption(caption);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java
new file mode 100644
index 0000000000..c15975d879
--- /dev/null
+++ b/server/src/com/vaadin/ui/Tree.java
@@ -0,0 +1,1615 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.StringTokenizer;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.terminal.KeyMapper;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.client.ui.tree.TreeConnector;
+import com.vaadin.terminal.gwt.client.ui.tree.VTree;
+import com.vaadin.tools.ReflectTools;
+
+/**
+ * Tree component. A Tree can be used to select an item (or multiple items) from
+ * a hierarchical set of items.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings({ "serial", "deprecation" })
+public class Tree extends AbstractSelect implements Container.Hierarchical,
+ Action.Container, ItemClickNotifier, DragSource, DropTarget {
+
+ /* Private members */
+
+ /**
+ * Set of expanded nodes.
+ */
+ private final HashSet<Object> expanded = new HashSet<Object>();
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Action.Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ * Is the tree selectable on the client side.
+ */
+ private boolean selectable = true;
+
+ /**
+ * Flag to indicate sub-tree loading
+ */
+ private boolean partialUpdate = false;
+
+ /**
+ * Holds a itemId which was recently expanded
+ */
+ private Object expandedItemId;
+
+ /**
+ * a flag which indicates initial paint. After this flag set true partial
+ * updates are allowed.
+ */
+ private boolean initialPaint = true;
+
+ /**
+ * Item tooltip generator
+ */
+ private ItemDescriptionGenerator itemDescriptionGenerator;
+
+ /**
+ * Supported drag modes for Tree.
+ */
+ public enum TreeDragMode {
+ /**
+ * When drag mode is NONE, dragging from Tree is not supported. Browsers
+ * may still support selecting text/icons from Tree which can initiate
+ * HTML 5 style drag and drop operation.
+ */
+ NONE,
+ /**
+ * When drag mode is NODE, users can initiate drag from Tree nodes that
+ * represent {@link Item}s in from the backed {@link Container}.
+ */
+ NODE
+ // , SUBTREE
+ }
+
+ private TreeDragMode dragMode = TreeDragMode.NONE;
+
+ private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ /* Tree constructors */
+
+ /**
+ * Creates a new empty tree.
+ */
+ public Tree() {
+ }
+
+ /**
+ * Creates a new empty tree with caption.
+ *
+ * @param caption
+ */
+ public Tree(String caption) {
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new tree with caption and connect it to a Container.
+ *
+ * @param caption
+ * @param dataSource
+ */
+ public Tree(String caption, Container dataSource) {
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /* Expanding and collapsing */
+
+ /**
+ * Check is an item is expanded
+ *
+ * @param itemId
+ * the item id.
+ * @return true iff the item is expanded.
+ */
+ public boolean isExpanded(Object itemId) {
+ return expanded.contains(itemId);
+ }
+
+ /**
+ * Expands an item.
+ *
+ * @param itemId
+ * the item id.
+ * @return True iff the expand operation succeeded
+ */
+ public boolean expandItem(Object itemId) {
+ boolean success = expandItem(itemId, true);
+ requestRepaint();
+ return success;
+ }
+
+ /**
+ * Expands an item.
+ *
+ * @param itemId
+ * the item id.
+ * @param sendChildTree
+ * flag to indicate if client needs subtree or not (may be
+ * cached)
+ * @return True iff the expand operation succeeded
+ */
+ private boolean expandItem(Object itemId, boolean sendChildTree) {
+
+ // Succeeds if the node is already expanded
+ if (isExpanded(itemId)) {
+ return true;
+ }
+
+ // Nodes that can not have children are not expandable
+ if (!areChildrenAllowed(itemId)) {
+ return false;
+ }
+
+ // Expands
+ expanded.add(itemId);
+
+ expandedItemId = itemId;
+ if (initialPaint) {
+ requestRepaint();
+ } else if (sendChildTree) {
+ requestPartialRepaint();
+ }
+ fireExpandEvent(itemId);
+
+ return true;
+ }
+
+ @Override
+ public void requestRepaint() {
+ super.requestRepaint();
+ partialUpdate = false;
+ }
+
+ private void requestPartialRepaint() {
+ super.requestRepaint();
+ partialUpdate = true;
+ }
+
+ /**
+ * Expands the items recursively
+ *
+ * Expands all the children recursively starting from an item. Operation
+ * succeeds only if all expandable items are expanded.
+ *
+ * @param startItemId
+ * @return True iff the expand operation succeeded
+ */
+ public boolean expandItemsRecursively(Object startItemId) {
+
+ boolean result = true;
+
+ // Initial stack
+ final Stack<Object> todo = new Stack<Object>();
+ todo.add(startItemId);
+
+ // Expands recursively
+ while (!todo.isEmpty()) {
+ final Object id = todo.pop();
+ if (areChildrenAllowed(id) && !expandItem(id, false)) {
+ result = false;
+ }
+ if (hasChildren(id)) {
+ todo.addAll(getChildren(id));
+ }
+ }
+ requestRepaint();
+ return result;
+ }
+
+ /**
+ * Collapses an item.
+ *
+ * @param itemId
+ * the item id.
+ * @return True iff the collapse operation succeeded
+ */
+ public boolean collapseItem(Object itemId) {
+
+ // Succeeds if the node is already collapsed
+ if (!isExpanded(itemId)) {
+ return true;
+ }
+
+ // Collapse
+ expanded.remove(itemId);
+ requestRepaint();
+ fireCollapseEvent(itemId);
+
+ return true;
+ }
+
+ /**
+ * Collapses the items recursively.
+ *
+ * Collapse all the children recursively starting from an item. Operation
+ * succeeds only if all expandable items are collapsed.
+ *
+ * @param startItemId
+ * @return True iff the collapse operation succeeded
+ */
+ public boolean collapseItemsRecursively(Object startItemId) {
+
+ boolean result = true;
+
+ // Initial stack
+ final Stack<Object> todo = new Stack<Object>();
+ todo.add(startItemId);
+
+ // Collapse recursively
+ while (!todo.isEmpty()) {
+ final Object id = todo.pop();
+ if (areChildrenAllowed(id) && !collapseItem(id)) {
+ result = false;
+ }
+ if (hasChildren(id)) {
+ todo.addAll(getChildren(id));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the current selectable state. Selectable determines if the a node
+ * can be selected on the client side. Selectable does not affect
+ * {@link #setValue(Object)} or {@link #select(Object)}.
+ *
+ * <p>
+ * The tree is selectable by default.
+ * </p>
+ *
+ * @return the current selectable state.
+ */
+ public boolean isSelectable() {
+ return selectable;
+ }
+
+ /**
+ * Sets the selectable state. Selectable determines if the a node can be
+ * selected on the client side. Selectable does not affect
+ * {@link #setValue(Object)} or {@link #select(Object)}.
+ *
+ * <p>
+ * The tree is selectable by default.
+ * </p>
+ *
+ * @param selectable
+ * The new selectable state.
+ */
+ public void setSelectable(boolean selectable) {
+ if (this.selectable != selectable) {
+ this.selectable = selectable;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Sets the behavior of the multiselect mode
+ *
+ * @param mode
+ * The mode to set
+ */
+ public void setMultiselectMode(MultiSelectMode mode) {
+ if (multiSelectMode != mode && mode != null) {
+ multiSelectMode = mode;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Returns the mode the multiselect is in. The mode controls how
+ * multiselection can be done.
+ *
+ * @return The mode
+ */
+ public MultiSelectMode getMultiselectMode() {
+ return multiSelectMode;
+ }
+
+ /* Component API */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractSelect#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ if (variables.containsKey("clickedKey")) {
+ String key = (String) variables.get("clickedKey");
+
+ Object id = itemIdMapper.get(key);
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("clickEvent"));
+ Item item = getItem(id);
+ if (item != null) {
+ fireEvent(new ItemClickEvent(this, item, id, null, details));
+ }
+ }
+
+ if (!isSelectable() && variables.containsKey("selected")) {
+ // Not-selectable is a special case, AbstractSelect does not support
+ // TODO could be optimized.
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ // Collapses the nodes
+ if (variables.containsKey("collapse")) {
+ final String[] keys = (String[]) variables.get("collapse");
+ for (int i = 0; i < keys.length; i++) {
+ final Object id = itemIdMapper.get(keys[i]);
+ if (id != null && isExpanded(id)) {
+ expanded.remove(id);
+ fireCollapseEvent(id);
+ }
+ }
+ }
+
+ // Expands the nodes
+ if (variables.containsKey("expand")) {
+ boolean sendChildTree = false;
+ if (variables.containsKey("requestChildTree")) {
+ sendChildTree = true;
+ }
+ final String[] keys = (String[]) variables.get("expand");
+ for (int i = 0; i < keys.length; i++) {
+ final Object id = itemIdMapper.get(keys[i]);
+ if (id != null) {
+ expandItem(id, sendChildTree);
+ }
+ }
+ }
+
+ // AbstractSelect cannot handle multiselection so we handle
+ // it ourself
+ if (variables.containsKey("selected") && isMultiSelect()
+ && multiSelectMode == MultiSelectMode.DEFAULT) {
+ handleSelectedItems(variables);
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ // Selections are handled by the select component
+ super.changeVariables(source, variables);
+
+ // Actions
+ if (variables.containsKey("action")) {
+ final StringTokenizer st = new StringTokenizer(
+ (String) variables.get("action"), ",");
+ if (st.countTokens() == 2) {
+ final Object itemId = itemIdMapper.get(st.nextToken());
+ final Action action = actionMapper.get(st.nextToken());
+ if (action != null && (itemId == null || containsId(itemId))
+ && actionHandlers != null) {
+ for (Handler ah : actionHandlers) {
+ ah.handleAction(action, this, itemId);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles the selection
+ *
+ * @param variables
+ * The variables sent to the server from the client
+ */
+ private void handleSelectedItems(Map<String, Object> variables) {
+ final String[] ka = (String[]) variables.get("selected");
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> s = new LinkedList<Object>();
+ for (int i = 0; i < ka.length; i++) {
+ final Object id = itemIdMapper.get(ka[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ requestRepaint();
+ } else if (id != null && containsId(id)) {
+ s.add(id);
+ }
+ }
+
+ if (!isNullSelectionAllowed() && s.size() < 1) {
+ // empty selection not allowed, keep old value
+ requestRepaint();
+ return;
+ }
+
+ setValue(s, true);
+ }
+
+ /**
+ * Paints any needed component-specific things to the given UIDL stream.
+ *
+ * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget)
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ initialPaint = false;
+
+ if (partialUpdate) {
+ target.addAttribute("partialUpdate", true);
+ target.addAttribute("rootKey", itemIdMapper.key(expandedItemId));
+ } else {
+ getCaptionChangeListener().clear();
+
+ // The tab ordering number
+ if (getTabIndex() > 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+
+ // Paint tree attributes
+ if (isSelectable()) {
+ target.addAttribute("selectmode", (isMultiSelect() ? "multi"
+ : "single"));
+ if (isMultiSelect()) {
+ target.addAttribute("multiselectmode",
+ multiSelectMode.ordinal());
+ }
+ } else {
+ target.addAttribute("selectmode", "none");
+ }
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ }
+
+ if (dragMode != TreeDragMode.NONE) {
+ target.addAttribute("dragMode", dragMode.ordinal());
+ }
+
+ }
+
+ // Initialize variables
+ final Set<Action> actionSet = new LinkedHashSet<Action>();
+
+ // rendered selectedKeys
+ LinkedList<String> selectedKeys = new LinkedList<String>();
+
+ final LinkedList<String> expandedKeys = new LinkedList<String>();
+
+ // Iterates through hierarchical tree using a stack of iterators
+ final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>();
+ Collection<?> ids;
+ if (partialUpdate) {
+ ids = getChildren(expandedItemId);
+ } else {
+ ids = rootItemIds();
+ }
+
+ if (ids != null) {
+ iteratorStack.push(ids.iterator());
+ }
+
+ /*
+ * Body actions - Actions which has the target null and can be invoked
+ * by right clicking on the Tree body
+ */
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+
+ // Getting action for the null item, which in this case
+ // means the body item
+ final Action[] aa = ah.getActions(null, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String akey = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(akey);
+ }
+ }
+ }
+ target.addAttribute("alb", keys.toArray());
+ }
+
+ while (!iteratorStack.isEmpty()) {
+
+ // Gets the iterator for current tree level
+ final Iterator<?> i = iteratorStack.peek();
+
+ // If the level is finished, back to previous tree level
+ if (!i.hasNext()) {
+
+ // Removes used iterator from the stack
+ iteratorStack.pop();
+
+ // Closes node
+ if (!iteratorStack.isEmpty()) {
+ target.endTag("node");
+ }
+ }
+
+ // Adds the item on current level
+ else {
+ final Object itemId = i.next();
+
+ // Starts the item / node
+ final boolean isNode = areChildrenAllowed(itemId);
+ if (isNode) {
+ target.startTag("node");
+ } else {
+ target.startTag("leaf");
+ }
+
+ if (itemStyleGenerator != null) {
+ String stylename = itemStyleGenerator.getStyle(itemId);
+ if (stylename != null) {
+ target.addAttribute(TreeConnector.ATTRIBUTE_NODE_STYLE,
+ stylename);
+ }
+ }
+
+ if (itemDescriptionGenerator != null) {
+ String description = itemDescriptionGenerator
+ .generateDescription(this, itemId, null);
+ if (description != null && !description.equals("")) {
+ target.addAttribute("descr", description);
+ }
+ }
+
+ // Adds the attributes
+ target.addAttribute(TreeConnector.ATTRIBUTE_NODE_CAPTION,
+ getItemCaption(itemId));
+ final Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ target.addAttribute(TreeConnector.ATTRIBUTE_NODE_ICON,
+ getItemIcon(itemId));
+ }
+ final String key = itemIdMapper.key(itemId);
+ target.addAttribute("key", key);
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ selectedKeys.add(key);
+ }
+ if (areChildrenAllowed(itemId) && isExpanded(itemId)) {
+ target.addAttribute("expanded", true);
+ expandedKeys.add(key);
+ }
+
+ // Add caption change listener
+ getCaptionChangeListener().addNotifierForItem(itemId);
+
+ // Actions
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ final Iterator<Action.Handler> ahi = actionHandlers
+ .iterator();
+ while (ahi.hasNext()) {
+ final Action[] aa = ahi.next().getActions(itemId, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String akey = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(akey);
+ }
+ }
+ }
+ target.addAttribute("al", keys.toArray());
+ }
+
+ // Adds the children if expanded, or close the tag
+ if (isExpanded(itemId) && hasChildren(itemId)
+ && areChildrenAllowed(itemId)) {
+ iteratorStack.push(getChildren(itemId).iterator());
+ } else {
+ if (isNode) {
+ target.endTag("node");
+ } else {
+ target.endTag("leaf");
+ }
+ }
+ }
+ }
+
+ // Actions
+ if (!actionSet.isEmpty()) {
+ target.addVariable(this, "action", "");
+ target.startTag("actions");
+ final Iterator<Action> i = actionSet.iterator();
+ while (i.hasNext()) {
+ final Action a = i.next();
+ target.startTag("action");
+ if (a.getCaption() != null) {
+ target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_CAPTION,
+ a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_ICON,
+ a.getIcon());
+ }
+ target.addAttribute("key", actionMapper.key(a));
+ target.endTag("action");
+ }
+ target.endTag("actions");
+ }
+
+ if (partialUpdate) {
+ partialUpdate = false;
+ } else {
+ // Selected
+ target.addVariable(this, "selected",
+ selectedKeys.toArray(new String[selectedKeys.size()]));
+
+ // Expand and collapse
+ target.addVariable(this, "expand", new String[] {});
+ target.addVariable(this, "collapse", new String[] {});
+
+ // New items
+ target.addVariable(this, "newitem", new String[] {});
+
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+
+ }
+ }
+
+ /* Container.Hierarchical API */
+
+ /**
+ * Tests if the Item with given ID can have any children.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#areChildrenAllowed(Object)
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return ((Container.Hierarchical) items).areChildrenAllowed(itemId);
+ }
+
+ /**
+ * Gets the IDs of all Items that are children of the specified Item.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#getChildren(Object)
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return ((Container.Hierarchical) items).getChildren(itemId);
+ }
+
+ /**
+ * Gets the ID of the parent Item of the specified Item.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#getParent(Object)
+ */
+ @Override
+ public Object getParent(Object itemId) {
+ return ((Container.Hierarchical) items).getParent(itemId);
+ }
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> has child Items.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#hasChildren(Object)
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return ((Container.Hierarchical) items).hasChildren(itemId);
+ }
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> is a root Item.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#isRoot(Object)
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+ return ((Container.Hierarchical) items).isRoot(itemId);
+ }
+
+ /**
+ * Gets the IDs of all Items in the container that don't have a parent.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#rootItemIds()
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+ return ((Container.Hierarchical) items).rootItemIds();
+ }
+
+ /**
+ * Sets the given Item's capability to have children.
+ *
+ * @see com.vaadin.data.Container.Hierarchical#setChildrenAllowed(Object,
+ * boolean)
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) {
+ final boolean success = ((Container.Hierarchical) items)
+ .setChildrenAllowed(itemId, areChildrenAllowed);
+ if (success) {
+ requestRepaint();
+ }
+ return success;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object ,
+ * java.lang.Object)
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+ final boolean success = ((Container.Hierarchical) items).setParent(
+ itemId, newParentId);
+ if (success) {
+ requestRepaint();
+ }
+ return success;
+ }
+
+ /* Overriding select behavior */
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container)
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ // Note: using wrapped IndexedContainer to match constructor (super
+ // creates an IndexedContainer, which is then wrapped).
+ newDataSource = new ContainerHierarchicalWrapper(
+ new IndexedContainer());
+ }
+
+ // Assure that the data source is ordered by making unordered
+ // containers ordered by wrapping them
+ if (Container.Hierarchical.class.isAssignableFrom(newDataSource
+ .getClass())) {
+ super.setContainerDataSource(newDataSource);
+ } else {
+ super.setContainerDataSource(new ContainerHierarchicalWrapper(
+ newDataSource));
+ }
+ }
+
+ /* Expand event and listener */
+
+ /**
+ * Event to fired when a node is expanded. ExapandEvent is fired when a node
+ * is to be expanded. it can me used to dynamically fill the sub-nodes of
+ * the node.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class ExpandEvent extends Component.Event {
+
+ private final Object expandedItemId;
+
+ /**
+ * New instance of options change event
+ *
+ * @param source
+ * the Source of the event.
+ * @param expandedItemId
+ */
+ public ExpandEvent(Component source, Object expandedItemId) {
+ super(source);
+ this.expandedItemId = expandedItemId;
+ }
+
+ /**
+ * Node where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public Object getItemId() {
+ return expandedItemId;
+ }
+ }
+
+ /**
+ * Expand event listener.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface ExpandListener extends Serializable {
+
+ public static final Method EXPAND_METHOD = ReflectTools.findMethod(
+ ExpandListener.class, "nodeExpand", ExpandEvent.class);
+
+ /**
+ * A node has been expanded.
+ *
+ * @param event
+ * the Expand event.
+ */
+ public void nodeExpand(ExpandEvent event);
+ }
+
+ /**
+ * Adds the expand listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(ExpandListener listener) {
+ addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * Removes the expand listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(ExpandListener listener) {
+ removeListener(ExpandEvent.class, listener,
+ ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * Emits the expand event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireExpandEvent(Object itemId) {
+ fireEvent(new ExpandEvent(this, itemId));
+ }
+
+ /* Collapse event */
+
+ /**
+ * Collapse event
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class CollapseEvent extends Component.Event {
+
+ private final Object collapsedItemId;
+
+ /**
+ * New instance of options change event.
+ *
+ * @param source
+ * the Source of the event.
+ * @param collapsedItemId
+ */
+ public CollapseEvent(Component source, Object collapsedItemId) {
+ super(source);
+ this.collapsedItemId = collapsedItemId;
+ }
+
+ /**
+ * Gets tge Collapsed Item id.
+ *
+ * @return the collapsed item id.
+ */
+ public Object getItemId() {
+ return collapsedItemId;
+ }
+ }
+
+ /**
+ * Collapse event listener.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface CollapseListener extends Serializable {
+
+ public static final Method COLLAPSE_METHOD = ReflectTools.findMethod(
+ CollapseListener.class, "nodeCollapse", CollapseEvent.class);
+
+ /**
+ * A node has been collapsed.
+ *
+ * @param event
+ * the Collapse event.
+ */
+ public void nodeCollapse(CollapseEvent event);
+ }
+
+ /**
+ * Adds the collapse listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(CollapseListener listener) {
+ addListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * Removes the collapse listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(CollapseListener listener) {
+ removeListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * Emits collapse event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireCollapseEvent(Object itemId) {
+ fireEvent(new CollapseEvent(this, itemId));
+ }
+
+ /* Action container */
+
+ /**
+ * Adds an action handler.
+ *
+ * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler)
+ */
+ @Override
+ public void addActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Action.Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ requestRepaint();
+ }
+ }
+ }
+
+ /**
+ * Removes an action handler.
+ *
+ * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler)
+ */
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ actionHandlers.remove(actionHandler);
+
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ actionHandlers = null;
+ actionMapper = null;
+ requestRepaint();
+ }
+
+ /**
+ * Gets the visible item ids.
+ *
+ * @see com.vaadin.ui.Select#getVisibleItemIds()
+ */
+ @Override
+ public Collection<?> getVisibleItemIds() {
+
+ final LinkedList<Object> visible = new LinkedList<Object>();
+
+ // Iterates trough hierarchical tree using a stack of iterators
+ final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>();
+ final Collection<?> ids = rootItemIds();
+ if (ids != null) {
+ iteratorStack.push(ids.iterator());
+ }
+ while (!iteratorStack.isEmpty()) {
+
+ // Gets the iterator for current tree level
+ final Iterator<?> i = iteratorStack.peek();
+
+ // If the level is finished, back to previous tree level
+ if (!i.hasNext()) {
+
+ // Removes used iterator from the stack
+ iteratorStack.pop();
+ }
+
+ // Adds the item on current level
+ else {
+ final Object itemId = i.next();
+
+ visible.add(itemId);
+
+ // Adds children if expanded, or close the tag
+ if (isExpanded(itemId) && hasChildren(itemId)) {
+ iteratorStack.push(getChildren(itemId).iterator());
+ }
+ }
+ }
+
+ return visible;
+ }
+
+ /**
+ * Tree does not support <code>setNullSelectionItemId</code>.
+ *
+ * @see com.vaadin.ui.AbstractSelect#setNullSelectionItemId(java.lang.Object)
+ */
+ @Override
+ public void setNullSelectionItemId(Object nullSelectionItemId)
+ throws UnsupportedOperationException {
+ if (nullSelectionItemId != null) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * Adding new items is not supported.
+ *
+ * @throws UnsupportedOperationException
+ * if set to true.
+ * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean)
+ */
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Tree does not support lazy options loading mode. Setting this true will
+ * throw UnsupportedOperationException.
+ *
+ * @see com.vaadin.ui.Select#setLazyLoading(boolean)
+ */
+ public void setLazyLoading(boolean useLazyLoading) {
+ if (useLazyLoading) {
+ throw new UnsupportedOperationException(
+ "Lazy options loading is not supported by Tree.");
+ }
+ }
+
+ private ItemStyleGenerator itemStyleGenerator;
+
+ private DropHandler dropHandler;
+
+ @Override
+ public void addListener(ItemClickListener listener) {
+ addListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener,
+ ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ public void removeListener(ItemClickListener listener) {
+ removeListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ /**
+ * Sets the {@link ItemStyleGenerator} to be used with this tree.
+ *
+ * @param itemStyleGenerator
+ * item style generator or null to remove generator
+ */
+ public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
+ if (this.itemStyleGenerator != itemStyleGenerator) {
+ this.itemStyleGenerator = itemStyleGenerator;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * @return the current {@link ItemStyleGenerator} for this tree. Null if
+ * {@link ItemStyleGenerator} is not set.
+ */
+ public ItemStyleGenerator getItemStyleGenerator() {
+ return itemStyleGenerator;
+ }
+
+ /**
+ * ItemStyleGenerator can be used to add custom styles to tree items. The
+ * CSS class name that will be added to the cell content is
+ * <tt>v-tree-node-[style name]</tt>.
+ */
+ public interface ItemStyleGenerator extends Serializable {
+
+ /**
+ * Called by Tree when an item is painted.
+ *
+ * @param itemId
+ * The itemId of the item to be painted
+ * @return The style name to add to this item. (the CSS class name will
+ * be v-tree-node-[style name]
+ */
+ public abstract String getStyle(Object itemId);
+ }
+
+ // Overriden so javadoc comes from Container.Hierarchical
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ return super.removeItem(itemId);
+ }
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ /**
+ * A {@link TargetDetails} implementation with Tree specific api.
+ *
+ * @since 6.3
+ */
+ public class TreeTargetDetails extends AbstractSelectTargetDetails {
+
+ TreeTargetDetails(Map<String, Object> rawVariables) {
+ super(rawVariables);
+ }
+
+ @Override
+ public Tree getTarget() {
+ return (Tree) super.getTarget();
+ }
+
+ /**
+ * If the event is on a node that can not have children (see
+ * {@link Tree#areChildrenAllowed(Object)}), this method returns the
+ * parent item id of the target item (see {@link #getItemIdOver()} ).
+ * The identifier of the parent node is also returned if the cursor is
+ * on the top part of node. Else this method returns the same as
+ * {@link #getItemIdOver()}.
+ * <p>
+ * In other words this method returns the identifier of the "folder"
+ * into the drag operation is targeted.
+ * <p>
+ * If the method returns null, the current target is on a root node or
+ * on other undefined area over the tree component.
+ * <p>
+ * The default Tree implementation marks the targetted tree node with
+ * CSS classnames v-tree-node-dragfolder and
+ * v-tree-node-caption-dragfolder (for the caption element).
+ */
+ public Object getItemIdInto() {
+
+ Object itemIdOver = getItemIdOver();
+ if (areChildrenAllowed(itemIdOver)
+ && getDropLocation() == VerticalDropLocation.MIDDLE) {
+ return itemIdOver;
+ }
+ return getParent(itemIdOver);
+ }
+
+ /**
+ * If drop is targeted into "folder node" (see {@link #getItemIdInto()}
+ * ), this method returns the item id of the node after the drag was
+ * targeted. This method is useful when implementing drop into specific
+ * location (between specific nodes) in tree.
+ *
+ * @return the id of the item after the user targets the drop or null if
+ * "target" is a first item in node list (or the first in root
+ * node list)
+ */
+ public Object getItemIdAfter() {
+ Object itemIdOver = getItemIdOver();
+ Object itemIdInto2 = getItemIdInto();
+ if (itemIdOver.equals(itemIdInto2)) {
+ return null;
+ }
+ VerticalDropLocation dropLocation = getDropLocation();
+ if (VerticalDropLocation.TOP == dropLocation) {
+ // if on top of the caption area, add before
+ Collection<?> children;
+ Object itemIdInto = getItemIdInto();
+ if (itemIdInto != null) {
+ // seek the previous from child list
+ children = getChildren(itemIdInto);
+ } else {
+ children = rootItemIds();
+ }
+ Object ref = null;
+ for (Object object : children) {
+ if (object.equals(itemIdOver)) {
+ return ref;
+ }
+ ref = object;
+ }
+ }
+ return itemIdOver;
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map)
+ */
+ @Override
+ public TreeTargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new TreeTargetDetails(clientVariables);
+ }
+
+ /**
+ * Helper API for {@link TreeDropCriterion}
+ *
+ * @param itemId
+ * @return
+ */
+ private String key(Object itemId) {
+ return itemIdMapper.key(itemId);
+ }
+
+ /**
+ * Sets the drag mode that controls how Tree behaves as a {@link DragSource}
+ * .
+ *
+ * @param dragMode
+ */
+ public void setDragMode(TreeDragMode dragMode) {
+ this.dragMode = dragMode;
+ requestRepaint();
+ }
+
+ /**
+ * @return the drag mode that controls how Tree behaves as a
+ * {@link DragSource}.
+ *
+ * @see TreeDragMode
+ */
+ public TreeDragMode getDragMode() {
+ return dragMode;
+ }
+
+ /**
+ * Concrete implementation of {@link DataBoundTransferable} for data
+ * transferred from a tree.
+ *
+ * @see {@link DataBoundTransferable}.
+ *
+ * @since 6.3
+ */
+ protected class TreeTransferable extends DataBoundTransferable {
+
+ public TreeTransferable(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ super(sourceComponent, rawVariables);
+ }
+
+ @Override
+ public Object getItemId() {
+ return getData("itemId");
+ }
+
+ @Override
+ public Object getPropertyId() {
+ return getItemCaptionPropertyId();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map)
+ */
+ @Override
+ public Transferable getTransferable(Map<String, Object> payload) {
+ TreeTransferable transferable = new TreeTransferable(this, payload);
+ // updating drag source variables
+ Object object = payload.get("itemId");
+ if (object != null) {
+ transferable.setData("itemId", itemIdMapper.get((String) object));
+ }
+
+ return transferable;
+ }
+
+ /**
+ * Lazy loading accept criterion for Tree. Accepted target nodes are loaded
+ * from server once per drag and drop operation. Developer must override one
+ * method that decides accepted tree nodes for the whole Tree.
+ *
+ * <p>
+ * Initially pretty much no data is sent to client. On first required
+ * criterion check (per drag request) the client side data structure is
+ * initialized from server and no subsequent requests requests are needed
+ * during that drag and drop operation.
+ */
+ public static abstract class TreeDropCriterion extends ServerSideCriterion {
+
+ private Tree tree;
+
+ private Set<Object> allowedItemIds;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.ServerSideCriterion#getIdentifier
+ * ()
+ */
+ @Override
+ protected String getIdentifier() {
+ return TreeDropCriterion.class.getCanonicalName();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#accepts(com.vaadin
+ * .event.dd.DragAndDropEvent)
+ */
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ tree = (Tree) dragEvent.getTargetDetails().getTarget();
+ allowedItemIds = getAllowedItemIds(dragEvent, tree);
+
+ return allowedItemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#paintResponse(
+ * com.vaadin.terminal.PaintTarget)
+ */
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ /*
+ * send allowed nodes to client so subsequent requests can be
+ * avoided
+ */
+ Object[] array = allowedItemIds.toArray();
+ for (int i = 0; i < array.length; i++) {
+ String key = tree.key(array[i]);
+ array[i] = key;
+ }
+ target.addAttribute("allowedIds", array);
+ }
+
+ protected abstract Set<Object> getAllowedItemIds(
+ DragAndDropEvent dragEvent, Tree tree);
+
+ }
+
+ /**
+ * A criterion that accepts {@link Transferable} only directly on a tree
+ * node that can have children.
+ * <p>
+ * Class is singleton, use {@link TargetItemAllowsChildren#get()} to get the
+ * instance.
+ *
+ * @see Tree#setChildrenAllowed(Object, boolean)
+ *
+ * @since 6.3
+ */
+ public static class TargetItemAllowsChildren extends TargetDetailIs {
+
+ private static TargetItemAllowsChildren instance = new TargetItemAllowsChildren();
+
+ public static TargetItemAllowsChildren get() {
+ return instance;
+ }
+
+ private TargetItemAllowsChildren() {
+ super("itemIdOverIsNode", Boolean.TRUE);
+ }
+
+ /*
+ * Uses enhanced server side check
+ */
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ try {
+ // must be over tree node and in the middle of it (not top or
+ // bottom
+ // part)
+ TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent
+ .getTargetDetails();
+
+ Object itemIdOver = eventDetails.getItemIdOver();
+ if (!eventDetails.getTarget().areChildrenAllowed(itemIdOver)) {
+ return false;
+ }
+ // return true if directly over
+ return eventDetails.getDropLocation() == VerticalDropLocation.MIDDLE;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ }
+
+ /**
+ * An accept criterion that checks the parent node (or parent hierarchy) for
+ * the item identifier given in constructor. If the parent is found, content
+ * is accepted. Criterion can be used to accepts drags on a specific sub
+ * tree only.
+ * <p>
+ * The root items is also consider to be valid target.
+ */
+ public class TargetInSubtree extends ClientSideCriterion {
+
+ private Object rootId;
+ private int depthToCheck = -1;
+
+ /**
+ * Constructs a criteria that accepts the drag if the targeted Item is a
+ * descendant of Item identified by given id
+ *
+ * @param parentItemId
+ * the item identifier of the parent node
+ */
+ public TargetInSubtree(Object parentItemId) {
+ rootId = parentItemId;
+ }
+
+ /**
+ * Constructs a criteria that accepts drops within given level below the
+ * subtree root identified by given id.
+ *
+ * @param rootId
+ * the item identifier to be sought for
+ * @param depthToCheck
+ * the depth that tree is traversed upwards to seek for the
+ * parent, -1 means that the whole structure should be
+ * checked
+ */
+ public TargetInSubtree(Object rootId, int depthToCheck) {
+ this.rootId = rootId;
+ this.depthToCheck = depthToCheck;
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ try {
+ TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent
+ .getTargetDetails();
+
+ if (eventDetails.getItemIdOver() != null) {
+ Object itemId = eventDetails.getItemIdOver();
+ int i = 0;
+ while (itemId != null
+ && (depthToCheck == -1 || i <= depthToCheck)) {
+ if (itemId.equals(rootId)) {
+ return true;
+ }
+ itemId = getParent(itemId);
+ i++;
+ }
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ target.addAttribute("depth", depthToCheck);
+ target.addAttribute("key", key(rootId));
+ }
+
+ }
+
+ /**
+ * Set the item description generator which generates tooltips for the tree
+ * items
+ *
+ * @param generator
+ * The generator to use or null to disable
+ */
+ public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) {
+ if (generator != itemDescriptionGenerator) {
+ itemDescriptionGenerator = generator;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Get the item description generator which generates tooltips for tree
+ * items
+ */
+ public ItemDescriptionGenerator getItemDescriptionGenerator() {
+ return itemDescriptionGenerator;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/TreeTable.java b/server/src/com/vaadin/ui/TreeTable.java
new file mode 100644
index 0000000000..6132b652f7
--- /dev/null
+++ b/server/src/com/vaadin/ui/TreeTable.java
@@ -0,0 +1,824 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.vaadin.data.Collapsible;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.data.util.HierarchicalContainer;
+import com.vaadin.data.util.HierarchicalContainerOrderedWrapper;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.client.ui.treetable.TreeTableConnector;
+import com.vaadin.ui.Tree.CollapseEvent;
+import com.vaadin.ui.Tree.CollapseListener;
+import com.vaadin.ui.Tree.ExpandEvent;
+import com.vaadin.ui.Tree.ExpandListener;
+
+/**
+ * TreeTable extends the {@link Table} component so that it can also visualize a
+ * hierarchy of its Items in a similar manner that {@link Tree} does. The tree
+ * hierarchy is always displayed in the first actual column of the TreeTable.
+ * <p>
+ * The TreeTable supports the usual {@link Table} features like lazy loading, so
+ * it should be no problem to display lots of items at once. Only required rows
+ * and some cache rows are sent to the client.
+ * <p>
+ * TreeTable supports standard {@link Hierarchical} container interfaces, but
+ * also a more fine tuned version - {@link Collapsible}. A container
+ * implementing the {@link Collapsible} interface stores the collapsed/expanded
+ * state internally and can this way scale better on the server side than with
+ * standard Hierarchical implementations. Developer must however note that
+ * {@link Collapsible} containers can not be shared among several users as they
+ * share UI state in the container.
+ */
+@SuppressWarnings({ "serial" })
+public class TreeTable extends Table implements Hierarchical {
+
+ private interface ContainerStrategy extends Serializable {
+ public int size();
+
+ public boolean isNodeOpen(Object itemId);
+
+ public int getDepth(Object itemId);
+
+ public void toggleChildVisibility(Object itemId);
+
+ public Object getIdByIndex(int index);
+
+ public int indexOfId(Object id);
+
+ public Object nextItemId(Object itemId);
+
+ public Object lastItemId();
+
+ public Object prevItemId(Object itemId);
+
+ public boolean isLastId(Object itemId);
+
+ public Collection<?> getItemIds();
+
+ public void containerItemSetChange(ItemSetChangeEvent event);
+ }
+
+ private abstract class AbstractStrategy implements ContainerStrategy {
+
+ /**
+ * Consider adding getDepth to {@link Collapsible}, might help
+ * scalability with some container implementations.
+ */
+
+ @Override
+ public int getDepth(Object itemId) {
+ int depth = 0;
+ Hierarchical hierarchicalContainer = getContainerDataSource();
+ while (!hierarchicalContainer.isRoot(itemId)) {
+ depth++;
+ itemId = hierarchicalContainer.getParent(itemId);
+ }
+ return depth;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ }
+
+ }
+
+ /**
+ * This strategy is used if current container implements {@link Collapsible}
+ * .
+ *
+ * open-collapsed logic diverted to container, otherwise use default
+ * implementations.
+ */
+ private class CollapsibleStrategy extends AbstractStrategy {
+
+ private Collapsible c() {
+ return (Collapsible) getContainerDataSource();
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ c().setCollapsed(itemId, !c().isCollapsed(itemId));
+ }
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return !c().isCollapsed(itemId);
+ }
+
+ @Override
+ public int size() {
+ return TreeTable.super.size();
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return TreeTable.super.getIdByIndex(index);
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return TreeTable.super.indexOfId(id);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ // using the default impl
+ return TreeTable.super.isLastId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ // using the default impl
+ return TreeTable.super.lastItemId();
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return TreeTable.super.nextItemId(itemId);
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return TreeTable.super.prevItemId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return TreeTable.super.getItemIds();
+ }
+
+ }
+
+ /**
+ * Strategy for Hierarchical but not Collapsible container like
+ * {@link HierarchicalContainer}.
+ *
+ * Store collapsed/open states internally, fool Table to use preorder when
+ * accessing items from container via Ordered/Indexed methods.
+ */
+ private class HierarchicalStrategy extends AbstractStrategy {
+
+ private final HashSet<Object> openItems = new HashSet<Object>();
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return openItems.contains(itemId);
+ }
+
+ @Override
+ public int size() {
+ return getPreOrder().size();
+ }
+
+ @Override
+ public Collection<Object> getItemIds() {
+ return Collections.unmodifiableCollection(getPreOrder());
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ return itemId.equals(lastItemId());
+ }
+
+ @Override
+ public Object lastItemId() {
+ if (getPreOrder().size() > 0) {
+ return getPreOrder().get(getPreOrder().size() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ if (indexOf == -1) {
+ return null;
+ }
+ indexOf++;
+ if (indexOf == getPreOrder().size()) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ indexOf--;
+ if (indexOf < 0) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ boolean removed = openItems.remove(itemId);
+ if (!removed) {
+ openItems.add(itemId);
+ getLogger().finest("Item " + itemId + " is now expanded");
+ } else {
+ getLogger().finest("Item " + itemId + " is now collapsed");
+ }
+ clearPreorderCache();
+ }
+
+ private void clearPreorderCache() {
+ preOrder = null; // clear preorder cache
+ }
+
+ List<Object> preOrder;
+
+ /**
+ * Preorder of ids currently visible
+ *
+ * @return
+ */
+ private List<Object> getPreOrder() {
+ if (preOrder == null) {
+ preOrder = new ArrayList<Object>();
+ Collection<?> rootItemIds = getContainerDataSource()
+ .rootItemIds();
+ for (Object id : rootItemIds) {
+ preOrder.add(id);
+ addVisibleChildTree(id);
+ }
+ }
+ return preOrder;
+ }
+
+ private void addVisibleChildTree(Object id) {
+ if (isNodeOpen(id)) {
+ Collection<?> children = getContainerDataSource().getChildren(
+ id);
+ if (children != null) {
+ for (Object childId : children) {
+ preOrder.add(childId);
+ addVisibleChildTree(childId);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return getPreOrder().indexOf(id);
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return getPreOrder().get(index);
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ // preorder becomes invalid on sort, item additions etc.
+ clearPreorderCache();
+ super.containerItemSetChange(event);
+ }
+
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ */
+ public TreeTable() {
+ super(null, new HierarchicalContainer());
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ *
+ * @param caption
+ * the caption for the TreeTable
+ */
+ public TreeTable(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a TreeTable instance with given captions and data source.
+ *
+ * @param caption
+ * the caption for the component
+ * @param dataSource
+ * the dataSource that is used to list items in the component
+ */
+ public TreeTable(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ private ContainerStrategy cStrategy;
+ private Object focusedRowId = null;
+ private Object hierarchyColumnId;
+
+ /**
+ * The item id that was expanded or collapsed during this request. Reset at
+ * the end of paint and only used for determining if a partial or full paint
+ * should be done.
+ *
+ * Can safely be reset to null whenever a change occurs that would prevent a
+ * partial update from rendering the correct result, e.g. rows added or
+ * removed during an expand operation.
+ */
+ private Object toggledItemId;
+ private boolean animationsEnabled;
+ private boolean clearFocusedRowPending;
+
+ /**
+ * If the container does not send item set change events, always do a full
+ * repaint instead of a partial update when expanding/collapsing nodes.
+ */
+ private boolean containerSupportsPartialUpdates;
+
+ private ContainerStrategy getContainerStrategy() {
+ if (cStrategy == null) {
+ if (getContainerDataSource() instanceof Collapsible) {
+ cStrategy = new CollapsibleStrategy();
+ } else {
+ cStrategy = new HierarchicalStrategy();
+ }
+ }
+ return cStrategy;
+ }
+
+ @Override
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+ super.paintRowAttributes(target, itemId);
+ target.addAttribute("depth", getContainerStrategy().getDepth(itemId));
+ if (getContainerDataSource().areChildrenAllowed(itemId)) {
+ target.addAttribute("ca", true);
+ target.addAttribute("open",
+ getContainerStrategy().isNodeOpen(itemId));
+ }
+ }
+
+ @Override
+ protected void paintRowIcon(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ // always paint if present (in parent only if row headers visible)
+ if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) {
+ Resource itemIcon = getItemIcon(cells[CELL_ITEMID][indexInRowbuffer]);
+ if (itemIcon != null) {
+ target.addAttribute("icon", itemIcon);
+ }
+ } else if (cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ super.changeVariables(source, variables);
+
+ if (variables.containsKey("toggleCollapsed")) {
+ String object = (String) variables.get("toggleCollapsed");
+ Object itemId = itemIdMapper.get(object);
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ if (variables.containsKey("selectCollapsed")) {
+ // ensure collapsed is selected unless opened with selection
+ // head
+ if (isSelectable()) {
+ select(itemId);
+ }
+ }
+ } else if (variables.containsKey("focusParent")) {
+ String key = (String) variables.get("focusParent");
+ Object refId = itemIdMapper.get(key);
+ Object itemId = getParent(refId);
+ focusParent(itemId);
+ }
+ }
+
+ private void focusParent(Object itemId) {
+ boolean inView = false;
+ Object inPageId = getCurrentPageFirstItemId();
+ for (int i = 0; inPageId != null && i < getPageLength(); i++) {
+ if (inPageId.equals(itemId)) {
+ inView = true;
+ break;
+ }
+ inPageId = nextItemId(inPageId);
+ i++;
+ }
+ if (!inView) {
+ setCurrentPageFirstItemId(itemId);
+ }
+ // Select the row if it is selectable.
+ if (isSelectable()) {
+ if (isMultiSelect()) {
+ setValue(Collections.singleton(itemId));
+ } else {
+ setValue(itemId);
+ }
+ }
+ setFocusedRow(itemId);
+ }
+
+ private void setFocusedRow(Object itemId) {
+ focusedRowId = itemId;
+ if (focusedRowId == null) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ clearFocusedRowPending = true;
+ }
+ requestRepaint();
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (focusedRowId != null) {
+ target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId));
+ focusedRowId = null;
+ } else if (clearFocusedRowPending) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ target.addAttribute("clearFocusPending", true);
+ clearFocusedRowPending = false;
+ }
+ target.addAttribute("animate", animationsEnabled);
+ if (hierarchyColumnId != null) {
+ Object[] visibleColumns2 = getVisibleColumns();
+ for (int i = 0; i < visibleColumns2.length; i++) {
+ Object object = visibleColumns2[i];
+ if (hierarchyColumnId.equals(object)) {
+ target.addAttribute(
+ TreeTableConnector.ATTRIBUTE_HIERARCHY_COLUMN_INDEX,
+ i);
+ break;
+ }
+ }
+ }
+ super.paintContent(target);
+ toggledItemId = null;
+ }
+
+ /*
+ * Override methods for partial row updates and additions when expanding /
+ * collapsing nodes.
+ */
+
+ @Override
+ protected boolean isPartialRowUpdate() {
+ return toggledItemId != null && containerSupportsPartialUpdates
+ && !isRowCacheInvalidated();
+ }
+
+ @Override
+ protected int getFirstAddedItemIndex() {
+ return indexOfId(toggledItemId) + 1;
+ }
+
+ @Override
+ protected int getAddedRowCount() {
+ return countSubNodesRecursively(getContainerDataSource(), toggledItemId);
+ }
+
+ private int countSubNodesRecursively(Hierarchical hc, Object itemId) {
+ int count = 0;
+ // we need the number of children for toggledItemId no matter if its
+ // collapsed or expanded. Other items' children are only counted if the
+ // item is expanded.
+ if (getContainerStrategy().isNodeOpen(itemId)
+ || itemId == toggledItemId) {
+ Collection<?> children = hc.getChildren(itemId);
+ if (children != null) {
+ count += children != null ? children.size() : 0;
+ for (Object id : children) {
+ count += countSubNodesRecursively(hc, id);
+ }
+ }
+ }
+ return count;
+ }
+
+ @Override
+ protected int getFirstUpdatedItemIndex() {
+ return indexOfId(toggledItemId);
+ }
+
+ @Override
+ protected int getUpdatedRowCount() {
+ return 1;
+ }
+
+ @Override
+ protected boolean shouldHideAddedRows() {
+ return !getContainerStrategy().isNodeOpen(toggledItemId);
+ }
+
+ private void toggleChildVisibility(Object itemId, boolean forceFullRefresh) {
+ getContainerStrategy().toggleChildVisibility(itemId);
+ // ensure that page still has first item in page, DON'T clear the
+ // caches.
+ setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false);
+
+ if (isCollapsed(itemId)) {
+ fireCollapseEvent(itemId);
+ } else {
+ fireExpandEvent(itemId);
+ }
+
+ if (containerSupportsPartialUpdates && !forceFullRefresh) {
+ requestRepaint();
+ } else {
+ // For containers that do not send item set change events, always do
+ // full repaint instead of partial row update.
+ refreshRowCache();
+ }
+ }
+
+ @Override
+ public int size() {
+ return getContainerStrategy().size();
+ }
+
+ @Override
+ public Hierarchical getContainerDataSource() {
+ return (Hierarchical) super.getContainerDataSource();
+ }
+
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ cStrategy = null;
+
+ // FIXME: This disables partial updates until TreeTable is fixed so it
+ // does not change component hierarchy during paint
+ containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier) && false;
+
+ if (!(newDataSource instanceof Hierarchical)) {
+ newDataSource = new ContainerHierarchicalWrapper(newDataSource);
+ }
+
+ if (!(newDataSource instanceof Ordered)) {
+ newDataSource = new HierarchicalContainerOrderedWrapper(
+ (Hierarchical) newDataSource);
+ }
+
+ super.setContainerDataSource(newDataSource);
+ }
+
+ @Override
+ public void containerItemSetChange(
+ com.vaadin.data.Container.ItemSetChangeEvent event) {
+ // Can't do partial repaints if items are added or removed during the
+ // expand/collapse request
+ toggledItemId = null;
+ getContainerStrategy().containerItemSetChange(event);
+ super.containerItemSetChange(event);
+ }
+
+ @Override
+ protected Object getIdByIndex(int index) {
+ return getContainerStrategy().getIdByIndex(index);
+ }
+
+ @Override
+ protected int indexOfId(Object itemId) {
+ return getContainerStrategy().indexOfId(itemId);
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return getContainerStrategy().nextItemId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ return getContainerStrategy().lastItemId();
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return getContainerStrategy().prevItemId(itemId);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return getContainerStrategy().isLastId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return getContainerStrategy().getItemIds();
+ }
+
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return getContainerDataSource().areChildrenAllowed(itemId);
+ }
+
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return getContainerDataSource().getChildren(itemId);
+ }
+
+ @Override
+ public Object getParent(Object itemId) {
+ return getContainerDataSource().getParent(itemId);
+ }
+
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return getContainerDataSource().hasChildren(itemId);
+ }
+
+ @Override
+ public boolean isRoot(Object itemId) {
+ return getContainerDataSource().isRoot(itemId);
+ }
+
+ @Override
+ public Collection<?> rootItemIds() {
+ return getContainerDataSource().rootItemIds();
+ }
+
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setChildrenAllowed(itemId,
+ areChildrenAllowed);
+ }
+
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setParent(itemId, newParentId);
+ }
+
+ /**
+ * Sets the Item specified by given identifier as collapsed or expanded. If
+ * the Item is collapsed, its children are not displayed to the user.
+ *
+ * @param itemId
+ * the identifier of the Item
+ * @param collapsed
+ * true if the Item should be collapsed, false if expanded
+ */
+ public void setCollapsed(Object itemId, boolean collapsed) {
+ if (isCollapsed(itemId) != collapsed) {
+ if (null == toggledItemId && !isRowCacheInvalidated()
+ && getVisibleItemIds().contains(itemId)) {
+ // optimization: partial refresh if only one item is
+ // collapsed/expanded
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ } else {
+ // make sure a full refresh takes place - otherwise neither
+ // partial nor full repaint of table content is performed
+ toggledItemId = null;
+ toggleChildVisibility(itemId, true);
+ }
+ }
+ }
+
+ /**
+ * Checks if Item with given identifier is collapsed in the UI.
+ *
+ * <p>
+ *
+ * @param itemId
+ * the identifier of the checked Item
+ * @return true if the Item with given id is collapsed
+ * @see Collapsible#isCollapsed(Object)
+ */
+ public boolean isCollapsed(Object itemId) {
+ return !getContainerStrategy().isNodeOpen(itemId);
+ }
+
+ /**
+ * Explicitly sets the column in which the TreeTable visualizes the
+ * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized
+ * in the first visible column.
+ *
+ * @param hierarchyColumnId
+ */
+ public void setHierarchyColumn(Object hierarchyColumnId) {
+ this.hierarchyColumnId = hierarchyColumnId;
+ }
+
+ /**
+ * @return the identifier of column into which the hierarchy will be
+ * visualized or null if the column is not explicitly defined.
+ */
+ public Object getHierarchyColumnId() {
+ return hierarchyColumnId;
+ }
+
+ /**
+ * Adds an expand listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(ExpandListener listener) {
+ addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * Removes an expand listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(ExpandListener listener) {
+ removeListener(ExpandEvent.class, listener,
+ ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * Emits an expand event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireExpandEvent(Object itemId) {
+ fireEvent(new ExpandEvent(this, itemId));
+ }
+
+ /**
+ * Adds a collapse listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(CollapseListener listener) {
+ addListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * Removes a collapse listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(CollapseListener listener) {
+ removeListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * Emits a collapse event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireCollapseEvent(Object itemId) {
+ fireEvent(new CollapseEvent(this, itemId));
+ }
+
+ /**
+ * @return true if animations are enabled
+ */
+ public boolean isAnimationsEnabled() {
+ return animationsEnabled;
+ }
+
+ /**
+ * Animations can be enabled by passing true to this method. Currently
+ * expanding rows slide in from the top and collapsing rows slide out the
+ * same way. NOTE! not supported in Internet Explorer 6 or 7.
+ *
+ * @param animationsEnabled
+ * true or false whether to enable animations or not.
+ */
+ public void setAnimationsEnabled(boolean animationsEnabled) {
+ this.animationsEnabled = animationsEnabled;
+ requestRepaint();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(TreeTable.class.getName());
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/TwinColSelect.java b/server/src/com/vaadin/ui/TwinColSelect.java
new file mode 100644
index 0000000000..5539236f77
--- /dev/null
+++ b/server/src/com/vaadin/ui/TwinColSelect.java
@@ -0,0 +1,180 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.gwt.client.ui.twincolselect.VTwinColSelect;
+
+/**
+ * Multiselect component with two lists: left side for available items and right
+ * side for selected items.
+ */
+@SuppressWarnings("serial")
+public class TwinColSelect extends AbstractSelect {
+
+ private int columns = 0;
+ private int rows = 0;
+
+ private String leftColumnCaption;
+ private String rightColumnCaption;
+
+ /**
+ *
+ */
+ public TwinColSelect() {
+ super();
+ setMultiSelect(true);
+ }
+
+ /**
+ * @param caption
+ */
+ public TwinColSelect(String caption) {
+ super(caption);
+ setMultiSelect(true);
+ }
+
+ /**
+ * @param caption
+ * @param dataSource
+ */
+ public TwinColSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ setMultiSelect(true);
+ }
+
+ /**
+ * Sets the number of columns in the editor. If the number of columns is set
+ * 0, the actual number of displayed columns is determined implicitly by the
+ * adapter.
+ * <p>
+ * The number of columns overrides the value set by setWidth. Only if
+ * columns are set to 0 (default) the width set using
+ * {@link #setWidth(float, int)} or {@link #setWidth(String)} is used.
+ *
+ * @param columns
+ * the number of columns to set.
+ */
+ public void setColumns(int columns) {
+ if (columns < 0) {
+ columns = 0;
+ }
+ if (this.columns != columns) {
+ this.columns = columns;
+ requestRepaint();
+ }
+ }
+
+ public int getColumns() {
+ return columns;
+ }
+
+ public int getRows() {
+ return rows;
+ }
+
+ /**
+ * Sets the number of rows in the editor. If the number of rows is set to 0,
+ * the actual number of displayed rows is determined implicitly by the
+ * adapter.
+ * <p>
+ * If a height if set (using {@link #setHeight(String)} or
+ * {@link #setHeight(float, int)}) it overrides the number of rows. Leave
+ * the height undefined to use this method. This is the opposite of how
+ * {@link #setColumns(int)} work.
+ *
+ *
+ * @param rows
+ * the number of rows to set.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ if (this.rows != rows) {
+ this.rows = rows;
+ requestRepaint();
+ }
+ }
+
+ /**
+ * @param caption
+ * @param options
+ */
+ public TwinColSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ setMultiSelect(true);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ target.addAttribute("type", "twincol");
+ // Adds the number of columns
+ if (columns != 0) {
+ target.addAttribute("cols", columns);
+ }
+ // Adds the number of rows
+ if (rows != 0) {
+ target.addAttribute("rows", rows);
+ }
+
+ // Right and left column captions and/or icons (if set)
+ String lc = getLeftColumnCaption();
+ String rc = getRightColumnCaption();
+ if (lc != null) {
+ target.addAttribute(VTwinColSelect.ATTRIBUTE_LEFT_CAPTION, lc);
+ }
+ if (rc != null) {
+ target.addAttribute(VTwinColSelect.ATTRIBUTE_RIGHT_CAPTION, rc);
+ }
+
+ super.paintContent(target);
+ }
+
+ /**
+ * Sets the text shown above the right column.
+ *
+ * @param caption
+ * The text to show
+ */
+ public void setRightColumnCaption(String rightColumnCaption) {
+ this.rightColumnCaption = rightColumnCaption;
+ requestRepaint();
+ }
+
+ /**
+ * Returns the text shown above the right column.
+ *
+ * @return The text shown or null if not set.
+ */
+ public String getRightColumnCaption() {
+ return rightColumnCaption;
+ }
+
+ /**
+ * Sets the text shown above the left column.
+ *
+ * @param caption
+ * The text to show
+ */
+ public void setLeftColumnCaption(String leftColumnCaption) {
+ this.leftColumnCaption = leftColumnCaption;
+ requestRepaint();
+ }
+
+ /**
+ * Returns the text shown above the left column.
+ *
+ * @return The text shown or null if not set.
+ */
+ public String getLeftColumnCaption() {
+ return leftColumnCaption;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/UniqueSerializable.java b/server/src/com/vaadin/ui/UniqueSerializable.java
new file mode 100644
index 0000000000..828b285538
--- /dev/null
+++ b/server/src/com/vaadin/ui/UniqueSerializable.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+import java.io.Serializable;
+
+/**
+ * A base class for generating an unique object that is serializable.
+ * <p>
+ * This class is abstract but has no abstract methods to force users to create
+ * an anonymous inner class. Otherwise each instance will not be unique.
+ *
+ * @author Vaadin Ltd
+ * @version @VERSION@
+ * @since 7.0
+ *
+ */
+public abstract class UniqueSerializable implements Serializable {
+
+ @Override
+ public int hashCode() {
+ return getClass().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return getClass() == obj.getClass();
+ }
+}
diff --git a/server/src/com/vaadin/ui/Upload.java b/server/src/com/vaadin/ui/Upload.java
new file mode 100644
index 0000000000..9d533b67f6
--- /dev/null
+++ b/server/src/com/vaadin/ui/Upload.java
@@ -0,0 +1,1055 @@
+/*
+ * @VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.StreamVariable.StreamingProgressEvent;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.server.NoInputStreamException;
+import com.vaadin.terminal.gwt.server.NoOutputStreamException;
+
+/**
+ * Component for uploading files from client to server.
+ *
+ * <p>
+ * The visible component consists of a file name input box and a browse button
+ * and an upload submit button to start uploading.
+ *
+ * <p>
+ * The Upload component needs a java.io.OutputStream to write the uploaded data.
+ * You need to implement the Upload.Receiver interface and return the output
+ * stream in the receiveUpload() method.
+ *
+ * <p>
+ * You can get an event regarding starting (StartedEvent), progress
+ * (ProgressEvent), and finishing (FinishedEvent) of upload by implementing
+ * StartedListener, ProgressListener, and FinishedListener, respectively. The
+ * FinishedListener is called for both failed and succeeded uploads. If you wish
+ * to separate between these two cases, you can use SucceededListener
+ * (SucceededEvenet) and FailedListener (FailedEvent).
+ *
+ * <p>
+ * The upload component does not itself show upload progress, but you can use
+ * the ProgressIndicator for providing progress feedback by implementing
+ * ProgressListener and updating the indicator in updateProgress().
+ *
+ * <p>
+ * Setting upload component immediate initiates the upload as soon as a file is
+ * selected, instead of the common pattern of file selection field and upload
+ * button.
+ *
+ * <p>
+ * Note! Because of browser dependent implementations of <input type="file">
+ * element, setting size for Upload component is not supported. For some
+ * browsers setting size may work to some extend.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Upload extends AbstractComponent implements Component.Focusable,
+ Vaadin6Component {
+
+ /**
+ * Should the field be focused on next repaint?
+ */
+ private final boolean focus = false;
+
+ /**
+ * The tab order number of this field.
+ */
+ private int tabIndex = 0;
+
+ /**
+ * The output of the upload is redirected to this receiver.
+ */
+ private Receiver receiver;
+
+ private boolean isUploading;
+
+ private long contentLength = -1;
+
+ private int totalBytes;
+
+ private String buttonCaption = "Upload";
+
+ /**
+ * ProgressListeners to which information about progress is sent during
+ * upload
+ */
+ private LinkedHashSet<ProgressListener> progressListeners;
+
+ private boolean interrupted = false;
+
+ private boolean notStarted;
+
+ private int nextid;
+
+ /**
+ * Flag to indicate that submitting file has been requested.
+ */
+ private boolean forceSubmit;
+
+ /**
+ * Creates a new instance of Upload.
+ *
+ * The receiver must be set before performing an upload.
+ */
+ public Upload() {
+ }
+
+ public Upload(String caption, Receiver uploadReceiver) {
+ setCaption(caption);
+ receiver = uploadReceiver;
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ if (variables.containsKey("pollForStart")) {
+ int id = (Integer) variables.get("pollForStart");
+ if (!isUploading && id == nextid) {
+ notStarted = true;
+ requestRepaint();
+ } else {
+ }
+ }
+ }
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * Target to paint the content on.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (notStarted) {
+ target.addAttribute("notStarted", true);
+ notStarted = false;
+ return;
+ }
+ if (forceSubmit) {
+ target.addAttribute("forceSubmit", true);
+ forceSubmit = true;
+ return;
+ }
+ // The field should be focused
+ if (focus) {
+ target.addAttribute("focus", true);
+ }
+
+ // The tab ordering number
+ if (tabIndex >= 0) {
+ target.addAttribute("tabindex", tabIndex);
+ }
+
+ target.addAttribute("state", isUploading);
+
+ if (buttonCaption != null) {
+ target.addAttribute("buttoncaption", buttonCaption);
+ }
+
+ target.addAttribute("nextid", nextid);
+
+ // Post file to this strean variable
+ target.addVariable(this, "action", getStreamVariable());
+
+ }
+
+ /**
+ * Interface that must be implemented by the upload receivers to provide the
+ * Upload component an output stream to write the uploaded data.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface Receiver extends Serializable {
+
+ /**
+ * Invoked when a new upload arrives.
+ *
+ * @param filename
+ * the desired filename of the upload, usually as specified
+ * by the client.
+ * @param mimeType
+ * the MIME type of the uploaded file.
+ * @return Stream to which the uploaded file should be written.
+ */
+ public OutputStream receiveUpload(String filename, String mimeType);
+
+ }
+
+ /* Upload events */
+
+ private static final Method UPLOAD_FINISHED_METHOD;
+
+ private static final Method UPLOAD_FAILED_METHOD;
+
+ private static final Method UPLOAD_SUCCEEDED_METHOD;
+
+ private static final Method UPLOAD_STARTED_METHOD;
+
+ static {
+ try {
+ UPLOAD_FINISHED_METHOD = FinishedListener.class.getDeclaredMethod(
+ "uploadFinished", new Class[] { FinishedEvent.class });
+ UPLOAD_FAILED_METHOD = FailedListener.class.getDeclaredMethod(
+ "uploadFailed", new Class[] { FailedEvent.class });
+ UPLOAD_STARTED_METHOD = StartedListener.class.getDeclaredMethod(
+ "uploadStarted", new Class[] { StartedEvent.class });
+ UPLOAD_SUCCEEDED_METHOD = SucceededListener.class
+ .getDeclaredMethod("uploadSucceeded",
+ new Class[] { SucceededEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in Upload");
+ }
+ }
+
+ /**
+ * Upload.FinishedEvent is sent when the upload receives a file, regardless
+ * of whether the reception was successful or failed. If you wish to
+ * distinguish between the two cases, use either SucceededEvent or
+ * FailedEvent, which are both subclasses of the FinishedEvent.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class FinishedEvent extends Component.Event {
+
+ /**
+ * Length of the received file.
+ */
+ private final long length;
+
+ /**
+ * MIME type of the received file.
+ */
+ private final String type;
+
+ /**
+ * Received file name.
+ */
+ private final String filename;
+
+ /**
+ *
+ * @param source
+ * the source of the file.
+ * @param filename
+ * the received file name.
+ * @param MIMEType
+ * the MIME type of the received file.
+ * @param length
+ * the length of the received file.
+ */
+ public FinishedEvent(Upload source, String filename, String MIMEType,
+ long length) {
+ super(source);
+ type = MIMEType;
+ this.filename = filename;
+ this.length = length;
+ }
+
+ /**
+ * Uploads where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public Upload getUpload() {
+ return (Upload) getSource();
+ }
+
+ /**
+ * Gets the file name.
+ *
+ * @return the filename.
+ */
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * Gets the MIME Type of the file.
+ *
+ * @return the MIME type.
+ */
+ public String getMIMEType() {
+ return type;
+ }
+
+ /**
+ * Gets the length of the file.
+ *
+ * @return the length.
+ */
+ public long getLength() {
+ return length;
+ }
+
+ }
+
+ /**
+ * Upload.FailedEvent event is sent when the upload is received, but the
+ * reception is interrupted for some reason.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class FailedEvent extends FinishedEvent {
+
+ private Exception reason = null;
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ * @param exception
+ */
+ public FailedEvent(Upload source, String filename, String MIMEType,
+ long length, Exception reason) {
+ this(source, filename, MIMEType, length);
+ this.reason = reason;
+ }
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ * @param exception
+ */
+ public FailedEvent(Upload source, String filename, String MIMEType,
+ long length) {
+ super(source, filename, MIMEType, length);
+ }
+
+ /**
+ * Gets the exception that caused the failure.
+ *
+ * @return the exception that caused the failure, null if n/a
+ */
+ public Exception getReason() {
+ return reason;
+ }
+
+ }
+
+ /**
+ * FailedEvent that indicates that an output stream could not be obtained.
+ */
+ public static class NoOutputStreamEvent extends FailedEvent {
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ public NoOutputStreamEvent(Upload source, String filename,
+ String MIMEType, long length) {
+ super(source, filename, MIMEType, length);
+ }
+ }
+
+ /**
+ * FailedEvent that indicates that an input stream could not be obtained.
+ */
+ public static class NoInputStreamEvent extends FailedEvent {
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ public NoInputStreamEvent(Upload source, String filename,
+ String MIMEType, long length) {
+ super(source, filename, MIMEType, length);
+ }
+
+ }
+
+ /**
+ * Upload.SucceededEvent event is sent when the upload is received
+ * successfully.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public static class SucceededEvent extends FinishedEvent {
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ public SucceededEvent(Upload source, String filename, String MIMEType,
+ long length) {
+ super(source, filename, MIMEType, length);
+ }
+
+ }
+
+ /**
+ * Upload.StartedEvent event is sent when the upload is started to received.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+ public static class StartedEvent extends Component.Event {
+
+ private final String filename;
+ private final String type;
+ /**
+ * Length of the received file.
+ */
+ private final long length;
+
+ /**
+ *
+ * @param source
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ public StartedEvent(Upload source, String filename, String MIMEType,
+ long contentLength) {
+ super(source);
+ this.filename = filename;
+ type = MIMEType;
+ length = contentLength;
+ }
+
+ /**
+ * Uploads where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public Upload getUpload() {
+ return (Upload) getSource();
+ }
+
+ /**
+ * Gets the file name.
+ *
+ * @return the filename.
+ */
+ public String getFilename() {
+ return filename;
+ }
+
+ /**
+ * Gets the MIME Type of the file.
+ *
+ * @return the MIME type.
+ */
+ public String getMIMEType() {
+ return type;
+ }
+
+ /**
+ * @return the length of the file that is being uploaded
+ */
+ public long getContentLength() {
+ return length;
+ }
+
+ }
+
+ /**
+ * Receives the events when the upload starts.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+ public interface StartedListener extends Serializable {
+
+ /**
+ * Upload has started.
+ *
+ * @param event
+ * the Upload started event.
+ */
+ public void uploadStarted(StartedEvent event);
+ }
+
+ /**
+ * Receives the events when the uploads are ready.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface FinishedListener extends Serializable {
+
+ /**
+ * Upload has finished.
+ *
+ * @param event
+ * the Upload finished event.
+ */
+ public void uploadFinished(FinishedEvent event);
+ }
+
+ /**
+ * Receives events when the uploads are finished, but unsuccessful.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface FailedListener extends Serializable {
+
+ /**
+ * Upload has finished unsuccessfully.
+ *
+ * @param event
+ * the Upload failed event.
+ */
+ public void uploadFailed(FailedEvent event);
+ }
+
+ /**
+ * Receives events when the uploads are successfully finished.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+ public interface SucceededListener extends Serializable {
+
+ /**
+ * Upload successfull..
+ *
+ * @param event
+ * the Upload successfull event.
+ */
+ public void uploadSucceeded(SucceededEvent event);
+ }
+
+ /**
+ * Adds the upload started event listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(StartedListener listener) {
+ addListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD);
+ }
+
+ /**
+ * Removes the upload started event listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(StartedListener listener) {
+ removeListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD);
+ }
+
+ /**
+ * Adds the upload received event listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(FinishedListener listener) {
+ addListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD);
+ }
+
+ /**
+ * Removes the upload received event listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(FinishedListener listener) {
+ removeListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD);
+ }
+
+ /**
+ * Adds the upload interrupted event listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(FailedListener listener) {
+ addListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD);
+ }
+
+ /**
+ * Removes the upload interrupted event listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(FailedListener listener) {
+ removeListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD);
+ }
+
+ /**
+ * Adds the upload success event listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(SucceededListener listener) {
+ addListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD);
+ }
+
+ /**
+ * Removes the upload success event listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(SucceededListener listener) {
+ removeListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD);
+ }
+
+ /**
+ * Adds the upload success event listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addListener(ProgressListener listener) {
+ if (progressListeners == null) {
+ progressListeners = new LinkedHashSet<ProgressListener>();
+ }
+ progressListeners.add(listener);
+ }
+
+ /**
+ * Removes the upload success event listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeListener(ProgressListener listener) {
+ if (progressListeners != null) {
+ progressListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Emit upload received event.
+ *
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ protected void fireStarted(String filename, String MIMEType) {
+ fireEvent(new Upload.StartedEvent(this, filename, MIMEType,
+ contentLength));
+ }
+
+ /**
+ * Emits the upload failed event.
+ *
+ * @param filename
+ * @param MIMEType
+ * @param length
+ */
+ protected void fireUploadInterrupted(String filename, String MIMEType,
+ long length) {
+ fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length));
+ }
+
+ protected void fireNoInputStream(String filename, String MIMEType,
+ long length) {
+ fireEvent(new Upload.NoInputStreamEvent(this, filename, MIMEType,
+ length));
+ }
+
+ protected void fireNoOutputStream(String filename, String MIMEType,
+ long length) {
+ fireEvent(new Upload.NoOutputStreamEvent(this, filename, MIMEType,
+ length));
+ }
+
+ protected void fireUploadInterrupted(String filename, String MIMEType,
+ long length, Exception e) {
+ fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length, e));
+ }
+
+ /**
+ * Emits the upload success event.
+ *
+ * @param filename
+ * @param MIMEType
+ * @param length
+ *
+ */
+ protected void fireUploadSuccess(String filename, String MIMEType,
+ long length) {
+ fireEvent(new Upload.SucceededEvent(this, filename, MIMEType, length));
+ }
+
+ /**
+ * Emits the progress event.
+ *
+ * @param totalBytes
+ * bytes received so far
+ * @param contentLength
+ * actual size of the file being uploaded, if known
+ *
+ */
+ protected void fireUpdateProgress(long totalBytes, long contentLength) {
+ // this is implemented differently than other listeners to maintain
+ // backwards compatibility
+ if (progressListeners != null) {
+ for (Iterator<ProgressListener> it = progressListeners.iterator(); it
+ .hasNext();) {
+ ProgressListener l = it.next();
+ l.updateProgress(totalBytes, contentLength);
+ }
+ }
+ }
+
+ /**
+ * Returns the current receiver.
+ *
+ * @return the StreamVariable.
+ */
+ public Receiver getReceiver() {
+ return receiver;
+ }
+
+ /**
+ * Sets the receiver.
+ *
+ * @param receiver
+ * the receiver to set.
+ */
+ public void setReceiver(Receiver receiver) {
+ this.receiver = receiver;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void focus() {
+ super.focus();
+ }
+
+ /**
+ * Gets the Tabulator index of this Focusable component.
+ *
+ * @see com.vaadin.ui.Component.Focusable#getTabIndex()
+ */
+ @Override
+ public int getTabIndex() {
+ return tabIndex;
+ }
+
+ /**
+ * Sets the Tabulator index of this Focusable component.
+ *
+ * @see com.vaadin.ui.Component.Focusable#setTabIndex(int)
+ */
+ @Override
+ public void setTabIndex(int tabIndex) {
+ this.tabIndex = tabIndex;
+ }
+
+ /**
+ * Go into upload state. This is to prevent double uploading on same
+ * component.
+ *
+ * Warning: this is an internal method used by the framework and should not
+ * be used by user of the Upload component. Using it results in the Upload
+ * component going in wrong state and not working. It is currently public
+ * because it is used by another class.
+ */
+ public void startUpload() {
+ if (isUploading) {
+ throw new IllegalStateException("uploading already started");
+ }
+ isUploading = true;
+ nextid++;
+ }
+
+ /**
+ * Interrupts the upload currently being received. The interruption will be
+ * done by the receiving tread so this method will return immediately and
+ * the actual interrupt will happen a bit later.
+ */
+ public void interruptUpload() {
+ if (isUploading) {
+ interrupted = true;
+ }
+ }
+
+ /**
+ * Go into state where new uploading can begin.
+ *
+ * Warning: this is an internal method used by the framework and should not
+ * be used by user of the Upload component.
+ */
+ private void endUpload() {
+ isUploading = false;
+ contentLength = -1;
+ interrupted = false;
+ requestRepaint();
+ }
+
+ public boolean isUploading() {
+ return isUploading;
+ }
+
+ /**
+ * Gets read bytes of the file currently being uploaded.
+ *
+ * @return bytes
+ */
+ public long getBytesRead() {
+ return totalBytes;
+ }
+
+ /**
+ * Returns size of file currently being uploaded. Value sane only during
+ * upload.
+ *
+ * @return size in bytes
+ */
+ public long getUploadSize() {
+ return contentLength;
+ }
+
+ /**
+ * This method is deprecated, use addListener(ProgressListener) instead.
+ *
+ * @deprecated Use addListener(ProgressListener) instead.
+ * @param progressListener
+ */
+ @Deprecated
+ public void setProgressListener(ProgressListener progressListener) {
+ addListener(progressListener);
+ }
+
+ /**
+ * This method is deprecated.
+ *
+ * @deprecated Replaced with addListener/removeListener
+ * @return listener
+ *
+ */
+ @Deprecated
+ public ProgressListener getProgressListener() {
+ if (progressListeners == null || progressListeners.isEmpty()) {
+ return null;
+ } else {
+ return progressListeners.iterator().next();
+ }
+ }
+
+ /**
+ * ProgressListener receives events to track progress of upload.
+ */
+ public interface ProgressListener extends Serializable {
+ /**
+ * Updates progress to listener
+ *
+ * @param readBytes
+ * bytes transferred
+ * @param contentLength
+ * total size of file currently being uploaded, -1 if unknown
+ */
+ public void updateProgress(long readBytes, long contentLength);
+ }
+
+ /**
+ * @return String to be rendered into button that fires uploading
+ */
+ public String getButtonCaption() {
+ return buttonCaption;
+ }
+
+ /**
+ * In addition to the actual file chooser, upload components have button
+ * that starts actual upload progress. This method is used to set text in
+ * that button.
+ * <p>
+ * In case the button text is set to null, the button is hidden. In this
+ * case developer must explicitly initiate the upload process with
+ * {@link #submitUpload()}.
+ * <p>
+ * In case the Upload is used in immediate mode using
+ * {@link #setImmediate(boolean)}, the file choose (html input with type
+ * "file") is hidden and only the button with this text is shown.
+ * <p>
+ *
+ * <p>
+ * <strong>Note</strong> the string given is set as is to the button. HTML
+ * formatting is not stripped. Be sure to properly validate your value
+ * according to your needs.
+ *
+ * @param buttonCaption
+ * text for upload components button.
+ */
+ public void setButtonCaption(String buttonCaption) {
+ this.buttonCaption = buttonCaption;
+ requestRepaint();
+ }
+
+ /**
+ * Forces the upload the send selected file to the server.
+ * <p>
+ * In case developer wants to use this feature, he/she will most probably
+ * want to hide the uploads internal submit button by setting its caption to
+ * null with {@link #setButtonCaption(String)} method.
+ * <p>
+ * Note, that the upload runs asynchronous. Developer should use normal
+ * upload listeners to trac the process of upload. If the field is empty
+ * uploaded the file name will be empty string and file length 0 in the
+ * upload finished event.
+ * <p>
+ * Also note, that the developer should not remove or modify the upload in
+ * the same user transaction where the upload submit is requested. The
+ * upload may safely be hidden or removed once the upload started event is
+ * fired.
+ */
+ public void submitUpload() {
+ requestRepaint();
+ forceSubmit = true;
+ }
+
+ @Override
+ public void requestRepaint() {
+ forceSubmit = false;
+ super.requestRepaint();
+ }
+
+ /*
+ * Handle to terminal via Upload monitors and controls the upload during it
+ * is being streamed.
+ */
+ private com.vaadin.terminal.StreamVariable streamVariable;
+
+ protected com.vaadin.terminal.StreamVariable getStreamVariable() {
+ if (streamVariable == null) {
+ streamVariable = new com.vaadin.terminal.StreamVariable() {
+ private StreamingStartEvent lastStartedEvent;
+
+ @Override
+ public boolean listenProgress() {
+ return (progressListeners != null && !progressListeners
+ .isEmpty());
+ }
+
+ @Override
+ public void onProgress(StreamingProgressEvent event) {
+ fireUpdateProgress(event.getBytesReceived(),
+ event.getContentLength());
+ }
+
+ @Override
+ public boolean isInterrupted() {
+ return interrupted;
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ OutputStream receiveUpload = receiver.receiveUpload(
+ lastStartedEvent.getFileName(),
+ lastStartedEvent.getMimeType());
+ lastStartedEvent = null;
+ return receiveUpload;
+ }
+
+ @Override
+ public void streamingStarted(StreamingStartEvent event) {
+ startUpload();
+ contentLength = event.getContentLength();
+ fireStarted(event.getFileName(), event.getMimeType());
+ lastStartedEvent = event;
+ }
+
+ @Override
+ public void streamingFinished(StreamingEndEvent event) {
+ fireUploadSuccess(event.getFileName(), event.getMimeType(),
+ event.getContentLength());
+ endUpload();
+ requestRepaint();
+ }
+
+ @Override
+ public void streamingFailed(StreamingErrorEvent event) {
+ Exception exception = event.getException();
+ if (exception instanceof NoInputStreamException) {
+ fireNoInputStream(event.getFileName(),
+ event.getMimeType(), 0);
+ } else if (exception instanceof NoOutputStreamException) {
+ fireNoOutputStream(event.getFileName(),
+ event.getMimeType(), 0);
+ } else {
+ fireUploadInterrupted(event.getFileName(),
+ event.getMimeType(), 0, exception);
+ }
+ endUpload();
+ }
+ };
+ }
+ return streamVariable;
+ }
+
+ @Override
+ public java.util.Collection<?> getListeners(java.lang.Class<?> eventType) {
+ if (StreamingProgressEvent.class.isAssignableFrom(eventType)) {
+ if (progressListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections.unmodifiableCollection(progressListeners);
+ }
+
+ }
+ return super.getListeners(eventType);
+ };
+}
diff --git a/server/src/com/vaadin/ui/VerticalLayout.java b/server/src/com/vaadin/ui/VerticalLayout.java
new file mode 100644
index 0000000000..a04d052d98
--- /dev/null
+++ b/server/src/com/vaadin/ui/VerticalLayout.java
@@ -0,0 +1,25 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+/**
+ * Vertical layout
+ *
+ * <code>VerticalLayout</code> is a component container, which shows the
+ * subcomponents in the order of their addition (vertically). A vertical layout
+ * is by default 100% wide.
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.3
+ */
+@SuppressWarnings("serial")
+public class VerticalLayout extends AbstractOrderedLayout {
+
+ public VerticalLayout() {
+ setWidth("100%");
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/VerticalSplitPanel.java b/server/src/com/vaadin/ui/VerticalSplitPanel.java
new file mode 100644
index 0000000000..0630240e9c
--- /dev/null
+++ b/server/src/com/vaadin/ui/VerticalSplitPanel.java
@@ -0,0 +1,30 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui;
+
+/**
+ * A vertical split panel contains two components and lays them vertically. The
+ * first component is above the second component.
+ *
+ * <pre>
+ * +--------------------------+
+ * | |
+ * | The first component |
+ * | |
+ * +==========================+ <-- splitter
+ * | |
+ * | The second component |
+ * | |
+ * +--------------------------+
+ * </pre>
+ *
+ */
+public class VerticalSplitPanel extends AbstractSplitPanel {
+
+ public VerticalSplitPanel() {
+ super();
+ setSizeFull();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Video.java b/server/src/com/vaadin/ui/Video.java
new file mode 100644
index 0000000000..d4f95a5be3
--- /dev/null
+++ b/server/src/com/vaadin/ui/Video.java
@@ -0,0 +1,81 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import com.vaadin.shared.ui.video.VideoState;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.server.ResourceReference;
+
+/**
+ * The Video component translates into an HTML5 &lt;video&gt; element and as
+ * such is only supported in browsers that support HTML5 media markup. Browsers
+ * that do not support HTML5 display the text or HTML set by calling
+ * {@link #setAltText(String)}.
+ *
+ * A flash-player fallback can be implemented by setting HTML content allowed (
+ * {@link #setHtmlContentAllowed(boolean)} and calling
+ * {@link #setAltText(String)} with the flash player markup. An example of flash
+ * fallback can be found at the <a href=
+ * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash"
+ * >Mozilla Developer Network</a>.
+ *
+ * Multiple sources can be specified. Which of the sources is used is selected
+ * by the browser depending on which file formats it supports. See <a
+ * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a
+ * table of formats supported by different browsers.
+ *
+ * @author Vaadin Ltd
+ * @since 6.7.0
+ */
+public class Video extends AbstractMedia {
+
+ @Override
+ public VideoState getState() {
+ return (VideoState) super.getState();
+ }
+
+ public Video() {
+ this("", null);
+ }
+
+ /**
+ * @param caption
+ * The caption for this video.
+ */
+ public Video(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * @param caption
+ * The caption for this video.
+ * @param source
+ * The Resource containing the video to play.
+ */
+ public Video(String caption, Resource source) {
+ setCaption(caption);
+ setSource(source);
+ setShowControls(true);
+ }
+
+ /**
+ * Sets the poster image, which is shown in place of the video before the
+ * user presses play.
+ *
+ * @param poster
+ */
+ public void setPoster(Resource poster) {
+ getState().setPoster(ResourceReference.create(poster));
+ requestRepaint();
+ }
+
+ /**
+ * @return The poster image.
+ */
+ public Resource getPoster() {
+ return ResourceReference.getResource(getState().getPoster());
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java
new file mode 100644
index 0000000000..e413d35e6d
--- /dev/null
+++ b/server/src/com/vaadin/ui/Window.java
@@ -0,0 +1,853 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.BlurNotifier;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.FieldEvents.FocusNotifier;
+import com.vaadin.event.MouseEvents.ClickEvent;
+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.shared.MouseEventDetails;
+import com.vaadin.shared.ui.window.WindowServerRpc;
+import com.vaadin.shared.ui.window.WindowState;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Vaadin6Component;
+import com.vaadin.terminal.gwt.client.ui.root.VRoot;
+
+/**
+ * A component that represents a floating popup window that can be added to a
+ * {@link Root}. A window is added to a {@code Root} using
+ * {@link Root#addWindow(Window)}. </p>
+ * <p>
+ * The contents of a window is set using {@link #setContent(ComponentContainer)}
+ * or by using the {@link #Window(String, ComponentContainer)} constructor. The
+ * contents can in turn contain other components. By default, a
+ * {@link VerticalLayout} is used as content.
+ * </p>
+ * <p>
+ * A window can be positioned on the screen using absolute coordinates (pixels)
+ * or set to be centered using {@link #center()}
+ * </p>
+ * <p>
+ * The caption is displayed in the window header.
+ * </p>
+ * <p>
+ * In Vaadin versions prior to 7.0.0, Window was also used as application level
+ * windows. This function is now covered by the {@link Root} class.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+@SuppressWarnings("serial")
+public class Window extends Panel implements FocusNotifier, BlurNotifier,
+ Vaadin6Component {
+
+ private WindowServerRpc rpc = new WindowServerRpc() {
+
+ @Override
+ public void click(MouseEventDetails mouseDetails) {
+ fireEvent(new ClickEvent(Window.this, mouseDetails));
+ }
+ };
+
+ private int browserWindowWidth = -1;
+
+ private int browserWindowHeight = -1;
+
+ /**
+ * Creates a new unnamed window with a default layout.
+ */
+ public Window() {
+ this("", null);
+ }
+
+ /**
+ * Creates a new unnamed window with a default layout and given title.
+ *
+ * @param caption
+ * the title of the window.
+ */
+ public Window(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new unnamed window with the given content and title.
+ *
+ * @param caption
+ * the title of the window.
+ * @param content
+ * the contents of the window
+ */
+ public Window(String caption, ComponentContainer content) {
+ super(caption, content);
+ registerRpc(rpc);
+ setSizeUndefined();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Panel#addComponent(com.vaadin.ui.Component)
+ */
+
+ @Override
+ public void addComponent(Component c) {
+ if (c instanceof Window) {
+ throw new IllegalArgumentException(
+ "Window cannot be added to another via addComponent. "
+ + "Use addWindow(Window) instead.");
+ }
+ super.addComponent(c);
+ }
+
+ /* ********************************************************************* */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Panel#paintContent(com.vaadin.terminal.PaintTarget)
+ */
+
+ @Override
+ public synchronized void paintContent(PaintTarget target)
+ throws PaintException {
+ if (bringToFront != null) {
+ target.addAttribute("bringToFront", bringToFront.intValue());
+ bringToFront = null;
+ }
+
+ // Contents of the window panel is painted
+ super.paintContent(target);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Panel#changeVariables(java.lang.Object, java.util.Map)
+ */
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ // TODO Are these for top level windows or sub windows?
+ boolean sizeHasChanged = false;
+ // size is handled in super class, but resize events only in windows ->
+ // so detect if size change occurs before super.changeVariables()
+ if (variables.containsKey("height")
+ && (getHeightUnits() != Unit.PIXELS || (Integer) variables
+ .get("height") != getHeight())) {
+ sizeHasChanged = true;
+ }
+ if (variables.containsKey("width")
+ && (getWidthUnits() != Unit.PIXELS || (Integer) variables
+ .get("width") != getWidth())) {
+ sizeHasChanged = true;
+ }
+ Integer browserHeightVar = (Integer) variables
+ .get(VRoot.BROWSER_HEIGHT_VAR);
+ if (browserHeightVar != null
+ && browserHeightVar.intValue() != browserWindowHeight) {
+ browserWindowHeight = browserHeightVar.intValue();
+ sizeHasChanged = true;
+ }
+ Integer browserWidthVar = (Integer) variables
+ .get(VRoot.BROWSER_WIDTH_VAR);
+ if (browserWidthVar != null
+ && browserWidthVar.intValue() != browserWindowWidth) {
+ browserWindowWidth = browserWidthVar.intValue();
+ sizeHasChanged = true;
+ }
+
+ super.changeVariables(source, variables);
+
+ // Positioning
+ final Integer positionx = (Integer) variables.get("positionx");
+ if (positionx != null) {
+ final int x = positionx.intValue();
+ // This is information from the client so it is already using the
+ // position. No need to repaint.
+ setPositionX(x < 0 ? -1 : x, false);
+ }
+ final Integer positiony = (Integer) variables.get("positiony");
+ if (positiony != null) {
+ final int y = positiony.intValue();
+ // This is information from the client so it is already using the
+ // position. No need to repaint.
+ setPositionY(y < 0 ? -1 : y, false);
+ }
+
+ if (isClosable()) {
+ // Closing
+ final Boolean close = (Boolean) variables.get("close");
+ if (close != null && close.booleanValue()) {
+ close();
+ }
+ }
+
+ // fire event if size has really changed
+ if (sizeHasChanged) {
+ fireResize();
+ }
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ } else if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+
+ }
+
+ /**
+ * Method that handles window closing (from UI).
+ *
+ * <p>
+ * By default, sub-windows are removed from their respective parent windows
+ * and thus visually closed on browser-side. Browser-level windows also
+ * closed on the client-side, but they are not implicitly removed from the
+ * application.
+ * </p>
+ *
+ * <p>
+ * To explicitly close a sub-window, use {@link #removeWindow(Window)}. To
+ * react to a window being closed (after it is closed), register a
+ * {@link CloseListener}.
+ * </p>
+ */
+ public void close() {
+ Root root = getRoot();
+
+ // Don't do anything if not attached to a root
+ if (root != null) {
+ // focus is restored to the parent window
+ root.focus();
+ // subwindow is removed from the root
+ root.removeWindow(this);
+ }
+ }
+
+ /**
+ * Gets the distance of Window left border in pixels from left border of the
+ * containing (main window).
+ *
+ * @return the Distance of Window left border in pixels from left border of
+ * the containing (main window). or -1 if unspecified.
+ * @since 4.0.0
+ */
+ public int getPositionX() {
+ return getState().getPositionX();
+ }
+
+ /**
+ * Sets the distance of Window left border in pixels from left border of the
+ * containing (main window).
+ *
+ * @param positionX
+ * the Distance of Window left border in pixels from left border
+ * of the containing (main window). or -1 if unspecified.
+ * @since 4.0.0
+ */
+ public void setPositionX(int positionX) {
+ setPositionX(positionX, true);
+ }
+
+ /**
+ * Sets the distance of Window left border in pixels from left border of the
+ * containing (main window).
+ *
+ * @param positionX
+ * the Distance of Window left border in pixels from left border
+ * of the containing (main window). or -1 if unspecified.
+ * @param repaintRequired
+ * true if the window needs to be repainted, false otherwise
+ * @since 6.3.4
+ */
+ private void setPositionX(int positionX, boolean repaintRequired) {
+ getState().setPositionX(positionX);
+ getState().setCentered(false);
+ if (repaintRequired) {
+ requestRepaint();
+ }
+ }
+
+ /**
+ * Gets the distance of Window top border in pixels from top border of the
+ * containing (main window).
+ *
+ * @return Distance of Window top border in pixels from top border of the
+ * containing (main window). or -1 if unspecified .
+ *
+ * @since 4.0.0
+ */
+ public int getPositionY() {
+ return getState().getPositionY();
+ }
+
+ /**
+ * Sets the distance of Window top border in pixels from top border of the
+ * containing (main window).
+ *
+ * @param positionY
+ * the Distance of Window top border in pixels from top border of
+ * the containing (main window). or -1 if unspecified
+ *
+ * @since 4.0.0
+ */
+ public void setPositionY(int positionY) {
+ setPositionY(positionY, true);
+ }
+
+ /**
+ * Sets the distance of Window top border in pixels from top border of the
+ * containing (main window).
+ *
+ * @param positionY
+ * the Distance of Window top border in pixels from top border of
+ * the containing (main window). or -1 if unspecified
+ * @param repaintRequired
+ * true if the window needs to be repainted, false otherwise
+ *
+ * @since 6.3.4
+ */
+ private void setPositionY(int positionY, boolean repaintRequired) {
+ getState().setPositionY(positionY);
+ getState().setCentered(false);
+ if (repaintRequired) {
+ requestRepaint();
+ }
+ }
+
+ private static final Method WINDOW_CLOSE_METHOD;
+ static {
+ try {
+ WINDOW_CLOSE_METHOD = CloseListener.class.getDeclaredMethod(
+ "windowClose", new Class[] { CloseEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error, window close method not found");
+ }
+ }
+
+ public class CloseEvent extends Component.Event {
+
+ /**
+ *
+ * @param source
+ */
+ public CloseEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Window.
+ *
+ * @return the window.
+ */
+ public Window getWindow() {
+ return (Window) getSource();
+ }
+ }
+
+ /**
+ * An interface used for listening to Window close events. Add the
+ * CloseListener to a browser level window or a sub window and
+ * {@link CloseListener#windowClose(CloseEvent)} will be called whenever the
+ * user closes the window.
+ *
+ * <p>
+ * Since Vaadin 6.5, removing a window using {@link #removeWindow(Window)}
+ * fires the CloseListener.
+ * </p>
+ */
+ public interface CloseListener extends Serializable {
+ /**
+ * Called when the user closes a window. Use
+ * {@link CloseEvent#getWindow()} to get a reference to the
+ * {@link Window} that was closed.
+ *
+ * @param e
+ * Event containing
+ */
+ public void windowClose(CloseEvent e);
+ }
+
+ /**
+ * Adds a CloseListener to the window.
+ *
+ * For a sub window the CloseListener is fired when the user closes it
+ * (clicks on the close button).
+ *
+ * For a browser level window the CloseListener is fired when the browser
+ * level window is closed. Note that closing a browser level window does not
+ * mean it will be destroyed. Also note that Opera does not send events like
+ * all other browsers and therefore the close listener might not be called
+ * if Opera is used.
+ *
+ * <p>
+ * Since Vaadin 6.5, removing windows using {@link #removeWindow(Window)}
+ * does fire the CloseListener.
+ * </p>
+ *
+ * @param listener
+ * the CloseListener to add.
+ */
+ public void addListener(CloseListener listener) {
+ addListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD);
+ }
+
+ /**
+ * Removes the CloseListener from the window.
+ *
+ * <p>
+ * For more information on CloseListeners see {@link CloseListener}.
+ * </p>
+ *
+ * @param listener
+ * the CloseListener to remove.
+ */
+ public void removeListener(CloseListener listener) {
+ removeListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD);
+ }
+
+ protected void fireClose() {
+ fireEvent(new Window.CloseEvent(this));
+ }
+
+ /**
+ * Method for the resize event.
+ */
+ private static final Method WINDOW_RESIZE_METHOD;
+ static {
+ try {
+ WINDOW_RESIZE_METHOD = ResizeListener.class.getDeclaredMethod(
+ "windowResized", new Class[] { ResizeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error, window resized method not found");
+ }
+ }
+
+ /**
+ * Resize events are fired whenever the client-side fires a resize-event
+ * (e.g. the browser window is resized). The frequency may vary across
+ * browsers.
+ */
+ public class ResizeEvent extends Component.Event {
+
+ /**
+ *
+ * @param source
+ */
+ public ResizeEvent(Component source) {
+ super(source);
+ }
+
+ /**
+ * Get the window form which this event originated
+ *
+ * @return the window
+ */
+ public Window getWindow() {
+ return (Window) getSource();
+ }
+ }
+
+ /**
+ * Listener for window resize events.
+ *
+ * @see com.vaadin.ui.Window.ResizeEvent
+ */
+ public interface ResizeListener extends Serializable {
+ public void windowResized(ResizeEvent e);
+ }
+
+ /**
+ * Add a resize listener.
+ *
+ * @param listener
+ */
+ public void addListener(ResizeListener listener) {
+ addListener(ResizeEvent.class, listener, WINDOW_RESIZE_METHOD);
+ }
+
+ /**
+ * Remove a resize listener.
+ *
+ * @param listener
+ */
+ public void removeListener(ResizeListener listener) {
+ removeListener(ResizeEvent.class, listener);
+ }
+
+ /**
+ * Fire the resize event.
+ */
+ protected void fireResize() {
+ fireEvent(new ResizeEvent(this));
+ }
+
+ /**
+ * Used to keep the right order of windows if multiple windows are brought
+ * to front in a single changeset. If this is not used, the order is quite
+ * random (depends on the order getting to dirty list. e.g. which window got
+ * variable changes).
+ */
+ private Integer bringToFront = null;
+
+ /**
+ * If there are currently several windows visible, calling this method makes
+ * this window topmost.
+ * <p>
+ * This method can only be called if this window connected a root. Else an
+ * illegal state exception is thrown. Also if there are modal windows and
+ * this window is not modal, and illegal state exception is thrown.
+ * <p>
+ */
+ public void bringToFront() {
+ Root root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException(
+ "Window must be attached to parent before calling bringToFront method.");
+ }
+ int maxBringToFront = -1;
+ for (Window w : root.getWindows()) {
+ if (!isModal() && w.isModal()) {
+ throw new IllegalStateException(
+ "The root contains modal windows, non-modal window cannot be brought to front.");
+ }
+ if (w.bringToFront != null) {
+ maxBringToFront = Math.max(maxBringToFront,
+ w.bringToFront.intValue());
+ }
+ }
+ bringToFront = Integer.valueOf(maxBringToFront + 1);
+ requestRepaint();
+ }
+
+ /**
+ * Sets sub-window modal, so that widgets behind it cannot be accessed.
+ * <b>Note:</b> affects sub-windows only.
+ *
+ * @param modal
+ * true if modality is to be turned on
+ */
+ public void setModal(boolean modal) {
+ getState().setModal(modal);
+ center();
+ requestRepaint();
+ }
+
+ /**
+ * @return true if this window is modal.
+ */
+ public boolean isModal() {
+ return getState().isModal();
+ }
+
+ /**
+ * Sets sub-window resizable. <b>Note:</b> affects sub-windows only.
+ *
+ * @param resizable
+ * true if resizability is to be turned on
+ */
+ public void setResizable(boolean resizable) {
+ getState().setResizable(resizable);
+ requestRepaint();
+ }
+
+ /**
+ *
+ * @return true if window is resizable by the end-user, otherwise false.
+ */
+ public boolean isResizable() {
+ return getState().isResizable();
+ }
+
+ /**
+ *
+ * @return true if a delay is used before recalculating sizes, false if
+ * sizes are recalculated immediately.
+ */
+ public boolean isResizeLazy() {
+ return getState().isResizeLazy();
+ }
+
+ /**
+ * 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.
+ *
+ * Note, some browser send false resize events for the browser window and
+ * are therefore always lazy.
+ *
+ * @param resizeLazy
+ * true to use a delay before recalculating sizes, false to
+ * calculate immediately.
+ */
+ public void setResizeLazy(boolean resizeLazy) {
+ getState().setResizeLazy(resizeLazy);
+ requestRepaint();
+ }
+
+ /**
+ * Sets this window to be centered relative to its parent window. Affects
+ * sub-windows only. If the window is resized as a result of the size of its
+ * content changing, it will keep itself centered as long as its position is
+ * not explicitly changed programmatically or by the user.
+ * <p>
+ * <b>NOTE:</b> This method has several issues as currently implemented.
+ * Please refer to http://dev.vaadin.com/ticket/8971 for details.
+ */
+ public void center() {
+ getState().setCentered(true);
+ requestRepaint();
+ }
+
+ /**
+ * Returns the closable status of the sub window. If a sub window is
+ * closable it typically shows an X in the upper right corner. Clicking on
+ * the X sends a close event to the server. Setting closable to false will
+ * remove the X from the sub window and prevent the user from closing the
+ * window.
+ *
+ * Note! For historical reasons readonly controls the closability of the sub
+ * window and therefore readonly and closable affect each other. Setting
+ * readonly to true will set closable to false and vice versa.
+ * <p/>
+ * Closable only applies to sub windows, not to browser level windows.
+ *
+ * @return true if the sub window can be closed by the user.
+ */
+ public boolean isClosable() {
+ return !isReadOnly();
+ }
+
+ /**
+ * Sets the closable status for the sub window. If a sub window is closable
+ * it typically shows an X in the upper right corner. Clicking on the X
+ * sends a close event to the server. Setting closable to false will remove
+ * the X from the sub window and prevent the user from closing the window.
+ *
+ * Note! For historical reasons readonly controls the closability of the sub
+ * window and therefore readonly and closable affect each other. Setting
+ * readonly to true will set closable to false and vice versa.
+ * <p/>
+ * Closable only applies to sub windows, not to browser level windows.
+ *
+ * @param closable
+ * determines if the sub window can be closed by the user.
+ */
+ public void setClosable(boolean closable) {
+ setReadOnly(!closable);
+ }
+
+ /**
+ * Indicates whether a sub window can be dragged or not. By default a sub
+ * window is draggable.
+ * <p/>
+ * Draggable only applies to sub windows, not to browser level windows.
+ *
+ * @param draggable
+ * true if the sub window can be dragged by the user
+ */
+ public boolean isDraggable() {
+ return getState().isDraggable();
+ }
+
+ /**
+ * Enables or disables that a sub window can be dragged (moved) by the user.
+ * By default a sub window is draggable.
+ * <p/>
+ * Draggable only applies to sub windows, not to browser level windows.
+ *
+ * @param draggable
+ * true if the sub window can be dragged by the user
+ */
+ public void setDraggable(boolean draggable) {
+ getState().setDraggable(draggable);
+ requestRepaint();
+ }
+
+ /*
+ * Actions
+ */
+ protected CloseShortcut closeShortcut;
+
+ /**
+ * Makes is possible to close the window by pressing the given
+ * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/>
+ * Note that this shortcut only reacts while the window has focus, closing
+ * itself - if you want to close a subwindow from a parent window, use
+ * {@link #addAction(com.vaadin.event.Action)} of the parent window instead.
+ *
+ * @param keyCode
+ * the keycode for invoking the shortcut
+ * @param modifiers
+ * the (optional) modifiers for invoking the shortcut, null for
+ * none
+ */
+ public void setCloseShortcut(int keyCode, int... modifiers) {
+ if (closeShortcut != null) {
+ removeAction(closeShortcut);
+ }
+ closeShortcut = new CloseShortcut(this, keyCode, modifiers);
+ addAction(closeShortcut);
+ }
+
+ /**
+ * Removes the keyboard shortcut previously set with
+ * {@link #setCloseShortcut(int, int...)}.
+ */
+ public void removeCloseShortcut() {
+ if (closeShortcut != null) {
+ removeAction(closeShortcut);
+ closeShortcut = null;
+ }
+ }
+
+ /**
+ * A {@link ShortcutListener} specifically made to define a keyboard
+ * shortcut that closes the window.
+ *
+ * <pre>
+ * <code>
+ * // within the window using helper
+ * subWindow.setCloseShortcut(KeyCode.ESCAPE, null);
+ *
+ * // or globally
+ * getWindow().addAction(new Window.CloseShortcut(subWindow, KeyCode.ESCAPE));
+ * </code>
+ * </pre>
+ *
+ */
+ public static class CloseShortcut extends ShortcutListener {
+ protected Window window;
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * shorthand notation defined in {@link ShortcutAction}.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param shorthandCaption
+ * the caption with shortcut keycode and modifiers indicated
+ */
+ public CloseShortcut(Window window, String shorthandCaption) {
+ super(shorthandCaption);
+ this.window = window;
+ }
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * given {@link KeyCode} and {@link ModifierKey}s.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ * @param modifiers
+ * optional modifiers for shortcut
+ */
+ public CloseShortcut(Window window, int keyCode, int... modifiers) {
+ super(null, keyCode, modifiers);
+ this.window = window;
+ }
+
+ /**
+ * Creates a keyboard shortcut for closing the given window using the
+ * given {@link KeyCode}.
+ *
+ * @param window
+ * to be closed when the shortcut is invoked
+ * @param keyCode
+ * KeyCode to react to
+ */
+ public CloseShortcut(Window window, int keyCode) {
+ this(window, keyCode, null);
+ }
+
+ @Override
+ public void handleAction(Object sender, Object target) {
+ window.close();
+ }
+ }
+
+ /**
+ * Note, that focus/blur listeners in Window class are only supported by sub
+ * windows. Also note that Window is not considered focused if its contained
+ * component currently has focus.
+ *
+ * @see com.vaadin.event.FieldEvents.FocusNotifier#addListener(com.vaadin.event.FieldEvents.FocusListener)
+ */
+
+ @Override
+ public void addListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ /**
+ * Note, that focus/blur listeners in Window class are only supported by sub
+ * windows. Also note that Window is not considered focused if its contained
+ * component currently has focus.
+ *
+ * @see com.vaadin.event.FieldEvents.BlurNotifier#addListener(com.vaadin.event.FieldEvents.BlurListener)
+ */
+
+ @Override
+ public void addListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * If the window is a sub-window focusing will cause the sub-window to be
+ * brought on top of other sub-windows on gain keyboard focus.
+ */
+
+ @Override
+ public void focus() {
+ /*
+ * When focusing a sub-window it basically means it should be brought to
+ * the front. Instead of just moving the keyboard focus we focus the
+ * window and bring it top-most.
+ */
+ super.focus();
+ bringToFront();
+ }
+
+ @Override
+ public WindowState getState() {
+ return (WindowState) super.getState();
+ }
+}
diff --git a/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif
new file mode 100644
index 0000000000..936c220d11
--- /dev/null
+++ b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif
Binary files differ
diff --git a/server/src/com/vaadin/ui/doc-files/component_interfaces.gif b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif
new file mode 100644
index 0000000000..44c99826bb
--- /dev/null
+++ b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif
Binary files differ
diff --git a/server/src/com/vaadin/ui/package.html b/server/src/com/vaadin/ui/package.html
new file mode 100644
index 0000000000..6b19a28fe7
--- /dev/null
+++ b/server/src/com/vaadin/ui/package.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides interfaces and classes in Vaadin.</p>
+
+<h2>Package Specification</h2>
+
+<p><strong>Interface hierarchy</strong></p>
+
+<p>The general interface hierarchy looks like this:</p>
+
+<p style="text-align: center;"><img
+ src="doc-files/component_interfaces.gif" /></p>
+
+<p><i>Note that the above picture includes only the main
+interfaces. This package includes several other lesser sub-interfaces
+which are not significant in this scope. The interfaces not appearing
+here are documented with the classes that define them.</i></p>
+
+<p>The {@link com.vaadin.ui.Component} interface is the top-level
+interface which must be implemented by all user interface components in
+Vaadin. It defines the common properties of the components and how the
+framework will handle them. Most simple components, such as {@link
+com.vaadin.ui.Button}, for example, do not need to implement the
+lower-level interfaces described below. Notice that also the classes and
+interfaces required by the component event framework are defined in
+{@link com.vaadin.ui.Component}.</p>
+
+<p>The next level in the component hierarchy are the classes
+implementing the {@link com.vaadin.ui.ComponentContainer} interface. It
+adds the capacity to contain other components to {@link
+com.vaadin.ui.Component} with a simple API.</p>
+
+<p>The third and last level is the {@link com.vaadin.ui.Layout},
+which adds the concept of location to the components contained in a
+{@link com.vaadin.ui.ComponentContainer}. It can be used to create
+containers which contents can be positioned.</p>
+
+<p><strong>Component class hierarchy</strong></p>
+
+<p>The actual component classes form a hierarchy like this:</p>
+
+<center><img src="doc-files/component_class_hierarchy.gif" /></center>
+<br />
+
+<center><i>Underlined classes are abstract.</i></center>
+
+<p>At the top level is {@link com.vaadin.ui.AbstractComponent} which
+implements the {@link com.vaadin.ui.Component} interface. As the name
+suggests it is abstract, but it does include a default implementation
+for all methods defined in <code>Component</code> so that a component is
+free to override only those functionalities it needs.</p>
+
+<p>As seen in the picture, <code>AbstractComponent</code> serves as
+the superclass for several "real" components, but it also has a some
+abstract extensions. {@link com.vaadin.ui.AbstractComponentContainer}
+serves as the root class for all components (for example, panels and
+windows) who can contain other components. {@link
+com.vaadin.ui.AbstractField}, on the other hand, implements several
+interfaces to provide a base class for components that are used for data
+display and manipulation.</p>
+
+
+<!-- Package spec here -->
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/server/src/com/vaadin/ui/themes/BaseTheme.java b/server/src/com/vaadin/ui/themes/BaseTheme.java
new file mode 100644
index 0000000000..6f448746bf
--- /dev/null
+++ b/server/src/com/vaadin/ui/themes/BaseTheme.java
@@ -0,0 +1,59 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui.themes;
+
+/**
+ * <p>
+ * The Base theme is the foundation for all Vaadin themes. Although it is not
+ * necessary to use it as the starting point for all other themes, it is heavily
+ * encouraged, since it abstracts and hides away many necessary style properties
+ * that the Vaadin terminal expects and needs.
+ * </p>
+ * <p>
+ * When creating your own theme, either extend this class and specify the styles
+ * implemented in your theme here, or extend some other theme that has a class
+ * file specified (e.g. Reindeer or Runo).
+ * </p>
+ * <p>
+ * All theme class files should follow the convention of specifying the theme
+ * name as a string constant <code>THEME_NAME</code>.
+ *
+ * @since 6.3.0
+ *
+ */
+public class BaseTheme {
+
+ public static final String THEME_NAME = "base";
+
+ /**
+ * Creates a button that looks like a regular hypertext link but still acts
+ * like a normal button.
+ */
+ public static final String BUTTON_LINK = "link";
+
+ /**
+ * Removes extra decorations from the panel.
+ *
+ * @deprecated Base theme does not implement this style, but it is defined
+ * here since it has been a part of the framework before
+ * multiple themes were available. Use the constant provided by
+ * the theme you're using instead, e.g.
+ * {@link Reindeer#PANEL_LIGHT} or {@link Runo#PANEL_LIGHT}.
+ */
+ @Deprecated
+ public static final String PANEL_LIGHT = "light";
+
+ /**
+ * Adds the connector lines between a parent node and its child nodes to
+ * indicate the tree hierarchy better.
+ */
+ public static final String TREE_CONNECTORS = "connectors";
+
+ /**
+ * Clips the component so it will be constrained to its given size and not
+ * overflow.
+ */
+ public static final String CLIP = "v-clip";
+
+} \ No newline at end of file
diff --git a/server/src/com/vaadin/ui/themes/ChameleonTheme.java b/server/src/com/vaadin/ui/themes/ChameleonTheme.java
new file mode 100644
index 0000000000..5ae8cd4e57
--- /dev/null
+++ b/server/src/com/vaadin/ui/themes/ChameleonTheme.java
@@ -0,0 +1,365 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui.themes;
+
+public class ChameleonTheme extends BaseTheme {
+
+ public static final String THEME_NAME = "chameleon";
+
+ /***************************************************************************
+ * Label styles
+ **************************************************************************/
+
+ /**
+ * Large font for main application headings
+ */
+ public static final String LABEL_H1 = "h1";
+
+ /**
+ * Large font for different sections in the application
+ */
+ public static final String LABEL_H2 = "h2";
+
+ /**
+ * Font for sub-section headers
+ */
+ public static final String LABEL_H3 = "h3";
+
+ /**
+ * Font for paragraphs headers
+ */
+ public static final String LABEL_H4 = "h4";
+
+ /**
+ * Big font for important or emphasized texts
+ */
+ public static final String LABEL_BIG = "big";
+
+ /**
+ * Small and a little lighter font
+ */
+ public static final String LABEL_SMALL = "small";
+
+ /**
+ * Very small and lighter font for things such as footnotes and component
+ * specific informations. Use carefully, since this style will usually
+ * reduce legibility.
+ */
+ public static final String LABEL_TINY = "tiny";
+
+ /**
+ * Adds color to the text (usually the alternate color of the theme)
+ */
+ public static final String LABEL_COLOR = "color";
+
+ /**
+ * Adds a warning icon on the left side and a yellow background to the label
+ */
+ public static final String LABEL_WARNING = "warning";
+
+ /**
+ * Adds an error icon on the left side and a red background to the label
+ */
+ public static final String LABEL_ERROR = "error";
+
+ /**
+ * Adds a spinner icon on the left side of the label
+ */
+ public static final String LABEL_LOADING = "loading";
+
+ /***************************************************************************
+ * Button styles
+ **************************************************************************/
+
+ /**
+ * Default action style for buttons (the button that gets activated when
+ * user presses 'enter' in a form). Use sparingly, only one default button
+ * per screen should be visible.
+ */
+ public static final String BUTTON_DEFAULT = "default";
+
+ /**
+ * Small sized button, use for context specific actions for example
+ */
+ public static final String BUTTON_SMALL = "small";
+
+ /**
+ * Big button, use to get more attention for the button action
+ */
+ public static final String BUTTON_BIG = "big";
+
+ /**
+ * Adds more padding on the sides of the button. Makes it easier for the
+ * user to hit the button.
+ */
+ public static final String BUTTON_WIDE = "wide";
+
+ /**
+ * Adds more padding on the top and on the bottom of the button. Makes it
+ * easier for the user to hit the button.
+ */
+ public static final String BUTTON_TALL = "tall";
+
+ /**
+ * Removes all graphics from the button, leaving only the caption and the
+ * icon visible. Useful for making icon-only buttons and toolbar buttons.
+ */
+ public static final String BUTTON_BORDERLESS = "borderless";
+
+ /**
+ * Places the button icon on top of the caption. By default the icon is on
+ * the left side of the button caption.
+ */
+ public static final String BUTTON_ICON_ON_TOP = "icon-on-top";
+
+ /**
+ * Places the button icon on the right side of the caption. By default the
+ * icon is on the left side of the button caption.
+ */
+ public static final String BUTTON_ICON_ON_RIGHT = "icon-on-right";
+
+ /**
+ * Removes the button caption and only shows its icon
+ */
+ public static final String BUTTON_ICON_ONLY = "icon-only";
+
+ /**
+ * Makes the button look like it is pressed down. Useful for creating a
+ * toggle button.
+ */
+ public static final String BUTTON_DOWN = "down";
+
+ /***************************************************************************
+ * TextField styles
+ **************************************************************************/
+
+ /**
+ * Small sized text field with small font
+ */
+ public static final String TEXTFIELD_SMALL = "small";
+
+ /**
+ * Large sized text field with big font
+ */
+ public static final String TEXTFIELD_BIG = "big";
+
+ /**
+ * Adds a magnifier icon on the left side of the fields text
+ */
+ public static final String TEXTFIELD_SEARCH = "search";
+
+ /***************************************************************************
+ * Select styles
+ **************************************************************************/
+
+ /**
+ * Small sized select with small font
+ */
+ public static final String SELECT_SMALL = "small";
+
+ /**
+ * Large sized select with big font
+ */
+ public static final String SELECT_BIG = "big";
+
+ /**
+ * Adds a magnifier icon on the left side of the fields text
+ */
+ public static final String COMBOBOX_SEARCH = "search";
+
+ /**
+ * Adds a magnifier icon on the left side of the fields text
+ */
+ public static final String COMBOBOX_SELECT_BUTTON = "select-button";
+
+ /***************************************************************************
+ * DateField styles
+ **************************************************************************/
+
+ /**
+ * Small sized date field with small font
+ */
+ public static final String DATEFIELD_SMALL = "small";
+
+ /**
+ * Large sized date field with big font
+ */
+ public static final String DATEFIELD_BIG = "big";
+
+ /***************************************************************************
+ * Panel styles
+ **************************************************************************/
+
+ /**
+ * Removes borders and background color from the panel
+ */
+ public static final String PANEL_BORDERLESS = "borderless";
+
+ /**
+ * Adds a more vibrant header for the panel, using the alternate color of
+ * the theme, and adds slight rounded corners (not supported in all
+ * browsers)
+ */
+ public static final String PANEL_BUBBLE = "bubble";
+
+ /**
+ * Removes borders and background color from the panel
+ */
+ public static final String PANEL_LIGHT = "light";
+
+ /***************************************************************************
+ * SplitPanel styles
+ **************************************************************************/
+
+ /**
+ * Reduces the split handle to a minimal size (1 pixel)
+ */
+ public static final String SPLITPANEL_SMALL = "small";
+
+ /***************************************************************************
+ * TabSheet styles
+ **************************************************************************/
+
+ /**
+ * Removes borders and background color from the tab sheet
+ */
+ public static final String TABSHEET_BORDERLESS = "borderless";
+
+ /***************************************************************************
+ * Accordion styles
+ **************************************************************************/
+
+ /**
+ * Makes the accordion background opaque (non-transparent)
+ */
+ public static final String ACCORDION_OPAQUE = "opaque";
+
+ /***************************************************************************
+ * Table styles
+ **************************************************************************/
+
+ /**
+ * Removes borders and background color from the table
+ */
+ public static final String TABLE_BORDERLESS = "borderless";
+
+ /**
+ * Makes the column header and content font size smaller inside the table
+ */
+ public static final String TABLE_SMALL = "small";
+
+ /**
+ * Makes the column header and content font size bigger inside the table
+ */
+ public static final String TABLE_BIG = "big";
+
+ /**
+ * Adds a light alternate background color to even rows in the table.
+ */
+ public static final String TABLE_STRIPED = "striped";
+
+ /***************************************************************************
+ * ProgressIndicator styles
+ **************************************************************************/
+
+ /**
+ * Reduces the height of the progress bar
+ */
+ public static final String PROGRESS_INDICATOR_SMALL = "small";
+
+ /**
+ * Increases the height of the progress bar. If the indicator is in
+ * indeterminate mode, shows a bigger spinner than the regular indeterminate
+ * indicator.
+ */
+ public static final String PROGRESS_INDICATOR_BIG = "big";
+
+ /**
+ * Displays an indeterminate progress indicator as a bar with animated
+ * background stripes. This style can be used in combination with the
+ * "small" and "big" styles.
+ */
+ public static final String PROGRESS_INDICATOR_INDETERMINATE_BAR = "bar";
+
+ /***************************************************************************
+ * Window styles
+ **************************************************************************/
+
+ /**
+ * Sub-window style that makes the window background opaque (i.e. not
+ * semi-transparent).
+ */
+ public static final String WINDOW_OPAQUE = "opaque";
+
+ /***************************************************************************
+ * Compound styles
+ **************************************************************************/
+
+ /**
+ * Creates a context for a segment button control. Place buttons inside the
+ * segment, and add "<code>first</code>" and "<code>last</code>" style names
+ * for the first and last button in the segment. Then use the
+ * {@link #BUTTON_DOWN} style to indicate button states.
+ *
+ * E.g.
+ *
+ * <pre>
+ * HorizontalLayout ("segment")
+ * + Button ("first down")
+ * + Button ("down")
+ * + Button
+ * ...
+ * + Button ("last")
+ * </pre>
+ *
+ * You can also use most of the different button styles for the contained
+ * buttons (e.g. {@link #BUTTON_BIG}, {@link #BUTTON_ICON_ONLY} etc.).
+ */
+ public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT = "segment";
+
+ /**
+ * Use this mixin-style in combination with the
+ * {@link #COMPOUND_HORIZONTAL_LAYOUT_SEGMENT} style to make buttons with
+ * the "down" style use the themes alternate color (e.g. blue instead of
+ * gray).
+ *
+ * E.g.
+ *
+ * <pre>
+ * HorizontalLayout ("segment segment-alternate")
+ * + Button ("first down")
+ * + Button ("down")
+ * + Button
+ * ...
+ * + Button ("last")
+ * </pre>
+ */
+ public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT_ALTERNATE = "segment-alternate";
+
+ /**
+ * Creates an iTunes-like menu from a CssLayout or a VerticalLayout. Place
+ * plain Labels and NativeButtons inside the layout, and you're all set.
+ *
+ * E.g.
+ *
+ * <pre>
+ * CssLayout ("sidebar-menu")
+ * + Label
+ * + NativeButton
+ * + NativeButton
+ * ...
+ * + Label
+ * + NativeButton
+ * </pre>
+ */
+ public static final String COMPOUND_LAYOUT_SIDEBAR_MENU = "sidebar-menu";
+
+ /**
+ * Adds a toolbar-like background for the layout, and aligns Buttons and
+ * Segments horizontally. Feel free to use different buttons styles inside
+ * the toolbar, like {@link #BUTTON_ICON_ON_TOP} and
+ * {@link #BUTTON_BORDERLESS}
+ */
+ public static final String COMPOUND_CSSLAYOUT_TOOLBAR = "toolbar";
+}
diff --git a/server/src/com/vaadin/ui/themes/LiferayTheme.java b/server/src/com/vaadin/ui/themes/LiferayTheme.java
new file mode 100644
index 0000000000..9b48306ac2
--- /dev/null
+++ b/server/src/com/vaadin/ui/themes/LiferayTheme.java
@@ -0,0 +1,31 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui.themes;
+
+public class LiferayTheme extends BaseTheme {
+
+ public static final String THEME_NAME = "liferay";
+
+ /***************************************************************************
+ *
+ * Panel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Removes borders and background from the panel
+ */
+ public static final String PANEL_LIGHT = "light";
+
+ /***************************************************************************
+ *
+ * SplitPanel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Reduces the split handle to a minimal size (1 pixel)
+ */
+ public static final String SPLITPANEL_SMALL = "small";
+}
diff --git a/server/src/com/vaadin/ui/themes/Reindeer.java b/server/src/com/vaadin/ui/themes/Reindeer.java
new file mode 100644
index 0000000000..7aaae8faa2
--- /dev/null
+++ b/server/src/com/vaadin/ui/themes/Reindeer.java
@@ -0,0 +1,217 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui.themes;
+
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.FormLayout;
+import com.vaadin.ui.GridLayout;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.HorizontalSplitPanel;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.VerticalSplitPanel;
+
+public class Reindeer extends BaseTheme {
+
+ public static final String THEME_NAME = "reindeer";
+
+ /***************************************************************************
+ *
+ * Label styles
+ *
+ **************************************************************************/
+
+ /**
+ * Large font for main application headings
+ */
+ public static final String LABEL_H1 = "h1";
+
+ /**
+ * Large font for different sections in the application
+ */
+ public static final String LABEL_H2 = "h2";
+
+ /**
+ * Small and a little lighter font
+ */
+ public static final String LABEL_SMALL = "light";
+
+ /**
+ * @deprecated Use {@link #LABEL_SMALL} instead.
+ */
+ @Deprecated
+ public static final String LABEL_LIGHT = "small";
+
+ /***************************************************************************
+ *
+ * Button styles
+ *
+ **************************************************************************/
+
+ /**
+ * Default action style for buttons (the button that should get activated
+ * when the user presses 'enter' in a form). Use sparingly, only one default
+ * button per view should be visible.
+ */
+ public static final String BUTTON_DEFAULT = "primary";
+
+ /**
+ * @deprecated Use {@link #BUTTON_DEFAULT} instead
+ */
+ @Deprecated
+ public static final String BUTTON_PRIMARY = BUTTON_DEFAULT;
+
+ /**
+ * Small sized button, use for context specific actions for example
+ */
+ public static final String BUTTON_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * TextField styles
+ *
+ **************************************************************************/
+
+ /**
+ * Small sized text field with small font
+ */
+ public static final String TEXTFIELD_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * Panel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Removes borders and background color from the panel
+ */
+ public static final String PANEL_LIGHT = "light";
+
+ /***************************************************************************
+ *
+ * SplitPanel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Reduces the split handle to a minimal size (1 pixel)
+ */
+ public static final String SPLITPANEL_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * TabSheet styles
+ *
+ **************************************************************************/
+
+ /**
+ * Removes borders from the default tab sheet style.
+ */
+ public static final String TABSHEET_BORDERLESS = "borderless";
+
+ /**
+ * Removes borders and background color from the tab sheet, and shows the
+ * tabs as a small bar.
+ */
+ public static final String TABSHEET_SMALL = "bar";
+
+ /**
+ * @deprecated Use {@link #TABSHEET_SMALL} instead.
+ */
+ @Deprecated
+ public static final String TABSHEET_BAR = TABSHEET_SMALL;
+
+ /**
+ * Removes borders and background color from the tab sheet. The tabs are
+ * presented with minimal lines indicating the selected tab.
+ */
+ public static final String TABSHEET_MINIMAL = "minimal";
+
+ /**
+ * Makes the tab close buttons visible only when the user is hovering over
+ * the tab.
+ */
+ public static final String TABSHEET_HOVER_CLOSABLE = "hover-closable";
+
+ /**
+ * Makes the tab close buttons visible only when the tab is selected.
+ */
+ public static final String TABSHEET_SELECTED_CLOSABLE = "selected-closable";
+
+ /***************************************************************************
+ *
+ * Table styles
+ *
+ **************************************************************************/
+
+ /**
+ * Removes borders from the table
+ */
+ public static final String TABLE_BORDERLESS = "borderless";
+
+ /**
+ * Makes the table headers dark and more prominent.
+ */
+ public static final String TABLE_STRONG = "strong";
+
+ /***************************************************************************
+ *
+ * Layout styles
+ *
+ **************************************************************************/
+
+ /**
+ * Changes the background of a layout to white. Applies to
+ * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout},
+ * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and
+ * {@link HorizontalSplitPanel}.
+ * <p>
+ * <em>Does not revert any contained components back to normal if some
+ * parent layout has style {@link #LAYOUT_BLACK} applied.</em>
+ */
+ public static final String LAYOUT_WHITE = "white";
+
+ /**
+ * Changes the background of a layout to a shade of blue. Applies to
+ * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout},
+ * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and
+ * {@link HorizontalSplitPanel}.
+ * <p>
+ * <em>Does not revert any contained components back to normal if some
+ * parent layout has style {@link #LAYOUT_BLACK} applied.</em>
+ */
+ public static final String LAYOUT_BLUE = "blue";
+
+ /**
+ * <p>
+ * Changes the background of a layout to almost black, and at the same time
+ * transforms contained components to their black style correspondents when
+ * available. At least texts, buttons, text fields, selects, date fields,
+ * tables and a few other component styles should change.
+ * </p>
+ * <p>
+ * Applies to {@link VerticalLayout}, {@link HorizontalLayout},
+ * {@link GridLayout}, {@link FormLayout} and {@link CssLayout}.
+ * </p>
+ *
+ */
+ public static final String LAYOUT_BLACK = "black";
+
+ /***************************************************************************
+ *
+ * Window styles
+ *
+ **************************************************************************/
+
+ /**
+ * Makes the whole window white and increases the font size of the title.
+ */
+ public static final String WINDOW_LIGHT = "light";
+
+ /**
+ * Makes the whole window black, and changes contained components in the
+ * same way as {@link #LAYOUT_BLACK} does.
+ */
+ public static final String WINDOW_BLACK = "black";
+}
diff --git a/server/src/com/vaadin/ui/themes/Runo.java b/server/src/com/vaadin/ui/themes/Runo.java
new file mode 100644
index 0000000000..28a19e8dcd
--- /dev/null
+++ b/server/src/com/vaadin/ui/themes/Runo.java
@@ -0,0 +1,183 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.ui.themes;
+
+public class Runo extends BaseTheme {
+
+ public static final String THEME_NAME = "runo";
+
+ public static String themeName() {
+ return THEME_NAME.toLowerCase();
+ }
+
+ /***************************************************************************
+ *
+ * Button styles
+ *
+ **************************************************************************/
+
+ /**
+ * Small sized button, use for context specific actions for example
+ */
+ public static final String BUTTON_SMALL = "small";
+
+ /**
+ * Big sized button, use to gather much attention for some particular action
+ */
+ public static final String BUTTON_BIG = "big";
+
+ /**
+ * Default action style for buttons (the button that should get activated
+ * when the user presses 'enter' in a form). Use sparingly, only one default
+ * button per view should be visible.
+ */
+ public static final String BUTTON_DEFAULT = "default";
+
+ /***************************************************************************
+ *
+ * Panel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Removes borders and background color from the panel
+ */
+ public static final String PANEL_LIGHT = "light";
+
+ /***************************************************************************
+ *
+ * TabSheet styles
+ *
+ **************************************************************************/
+
+ /**
+ * Smaller tabs, no border and background for content area
+ */
+ public static final String TABSHEET_SMALL = "light";
+
+ /***************************************************************************
+ *
+ * SplitPanel styles
+ *
+ **************************************************************************/
+
+ /**
+ * Reduces the width/height of the split handle. Useful when you don't want
+ * the split handle to touch the sides of the containing layout.
+ */
+ public static final String SPLITPANEL_REDUCED = "rounded";
+
+ /**
+ * Reduces the visual size of the split handle to one pixel (the active drag
+ * size is still larger).
+ */
+ public static final String SPLITPANEL_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * Label styles
+ *
+ **************************************************************************/
+
+ /**
+ * Largest title/header size. Use for main sections in your application.
+ */
+ public static final String LABEL_H1 = "h1";
+
+ /**
+ * Similar style as in panel captions. Useful for sub-sections within a
+ * view.
+ */
+ public static final String LABEL_H2 = "h2";
+
+ /**
+ * Small font size. Useful for contextual help texts and similar less
+ * frequently needed information. Use with modesty, since this style will be
+ * more harder to read due to its smaller size and contrast.
+ */
+ public static final String LABEL_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * Layout styles
+ *
+ **************************************************************************/
+
+ /**
+ * An alternative background color for layouts. Use on top of white
+ * background (e.g. inside Panels, TabSheets and sub-windows).
+ */
+ public static final String LAYOUT_DARKER = "darker";
+
+ /**
+ * Add a drop shadow around the layout and its contained components.
+ * Produces a rectangular shadow, even if the contained component would have
+ * a different shape.
+ * <p>
+ * Note: does not work in Internet Explorer 6
+ */
+ public static final String CSSLAYOUT_SHADOW = "box-shadow";
+
+ /**
+ * Adds necessary styles to the layout to make it look selectable (i.e.
+ * clickable). Add a click listener for the layout, and toggle the
+ * {@link #CSSLAYOUT_SELECTABLE_SELECTED} style for the same layout to make
+ * it look selected or not.
+ */
+ public static final String CSSLAYOUT_SELECTABLE = "selectable";
+ public static final String CSSLAYOUT_SELECTABLE_SELECTED = "selectable-selected";
+
+ /***************************************************************************
+ *
+ * TextField styles
+ *
+ **************************************************************************/
+
+ /**
+ * Small sized text field with small font
+ */
+ public static final String TEXTFIELD_SMALL = "small";
+
+ /***************************************************************************
+ *
+ * Table styles
+ *
+ **************************************************************************/
+
+ /**
+ * Smaller header and item fonts.
+ */
+ public static final String TABLE_SMALL = "small";
+
+ /**
+ * Removes the border and background color from the table. Removes
+ * alternating row background colors as well.
+ */
+ public static final String TABLE_BORDERLESS = "borderless";
+
+ /***************************************************************************
+ *
+ * Accordion styles
+ *
+ **************************************************************************/
+
+ /**
+ * A detached looking accordion, providing space around its captions and
+ * content. Doesn't necessarily need a Panel or other container to wrap it
+ * in order to make it look right.
+ */
+ public static final String ACCORDION_LIGHT = "light";
+
+ /***************************************************************************
+ *
+ * Window styles
+ *
+ **************************************************************************/
+
+ /**
+ * Smaller header and a darker background color for the window. Useful for
+ * smaller dialog-like windows.
+ */
+ public static final String WINDOW_DIALOG = "dialog";
+}
diff --git a/server/src/com/vaadin/util/SerializerHelper.java b/server/src/com/vaadin/util/SerializerHelper.java
new file mode 100644
index 0000000000..5b7b388dd6
--- /dev/null
+++ b/server/src/com/vaadin/util/SerializerHelper.java
@@ -0,0 +1,145 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+package com.vaadin.util;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+/**
+ * Helper class for performing serialization. Most of the methods are here are
+ * workarounds for problems in Google App Engine. Used internally by Vaadin and
+ * should not be used by application developers. Subject to change at any time.
+ *
+ * @since 6.0
+ */
+public class SerializerHelper {
+
+ /**
+ * Serializes the class reference so {@link #readClass(ObjectInputStream)}
+ * can deserialize it. Supports null class references.
+ *
+ * @param out
+ * The {@link ObjectOutputStream} to serialize to.
+ * @param cls
+ * A class or null.
+ * @throws IOException
+ * Rethrows any IOExceptions from the ObjectOutputStream
+ */
+ public static void writeClass(ObjectOutputStream out, Class<?> cls)
+ throws IOException {
+ if (cls == null) {
+ out.writeObject(null);
+ } else {
+ out.writeObject(cls.getName());
+ }
+
+ }
+
+ /**
+ * Serializes the class references so
+ * {@link #readClassArray(ObjectInputStream)} can deserialize it. Supports
+ * null class arrays.
+ *
+ * @param out
+ * The {@link ObjectOutputStream} to serialize to.
+ * @param classes
+ * An array containing class references or null.
+ * @throws IOException
+ * Rethrows any IOExceptions from the ObjectOutputStream
+ */
+ public static void writeClassArray(ObjectOutputStream out,
+ Class<?>[] classes) throws IOException {
+ if (classes == null) {
+ out.writeObject(null);
+ } else {
+ String[] classNames = new String[classes.length];
+ for (int i = 0; i < classes.length; i++) {
+ classNames[i] = classes[i].getName();
+ }
+ out.writeObject(classNames);
+ }
+ }
+
+ /**
+ * Deserializes a class references serialized by
+ * {@link #writeClassArray(ObjectOutputStream, Class[])}. Supports null
+ * class arrays.
+ *
+ * @param in
+ * {@link ObjectInputStream} to read from.
+ * @return Class array with the class references or null.
+ * @throws ClassNotFoundException
+ * If one of the classes could not be resolved.
+ * @throws IOException
+ * Rethrows IOExceptions from the ObjectInputStream
+ */
+ public static Class<?>[] readClassArray(ObjectInputStream in)
+ throws ClassNotFoundException, IOException {
+ String[] classNames = (String[]) in.readObject();
+ if (classNames == null) {
+ return null;
+ }
+ Class<?>[] classes = new Class<?>[classNames.length];
+ for (int i = 0; i < classNames.length; i++) {
+ classes[i] = resolveClass(classNames[i]);
+ }
+
+ return classes;
+ }
+
+ /**
+ * List of primitive classes. Google App Engine has problems
+ * serializing/deserializing these (#3064).
+ */
+ private static Class<?>[] primitiveClasses = new Class<?>[] { byte.class,
+ short.class, int.class, long.class, float.class, double.class,
+ boolean.class, char.class };
+
+ /**
+ * Resolves the class given by {@code className}.
+ *
+ * @param className
+ * The fully qualified class name.
+ * @return A {@code Class} reference.
+ * @throws ClassNotFoundException
+ * If the class could not be resolved.
+ */
+ public static Class<?> resolveClass(String className)
+ throws ClassNotFoundException {
+ for (Class<?> c : primitiveClasses) {
+ if (className.equals(c.getName())) {
+ return c;
+ }
+ }
+
+ return Class.forName(className);
+ }
+
+ /**
+ * Deserializes a class reference serialized by
+ * {@link #writeClass(ObjectOutputStream, Class)}. Supports null class
+ * references.
+ *
+ * @param in
+ * {@code ObjectInputStream} to read from.
+ * @return Class reference to the resolved class
+ * @throws ClassNotFoundException
+ * If the class could not be resolved.
+ * @throws IOException
+ * Rethrows IOExceptions from the ObjectInputStream
+ */
+ public static Class<?> readClass(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ String className = (String) in.readObject();
+ if (className == null) {
+ return null;
+ } else {
+ return resolveClass(className);
+
+ }
+
+ }
+
+}
diff --git a/server/src/org/jsoup/Connection.java b/server/src/org/jsoup/Connection.java
new file mode 100644
index 0000000000..564eeb89b7
--- /dev/null
+++ b/server/src/org/jsoup/Connection.java
@@ -0,0 +1,481 @@
+package org.jsoup;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.parser.Parser;
+
+import java.net.URL;
+import java.util.Map;
+import java.util.Collection;
+import java.io.IOException;
+
+/**
+ * A Connection provides a convenient interface to fetch content from the web, and parse them into Documents.
+ * <p>
+ * To get a new Connection, use {@link org.jsoup.Jsoup#connect(String)}. Connections contain {@link Connection.Request}
+ * and {@link Connection.Response} objects. The request objects are reusable as prototype requests.
+ * <p>
+ * Request configuration can be made using either the shortcut methods in Connection (e.g. {@link #userAgent(String)}),
+ * or by methods in the Connection.Request object directly. All request configuration must be made before the request
+ * is executed.
+ * <p>
+ * The Connection interface is <b>currently in beta</b> and subject to change. Comments, suggestions, and bug reports are welcome.
+ */
+public interface Connection {
+
+ /**
+ * GET and POST http methods.
+ */
+ public enum Method {
+ GET, POST
+ }
+
+ /**
+ * Set the request URL to fetch. The protocol must be HTTP or HTTPS.
+ * @param url URL to connect to
+ * @return this Connection, for chaining
+ */
+ public Connection url(URL url);
+
+ /**
+ * Set the request URL to fetch. The protocol must be HTTP or HTTPS.
+ * @param url URL to connect to
+ * @return this Connection, for chaining
+ */
+ public Connection url(String url);
+
+ /**
+ * Set the request user-agent header.
+ * @param userAgent user-agent to use
+ * @return this Connection, for chaining
+ */
+ public Connection userAgent(String userAgent);
+
+ /**
+ * Set the request timeouts (connect and read). If a timeout occurs, an IOException will be thrown. The default
+ * timeout is 3 seconds (3000 millis). A timeout of zero is treated as an infinite timeout.
+ * @param millis number of milliseconds (thousandths of a second) before timing out connects or reads.
+ * @return this Connection, for chaining
+ */
+ public Connection timeout(int millis);
+
+ /**
+ * Set the request referrer (aka "referer") header.
+ * @param referrer referrer to use
+ * @return this Connection, for chaining
+ */
+ public Connection referrer(String referrer);
+
+ /**
+ * Configures the connection to (not) follow server redirects. By default this is <b>true</b>.
+ * @param followRedirects true if server redirects should be followed.
+ * @return this Connection, for chaining
+ */
+ public Connection followRedirects(boolean followRedirects);
+
+ /**
+ * Set the request method to use, GET or POST. Default is GET.
+ * @param method HTTP request method
+ * @return this Connection, for chaining
+ */
+ public Connection method(Method method);
+
+ /**
+ * Configures the connection to not throw exceptions when a HTTP error occurs. (4xx - 5xx, e.g. 404 or 500). By
+ * default this is <b>false</b>; an IOException is thrown if an error is encountered. If set to <b>true</b>, the
+ * response is populated with the error body, and the status message will reflect the error.
+ * @param ignoreHttpErrors - false (default) if HTTP errors should be ignored.
+ * @return this Connection, for chaining
+ */
+ public Connection ignoreHttpErrors(boolean ignoreHttpErrors);
+
+ /**
+ * Ignore the document's Content-Type when parsing the response. By default this is <b>false</b>, an unrecognised
+ * content-type will cause an IOException to be thrown. (This is to prevent producing garbage by attempting to parse
+ * a JPEG binary image, for example.) Set to true to force a parse attempt regardless of content type.
+ * @param ignoreContentType set to true if you would like the content type ignored on parsing the response into a
+ * Document.
+ * @return this Connection, for chaining
+ */
+ public Connection ignoreContentType(boolean ignoreContentType);
+
+ /**
+ * Add a request data parameter. Request parameters are sent in the request query string for GETs, and in the request
+ * body for POSTs. A request may have multiple values of the same name.
+ * @param key data key
+ * @param value data value
+ * @return this Connection, for chaining
+ */
+ public Connection data(String key, String value);
+
+ /**
+ * Adds all of the supplied data to the request data parameters
+ * @param data map of data parameters
+ * @return this Connection, for chaining
+ */
+ public Connection data(Map<String, String> data);
+
+ /**
+ * Add a number of request data parameters. Multiple parameters may be set at once, e.g.:
+ * <code>.data("name", "jsoup", "language", "Java", "language", "English");</code> creates a query string like:
+ * <code>?name=jsoup&language=Java&language=English</code>
+ * @param keyvals a set of key value pairs.
+ * @return this Connection, for chaining
+ */
+ public Connection data(String... keyvals);
+
+ /**
+ * Set a request header.
+ * @param name header name
+ * @param value header value
+ * @return this Connection, for chaining
+ * @see org.jsoup.Connection.Request#headers()
+ */
+ public Connection header(String name, String value);
+
+ /**
+ * Set a cookie to be sent in the request.
+ * @param name name of cookie
+ * @param value value of cookie
+ * @return this Connection, for chaining
+ */
+ public Connection cookie(String name, String value);
+
+ /**
+ * Adds each of the supplied cookies to the request.
+ * @param cookies map of cookie name -> value pairs
+ * @return this Connection, for chaining
+ */
+ public Connection cookies(Map<String, String> cookies);
+
+ /**
+ * Provide an alternate parser to use when parsing the response to a Document.
+ * @param parser alternate parser
+ * @return this Connection, for chaining
+ */
+ public Connection parser(Parser parser);
+
+ /**
+ * Execute the request as a GET, and parse the result.
+ * @return parsed Document
+ * @throws IOException on error
+ */
+ public Document get() throws IOException;
+
+ /**
+ * Execute the request as a POST, and parse the result.
+ * @return parsed Document
+ * @throws IOException on error
+ */
+ public Document post() throws IOException;
+
+ /**
+ * Execute the request.
+ * @return a response object
+ * @throws IOException on error
+ */
+ public Response execute() throws IOException;
+
+ /**
+ * Get the request object associated with this connection
+ * @return request
+ */
+ public Request request();
+
+ /**
+ * Set the connection's request
+ * @param request new request object
+ * @return this Connection, for chaining
+ */
+ public Connection request(Request request);
+
+ /**
+ * Get the response, once the request has been executed
+ * @return response
+ */
+ public Response response();
+
+ /**
+ * Set the connection's response
+ * @param response new response
+ * @return this Connection, for chaining
+ */
+ public Connection response(Response response);
+
+
+ /**
+ * Common methods for Requests and Responses
+ * @param <T> Type of Base, either Request or Response
+ */
+ interface Base<T extends Base> {
+
+ /**
+ * Get the URL
+ * @return URL
+ */
+ public URL url();
+
+ /**
+ * Set the URL
+ * @param url new URL
+ * @return this, for chaining
+ */
+ public T url(URL url);
+
+ /**
+ * Get the request method
+ * @return method
+ */
+ public Method method();
+
+ /**
+ * Set the request method
+ * @param method new method
+ * @return this, for chaining
+ */
+ public T method(Method method);
+
+ /**
+ * Get the value of a header. This is a simplified header model, where a header may only have one value.
+ * <p>
+ * Header names are case insensitive.
+ * @param name name of header (case insensitive)
+ * @return value of header, or null if not set.
+ * @see #hasHeader(String)
+ * @see #cookie(String)
+ */
+ public String header(String name);
+
+ /**
+ * Set a header. This method will overwrite any existing header with the same case insensitive name.
+ * @param name Name of header
+ * @param value Value of header
+ * @return this, for chaining
+ */
+ public T header(String name, String value);
+
+ /**
+ * Check if a header is present
+ * @param name name of header (case insensitive)
+ * @return if the header is present in this request/response
+ */
+ public boolean hasHeader(String name);
+
+ /**
+ * Remove a header by name
+ * @param name name of header to remove (case insensitive)
+ * @return this, for chaining
+ */
+ public T removeHeader(String name);
+
+ /**
+ * Retrieve all of the request/response headers as a map
+ * @return headers
+ */
+ public Map<String, String> headers();
+
+ /**
+ * Get a cookie value by name from this request/response.
+ * <p>
+ * Response objects have a simplified cookie model. Each cookie set in the response is added to the response
+ * object's cookie key=value map. The cookie's path, domain, and expiry date are ignored.
+ * @param name name of cookie to retrieve.
+ * @return value of cookie, or null if not set
+ */
+ public String cookie(String name);
+
+ /**
+ * Set a cookie in this request/response.
+ * @param name name of cookie
+ * @param value value of cookie
+ * @return this, for chaining
+ */
+ public T cookie(String name, String value);
+
+ /**
+ * Check if a cookie is present
+ * @param name name of cookie
+ * @return if the cookie is present in this request/response
+ */
+ public boolean hasCookie(String name);
+
+ /**
+ * Remove a cookie by name
+ * @param name name of cookie to remove
+ * @return this, for chaining
+ */
+ public T removeCookie(String name);
+
+ /**
+ * Retrieve all of the request/response cookies as a map
+ * @return cookies
+ */
+ public Map<String, String> cookies();
+
+ }
+
+ /**
+ * Represents a HTTP request.
+ */
+ public interface Request extends Base<Request> {
+ /**
+ * Get the request timeout, in milliseconds.
+ * @return the timeout in milliseconds.
+ */
+ public int timeout();
+
+ /**
+ * Update the request timeout.
+ * @param millis timeout, in milliseconds
+ * @return this Request, for chaining
+ */
+ public Request timeout(int millis);
+
+ /**
+ * Get the current followRedirects configuration.
+ * @return true if followRedirects is enabled.
+ */
+ public boolean followRedirects();
+
+ /**
+ * Configures the request to (not) follow server redirects. By default this is <b>true</b>.
+ *
+ * @param followRedirects true if server redirects should be followed.
+ * @return this Request, for chaining
+ */
+ public Request followRedirects(boolean followRedirects);
+
+ /**
+ * Get the current ignoreHttpErrors configuration.
+ * @return true if errors will be ignored; false (default) if HTTP errors will cause an IOException to be thrown.
+ */
+ public boolean ignoreHttpErrors();
+
+ /**
+ * Configures the request to ignore HTTP errors in the response.
+ * @param ignoreHttpErrors set to true to ignore HTTP errors.
+ * @return this Request, for chaining
+ */
+ public Request ignoreHttpErrors(boolean ignoreHttpErrors);
+
+ /**
+ * Get the current ignoreContentType configuration.
+ * @return true if invalid content-types will be ignored; false (default) if they will cause an IOException to be thrown.
+ */
+ public boolean ignoreContentType();
+
+ /**
+ * Configures the request to ignore the Content-Type of the response.
+ * @param ignoreContentType set to true to ignore the content type.
+ * @return this Request, for chaining
+ */
+ public Request ignoreContentType(boolean ignoreContentType);
+
+ /**
+ * Add a data parameter to the request
+ * @param keyval data to add.
+ * @return this Request, for chaining
+ */
+ public Request data(KeyVal keyval);
+
+ /**
+ * Get all of the request's data parameters
+ * @return collection of keyvals
+ */
+ public Collection<KeyVal> data();
+
+ /**
+ * Specify the parser to use when parsing the document.
+ * @param parser parser to use.
+ * @return this Request, for chaining
+ */
+ public Request parser(Parser parser);
+
+ /**
+ * Get the current parser to use when parsing the document.
+ * @return current Parser
+ */
+ public Parser parser();
+ }
+
+ /**
+ * Represents a HTTP response.
+ */
+ public interface Response extends Base<Response> {
+
+ /**
+ * Get the status code of the response.
+ * @return status code
+ */
+ public int statusCode();
+
+ /**
+ * Get the status message of the response.
+ * @return status message
+ */
+ public String statusMessage();
+
+ /**
+ * Get the character set name of the response.
+ * @return character set name
+ */
+ public String charset();
+
+ /**
+ * Get the response content type (e.g. "text/html");
+ * @return the response content type
+ */
+ public String contentType();
+
+ /**
+ * Parse the body of the response as a Document.
+ * @return a parsed Document
+ * @throws IOException on error
+ */
+ public Document parse() throws IOException;
+
+ /**
+ * Get the body of the response as a plain string.
+ * @return body
+ */
+ public String body();
+
+ /**
+ * Get the body of the response as an array of bytes.
+ * @return body bytes
+ */
+ public byte[] bodyAsBytes();
+ }
+
+ /**
+ * A Key Value tuple.
+ */
+ public interface KeyVal {
+
+ /**
+ * Update the key of a keyval
+ * @param key new key
+ * @return this KeyVal, for chaining
+ */
+ public KeyVal key(String key);
+
+ /**
+ * Get the key of a keyval
+ * @return the key
+ */
+ public String key();
+
+ /**
+ * Update the value of a keyval
+ * @param value the new value
+ * @return this KeyVal, for chaining
+ */
+ public KeyVal value(String value);
+
+ /**
+ * Get the value of a keyval
+ * @return the value
+ */
+ public String value();
+ }
+}
+
diff --git a/server/src/org/jsoup/Jsoup.java b/server/src/org/jsoup/Jsoup.java
new file mode 100644
index 0000000000..8c6afcee36
--- /dev/null
+++ b/server/src/org/jsoup/Jsoup.java
@@ -0,0 +1,229 @@
+package org.jsoup;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.parser.Parser;
+import org.jsoup.safety.Cleaner;
+import org.jsoup.safety.Whitelist;
+import org.jsoup.helper.DataUtil;
+import org.jsoup.helper.HttpConnection;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ The core public access point to the jsoup functionality.
+
+ @author Jonathan Hedley */
+public class Jsoup {
+ private Jsoup() {}
+
+ /**
+ Parse HTML into a Document. The parser will make a sensible, balanced document tree out of any HTML.
+
+ @param html HTML to parse
+ @param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
+ before the HTML declares a {@code <base href>} tag.
+ @return sane HTML
+ */
+ public static Document parse(String html, String baseUri) {
+ return Parser.parse(html, baseUri);
+ }
+
+ /**
+ Parse HTML into a Document, using the provided Parser. You can provide an alternate parser, such as a simple XML
+ (non-HTML) parser.
+
+ @param html HTML to parse
+ @param baseUri The URL where the HTML was retrieved from. Used to resolve relative URLs to absolute URLs, that occur
+ before the HTML declares a {@code <base href>} tag.
+ @param parser alternate {@link Parser#xmlParser() parser} to use.
+ @return sane HTML
+ */
+ public static Document parse(String html, String baseUri, Parser parser) {
+ return parser.parseInput(html, baseUri);
+ }
+
+ /**
+ Parse HTML into a Document. As no base URI is specified, absolute URL detection relies on the HTML including a
+ {@code <base href>} tag.
+
+ @param html HTML to parse
+ @return sane HTML
+
+ @see #parse(String, String)
+ */
+ public static Document parse(String html) {
+ return Parser.parse(html, "");
+ }
+
+ /**
+ * Creates a new {@link Connection} to a URL. Use to fetch and parse a HTML page.
+ * <p>
+ * Use examples:
+ * <ul>
+ * <li><code>Document doc = Jsoup.connect("http://example.com").userAgent("Mozilla").data("name", "jsoup").get();</code></li>
+ * <li><code>Document doc = Jsoup.connect("http://example.com").cookie("auth", "token").post();
+ * </ul>
+ * @param url URL to connect to. The protocol must be {@code http} or {@code https}.
+ * @return the connection. You can add data, cookies, and headers; set the user-agent, referrer, method; and then execute.
+ */
+ public static Connection connect(String url) {
+ return HttpConnection.connect(url);
+ }
+
+ /**
+ Parse the contents of a file as HTML.
+
+ @param in file to load HTML from
+ @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
+ present, or fall back to {@code UTF-8} (which is often safe to do).
+ @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
+ @return sane HTML
+
+ @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
+ */
+ public static Document parse(File in, String charsetName, String baseUri) throws IOException {
+ return DataUtil.load(in, charsetName, baseUri);
+ }
+
+ /**
+ Parse the contents of a file as HTML. The location of the file is used as the base URI to qualify relative URLs.
+
+ @param in file to load HTML from
+ @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
+ present, or fall back to {@code UTF-8} (which is often safe to do).
+ @return sane HTML
+
+ @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
+ @see #parse(File, String, String)
+ */
+ public static Document parse(File in, String charsetName) throws IOException {
+ return DataUtil.load(in, charsetName, in.getAbsolutePath());
+ }
+
+ /**
+ Read an input stream, and parse it to a Document.
+
+ @param in input stream to read. Make sure to close it after parsing.
+ @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
+ present, or fall back to {@code UTF-8} (which is often safe to do).
+ @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
+ @return sane HTML
+
+ @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
+ */
+ public static Document parse(InputStream in, String charsetName, String baseUri) throws IOException {
+ return DataUtil.load(in, charsetName, baseUri);
+ }
+
+ /**
+ Read an input stream, and parse it to a Document. You can provide an alternate parser, such as a simple XML
+ (non-HTML) parser.
+
+ @param in input stream to read. Make sure to close it after parsing.
+ @param charsetName (optional) character set of file contents. Set to {@code null} to determine from {@code http-equiv} meta tag, if
+ present, or fall back to {@code UTF-8} (which is often safe to do).
+ @param baseUri The URL where the HTML was retrieved from, to resolve relative links against.
+ @param parser alternate {@link Parser#xmlParser() parser} to use.
+ @return sane HTML
+
+ @throws IOException if the file could not be found, or read, or if the charsetName is invalid.
+ */
+ public static Document parse(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {
+ return DataUtil.load(in, charsetName, baseUri, parser);
+ }
+
+ /**
+ Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML.
+
+ @param bodyHtml body HTML fragment
+ @param baseUri URL to resolve relative URLs against.
+ @return sane HTML document
+
+ @see Document#body()
+ */
+ public static Document parseBodyFragment(String bodyHtml, String baseUri) {
+ return Parser.parseBodyFragment(bodyHtml, baseUri);
+ }
+
+ /**
+ Parse a fragment of HTML, with the assumption that it forms the {@code body} of the HTML.
+
+ @param bodyHtml body HTML fragment
+ @return sane HTML document
+
+ @see Document#body()
+ */
+ public static Document parseBodyFragment(String bodyHtml) {
+ return Parser.parseBodyFragment(bodyHtml, "");
+ }
+
+ /**
+ Fetch a URL, and parse it as HTML. Provided for compatibility; in most cases use {@link #connect(String)} instead.
+ <p>
+ The encoding character set is determined by the content-type header or http-equiv meta tag, or falls back to {@code UTF-8}.
+
+ @param url URL to fetch (with a GET). The protocol must be {@code http} or {@code https}.
+ @param timeoutMillis Connection and read timeout, in milliseconds. If exceeded, IOException is thrown.
+ @return The parsed HTML.
+
+ @throws IOException If the final server response != 200 OK (redirects are followed), or if there's an error reading
+ the response stream.
+
+ @see #connect(String)
+ */
+ public static Document parse(URL url, int timeoutMillis) throws IOException {
+ Connection con = HttpConnection.connect(url);
+ con.timeout(timeoutMillis);
+ return con.get();
+ }
+
+ /**
+ Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
+ tags and attributes.
+
+ @param bodyHtml input untrusted HTML
+ @param baseUri URL to resolve relative URLs against
+ @param whitelist white-list of permitted HTML elements
+ @return safe HTML
+
+ @see Cleaner#clean(Document)
+ */
+ public static String clean(String bodyHtml, String baseUri, Whitelist whitelist) {
+ Document dirty = parseBodyFragment(bodyHtml, baseUri);
+ Cleaner cleaner = new Cleaner(whitelist);
+ Document clean = cleaner.clean(dirty);
+ return clean.body().html();
+ }
+
+ /**
+ Get safe HTML from untrusted input HTML, by parsing input HTML and filtering it through a white-list of permitted
+ tags and attributes.
+
+ @param bodyHtml input untrusted HTML
+ @param whitelist white-list of permitted HTML elements
+ @return safe HTML
+
+ @see Cleaner#clean(Document)
+ */
+ public static String clean(String bodyHtml, Whitelist whitelist) {
+ return clean(bodyHtml, "", whitelist);
+ }
+
+ /**
+ Test if the input HTML has only tags and attributes allowed by the Whitelist. Useful for form validation. The input HTML should
+ still be run through the cleaner to set up enforced attributes, and to tidy the output.
+ @param bodyHtml HTML to test
+ @param whitelist whitelist to test against
+ @return true if no tags or attributes were removed; false otherwise
+ @see #clean(String, org.jsoup.safety.Whitelist)
+ */
+ public static boolean isValid(String bodyHtml, Whitelist whitelist) {
+ Document dirty = parseBodyFragment(bodyHtml, "");
+ Cleaner cleaner = new Cleaner(whitelist);
+ return cleaner.isValid(dirty);
+ }
+
+}
diff --git a/server/src/org/jsoup/examples/HtmlToPlainText.java b/server/src/org/jsoup/examples/HtmlToPlainText.java
new file mode 100644
index 0000000000..8f563e9608
--- /dev/null
+++ b/server/src/org/jsoup/examples/HtmlToPlainText.java
@@ -0,0 +1,109 @@
+package org.jsoup.examples;
+
+import org.jsoup.Jsoup;
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.select.NodeTraversor;
+import org.jsoup.select.NodeVisitor;
+
+import java.io.IOException;
+
+/**
+ * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
+ * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
+ * scrape.
+ * <p/>
+ * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ */
+public class HtmlToPlainText {
+ public static void main(String... args) throws IOException {
+ Validate.isTrue(args.length == 1, "usage: supply url to fetch");
+ String url = args[0];
+
+ // fetch the specified URL and parse to a HTML DOM
+ Document doc = Jsoup.connect(url).get();
+
+ HtmlToPlainText formatter = new HtmlToPlainText();
+ String plainText = formatter.getPlainText(doc);
+ System.out.println(plainText);
+ }
+
+ /**
+ * Format an Element to plain-text
+ * @param element the root element to format
+ * @return formatted text
+ */
+ public String getPlainText(Element element) {
+ FormattingVisitor formatter = new FormattingVisitor();
+ NodeTraversor traversor = new NodeTraversor(formatter);
+ traversor.traverse(element); // walk the DOM, and call .head() and .tail() for each node
+
+ return formatter.toString();
+ }
+
+ // the formatting rules, implemented in a breadth-first DOM traverse
+ private class FormattingVisitor implements NodeVisitor {
+ private static final int maxWidth = 80;
+ private int width = 0;
+ private StringBuilder accum = new StringBuilder(); // holds the accumulated text
+
+ // hit when the node is first seen
+ public void head(Node node, int depth) {
+ String name = node.nodeName();
+ if (node instanceof TextNode)
+ append(((TextNode) node).text()); // TextNodes carry all user-readable text in the DOM.
+ else if (name.equals("li"))
+ append("\n * ");
+ }
+
+ // hit when all of the node's children (if any) have been visited
+ public void tail(Node node, int depth) {
+ String name = node.nodeName();
+ if (name.equals("br"))
+ append("\n");
+ else if (StringUtil.in(name, "p", "h1", "h2", "h3", "h4", "h5"))
+ append("\n\n");
+ else if (name.equals("a"))
+ append(String.format(" <%s>", node.absUrl("href")));
+ }
+
+ // appends text to the string builder with a simple word wrap method
+ private void append(String text) {
+ if (text.startsWith("\n"))
+ width = 0; // reset counter if starts with a newline. only from formats above, not in natural text
+ if (text.equals(" ") &&
+ (accum.length() == 0 || StringUtil.in(accum.substring(accum.length() - 1), " ", "\n")))
+ return; // don't accumulate long runs of empty spaces
+
+ if (text.length() + width > maxWidth) { // won't fit, needs to wrap
+ String words[] = text.split("\\s+");
+ for (int i = 0; i < words.length; i++) {
+ String word = words[i];
+ boolean last = i == words.length - 1;
+ if (!last) // insert a space if not the last word
+ word = word + " ";
+ if (word.length() + width > maxWidth) { // wrap and reset counter
+ accum.append("\n").append(word);
+ width = word.length();
+ } else {
+ accum.append(word);
+ width += word.length();
+ }
+ }
+ } else { // fits as is, without need to wrap text
+ accum.append(text);
+ width += text.length();
+ }
+ }
+
+ public String toString() {
+ return accum.toString();
+ }
+ }
+}
diff --git a/server/src/org/jsoup/examples/ListLinks.java b/server/src/org/jsoup/examples/ListLinks.java
new file mode 100644
index 0000000000..64b29ba107
--- /dev/null
+++ b/server/src/org/jsoup/examples/ListLinks.java
@@ -0,0 +1,56 @@
+package org.jsoup.examples;
+
+import org.jsoup.Jsoup;
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import java.io.IOException;
+
+/**
+ * Example program to list links from a URL.
+ */
+public class ListLinks {
+ public static void main(String[] args) throws IOException {
+ Validate.isTrue(args.length == 1, "usage: supply url to fetch");
+ String url = args[0];
+ print("Fetching %s...", url);
+
+ Document doc = Jsoup.connect(url).get();
+ Elements links = doc.select("a[href]");
+ Elements media = doc.select("[src]");
+ Elements imports = doc.select("link[href]");
+
+ print("\nMedia: (%d)", media.size());
+ for (Element src : media) {
+ if (src.tagName().equals("img"))
+ print(" * %s: <%s> %sx%s (%s)",
+ src.tagName(), src.attr("abs:src"), src.attr("width"), src.attr("height"),
+ trim(src.attr("alt"), 20));
+ else
+ print(" * %s: <%s>", src.tagName(), src.attr("abs:src"));
+ }
+
+ print("\nImports: (%d)", imports.size());
+ for (Element link : imports) {
+ print(" * %s <%s> (%s)", link.tagName(),link.attr("abs:href"), link.attr("rel"));
+ }
+
+ print("\nLinks: (%d)", links.size());
+ for (Element link : links) {
+ print(" * a: <%s> (%s)", link.attr("abs:href"), trim(link.text(), 35));
+ }
+ }
+
+ private static void print(String msg, Object... args) {
+ System.out.println(String.format(msg, args));
+ }
+
+ private static String trim(String s, int width) {
+ if (s.length() > width)
+ return s.substring(0, width-1) + ".";
+ else
+ return s;
+ }
+}
diff --git a/server/src/org/jsoup/examples/package-info.java b/server/src/org/jsoup/examples/package-info.java
new file mode 100644
index 0000000000..c312f430d4
--- /dev/null
+++ b/server/src/org/jsoup/examples/package-info.java
@@ -0,0 +1,4 @@
+/**
+ Contains example programs and use of jsoup. See the <a href="http://jsoup.org/cookbook/">jsoup cookbook</a>.
+ */
+package org.jsoup.examples; \ No newline at end of file
diff --git a/server/src/org/jsoup/helper/DataUtil.java b/server/src/org/jsoup/helper/DataUtil.java
new file mode 100644
index 0000000000..9adfe42153
--- /dev/null
+++ b/server/src/org/jsoup/helper/DataUtil.java
@@ -0,0 +1,135 @@
+package org.jsoup.helper;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.parser.Parser;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Internal static utilities for handling data.
+ *
+ */
+public class DataUtil {
+ private static final Pattern charsetPattern = Pattern.compile("(?i)\\bcharset=\\s*\"?([^\\s;\"]*)");
+ static final String defaultCharset = "UTF-8"; // used if not found in header or meta charset
+ private static final int bufferSize = 0x20000; // ~130K.
+
+ private DataUtil() {}
+
+ /**
+ * Loads a file to a Document.
+ * @param in file to load
+ * @param charsetName character set of input
+ * @param baseUri base URI of document, to resolve relative links against
+ * @return Document
+ * @throws IOException on IO error
+ */
+ public static Document load(File in, String charsetName, String baseUri) throws IOException {
+ FileInputStream inStream = null;
+ try {
+ inStream = new FileInputStream(in);
+ ByteBuffer byteData = readToByteBuffer(inStream);
+ return parseByteData(byteData, charsetName, baseUri, Parser.htmlParser());
+ } finally {
+ if (inStream != null)
+ inStream.close();
+ }
+ }
+
+ /**
+ * Parses a Document from an input steam.
+ * @param in input stream to parse. You will need to close it.
+ * @param charsetName character set of input
+ * @param baseUri base URI of document, to resolve relative links against
+ * @return Document
+ * @throws IOException on IO error
+ */
+ public static Document load(InputStream in, String charsetName, String baseUri) throws IOException {
+ ByteBuffer byteData = readToByteBuffer(in);
+ return parseByteData(byteData, charsetName, baseUri, Parser.htmlParser());
+ }
+
+ /**
+ * Parses a Document from an input steam, using the provided Parser.
+ * @param in input stream to parse. You will need to close it.
+ * @param charsetName character set of input
+ * @param baseUri base URI of document, to resolve relative links against
+ * @param parser alternate {@link Parser#xmlParser() parser} to use.
+ * @return Document
+ * @throws IOException on IO error
+ */
+ public static Document load(InputStream in, String charsetName, String baseUri, Parser parser) throws IOException {
+ ByteBuffer byteData = readToByteBuffer(in);
+ return parseByteData(byteData, charsetName, baseUri, parser);
+ }
+
+ // reads bytes first into a buffer, then decodes with the appropriate charset. done this way to support
+ // switching the chartset midstream when a meta http-equiv tag defines the charset.
+ static Document parseByteData(ByteBuffer byteData, String charsetName, String baseUri, Parser parser) {
+ String docData;
+ Document doc = null;
+ if (charsetName == null) { // determine from meta. safe parse as UTF-8
+ // look for <meta http-equiv="Content-Type" content="text/html;charset=gb2312"> or HTML5 <meta charset="gb2312">
+ docData = Charset.forName(defaultCharset).decode(byteData).toString();
+ doc = parser.parseInput(docData, baseUri);
+ Element meta = doc.select("meta[http-equiv=content-type], meta[charset]").first();
+ if (meta != null) { // if not found, will keep utf-8 as best attempt
+ String foundCharset = meta.hasAttr("http-equiv") ? getCharsetFromContentType(meta.attr("content")) : meta.attr("charset");
+ if (foundCharset != null && foundCharset.length() != 0 && !foundCharset.equals(defaultCharset)) { // need to re-decode
+ charsetName = foundCharset;
+ byteData.rewind();
+ docData = Charset.forName(foundCharset).decode(byteData).toString();
+ doc = null;
+ }
+ }
+ } else { // specified by content type header (or by user on file load)
+ Validate.notEmpty(charsetName, "Must set charset arg to character set of file to parse. Set to null to attempt to detect from HTML");
+ docData = Charset.forName(charsetName).decode(byteData).toString();
+ }
+ if (doc == null) {
+ // there are times where there is a spurious byte-order-mark at the start of the text. Shouldn't be present
+ // in utf-8. If after decoding, there is a BOM, strip it; otherwise will cause the parser to go straight
+ // into head mode
+ if (docData.charAt(0) == 65279)
+ docData = docData.substring(1);
+
+ doc = parser.parseInput(docData, baseUri);
+ doc.outputSettings().charset(charsetName);
+ }
+ return doc;
+ }
+
+ static ByteBuffer readToByteBuffer(InputStream inStream) throws IOException {
+ byte[] buffer = new byte[bufferSize];
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream(bufferSize);
+ int read;
+ while(true) {
+ read = inStream.read(buffer);
+ if (read == -1) break;
+ outStream.write(buffer, 0, read);
+ }
+ ByteBuffer byteData = ByteBuffer.wrap(outStream.toByteArray());
+ return byteData;
+ }
+
+ /**
+ * Parse out a charset from a content type header.
+ * @param contentType e.g. "text/html; charset=EUC-JP"
+ * @return "EUC-JP", or null if not found. Charset is trimmed and uppercased.
+ */
+ static String getCharsetFromContentType(String contentType) {
+ if (contentType == null) return null;
+ Matcher m = charsetPattern.matcher(contentType);
+ if (m.find()) {
+ return m.group(1).trim().toUpperCase();
+ }
+ return null;
+ }
+
+
+}
diff --git a/server/src/org/jsoup/helper/DescendableLinkedList.java b/server/src/org/jsoup/helper/DescendableLinkedList.java
new file mode 100644
index 0000000000..28ca1971eb
--- /dev/null
+++ b/server/src/org/jsoup/helper/DescendableLinkedList.java
@@ -0,0 +1,82 @@
+package org.jsoup.helper;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.ListIterator;
+
+/**
+ * Provides a descending iterator and other 1.6 methods to allow support on the 1.5 JRE.
+ */
+public class DescendableLinkedList<E> extends LinkedList<E> {
+
+ /**
+ * Create a new DescendableLinkedList.
+ */
+ public DescendableLinkedList() {
+ super();
+ }
+
+ /**
+ * Add a new element to the start of the list.
+ * @param e element to add
+ */
+ public void push(E e) {
+ addFirst(e);
+ }
+
+ /**
+ * Look at the last element, if there is one.
+ * @return the last element, or null
+ */
+ public E peekLast() {
+ return size() == 0 ? null : getLast();
+ }
+
+ /**
+ * Remove and return the last element, if there is one
+ * @return the last element, or null
+ */
+ public E pollLast() {
+ return size() == 0 ? null : removeLast();
+ }
+
+ /**
+ * Get an iterator that starts and the end of the list and works towards the start.
+ * @return an iterator that starts and the end of the list and works towards the start.
+ */
+ public Iterator<E> descendingIterator() {
+ return new DescendingIterator<E>(size());
+ }
+
+ private class DescendingIterator<E> implements Iterator<E> {
+ private final ListIterator<E> iter;
+
+ @SuppressWarnings("unchecked")
+ private DescendingIterator(int index) {
+ iter = (ListIterator<E>) listIterator(index);
+ }
+
+ /**
+ * Check if there is another element on the list.
+ * @return if another element
+ */
+ public boolean hasNext() {
+ return iter.hasPrevious();
+ }
+
+ /**
+ * Get the next element.
+ * @return the next element.
+ */
+ public E next() {
+ return iter.previous();
+ }
+
+ /**
+ * Remove the current element.
+ */
+ public void remove() {
+ iter.remove();
+ }
+ }
+}
diff --git a/server/src/org/jsoup/helper/HttpConnection.java b/server/src/org/jsoup/helper/HttpConnection.java
new file mode 100644
index 0000000000..06200a2547
--- /dev/null
+++ b/server/src/org/jsoup/helper/HttpConnection.java
@@ -0,0 +1,658 @@
+package org.jsoup.helper;
+
+import org.jsoup.Connection;
+import org.jsoup.nodes.Document;
+import org.jsoup.parser.Parser;
+import org.jsoup.parser.TokenQueue;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * Implementation of {@link Connection}.
+ * @see org.jsoup.Jsoup#connect(String)
+ */
+public class HttpConnection implements Connection {
+ public static Connection connect(String url) {
+ Connection con = new HttpConnection();
+ con.url(url);
+ return con;
+ }
+
+ public static Connection connect(URL url) {
+ Connection con = new HttpConnection();
+ con.url(url);
+ return con;
+ }
+
+ private Connection.Request req;
+ private Connection.Response res;
+
+ private HttpConnection() {
+ req = new Request();
+ res = new Response();
+ }
+
+ public Connection url(URL url) {
+ req.url(url);
+ return this;
+ }
+
+ public Connection url(String url) {
+ Validate.notEmpty(url, "Must supply a valid URL");
+ try {
+ req.url(new URL(url));
+ } catch (MalformedURLException e) {
+ throw new IllegalArgumentException("Malformed URL: " + url, e);
+ }
+ return this;
+ }
+
+ public Connection userAgent(String userAgent) {
+ Validate.notNull(userAgent, "User agent must not be null");
+ req.header("User-Agent", userAgent);
+ return this;
+ }
+
+ public Connection timeout(int millis) {
+ req.timeout(millis);
+ return this;
+ }
+
+ public Connection followRedirects(boolean followRedirects) {
+ req.followRedirects(followRedirects);
+ return this;
+ }
+
+ public Connection referrer(String referrer) {
+ Validate.notNull(referrer, "Referrer must not be null");
+ req.header("Referer", referrer);
+ return this;
+ }
+
+ public Connection method(Method method) {
+ req.method(method);
+ return this;
+ }
+
+ public Connection ignoreHttpErrors(boolean ignoreHttpErrors) {
+ req.ignoreHttpErrors(ignoreHttpErrors);
+ return this;
+ }
+
+ public Connection ignoreContentType(boolean ignoreContentType) {
+ req.ignoreContentType(ignoreContentType);
+ return this;
+ }
+
+ public Connection data(String key, String value) {
+ req.data(KeyVal.create(key, value));
+ return this;
+ }
+
+ public Connection data(Map<String, String> data) {
+ Validate.notNull(data, "Data map must not be null");
+ for (Map.Entry<String, String> entry : data.entrySet()) {
+ req.data(KeyVal.create(entry.getKey(), entry.getValue()));
+ }
+ return this;
+ }
+
+ public Connection data(String... keyvals) {
+ Validate.notNull(keyvals, "Data key value pairs must not be null");
+ Validate.isTrue(keyvals.length %2 == 0, "Must supply an even number of key value pairs");
+ for (int i = 0; i < keyvals.length; i += 2) {
+ String key = keyvals[i];
+ String value = keyvals[i+1];
+ Validate.notEmpty(key, "Data key must not be empty");
+ Validate.notNull(value, "Data value must not be null");
+ req.data(KeyVal.create(key, value));
+ }
+ return this;
+ }
+
+ public Connection header(String name, String value) {
+ req.header(name, value);
+ return this;
+ }
+
+ public Connection cookie(String name, String value) {
+ req.cookie(name, value);
+ return this;
+ }
+
+ public Connection cookies(Map<String, String> cookies) {
+ Validate.notNull(cookies, "Cookie map must not be null");
+ for (Map.Entry<String, String> entry : cookies.entrySet()) {
+ req.cookie(entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+
+ public Connection parser(Parser parser) {
+ req.parser(parser);
+ return this;
+ }
+
+ public Document get() throws IOException {
+ req.method(Method.GET);
+ execute();
+ return res.parse();
+ }
+
+ public Document post() throws IOException {
+ req.method(Method.POST);
+ execute();
+ return res.parse();
+ }
+
+ public Connection.Response execute() throws IOException {
+ res = Response.execute(req);
+ return res;
+ }
+
+ public Connection.Request request() {
+ return req;
+ }
+
+ public Connection request(Connection.Request request) {
+ req = request;
+ return this;
+ }
+
+ public Connection.Response response() {
+ return res;
+ }
+
+ public Connection response(Connection.Response response) {
+ res = response;
+ return this;
+ }
+
+ @SuppressWarnings({"unchecked"})
+ private static abstract class Base<T extends Connection.Base> implements Connection.Base<T> {
+ URL url;
+ Method method;
+ Map<String, String> headers;
+ Map<String, String> cookies;
+
+ private Base() {
+ headers = new LinkedHashMap<String, String>();
+ cookies = new LinkedHashMap<String, String>();
+ }
+
+ public URL url() {
+ return url;
+ }
+
+ public T url(URL url) {
+ Validate.notNull(url, "URL must not be null");
+ this.url = url;
+ return (T) this;
+ }
+
+ public Method method() {
+ return method;
+ }
+
+ public T method(Method method) {
+ Validate.notNull(method, "Method must not be null");
+ this.method = method;
+ return (T) this;
+ }
+
+ public String header(String name) {
+ Validate.notNull(name, "Header name must not be null");
+ return getHeaderCaseInsensitive(name);
+ }
+
+ public T header(String name, String value) {
+ Validate.notEmpty(name, "Header name must not be empty");
+ Validate.notNull(value, "Header value must not be null");
+ removeHeader(name); // ensures we don't get an "accept-encoding" and a "Accept-Encoding"
+ headers.put(name, value);
+ return (T) this;
+ }
+
+ public boolean hasHeader(String name) {
+ Validate.notEmpty(name, "Header name must not be empty");
+ return getHeaderCaseInsensitive(name) != null;
+ }
+
+ public T removeHeader(String name) {
+ Validate.notEmpty(name, "Header name must not be empty");
+ Map.Entry<String, String> entry = scanHeaders(name); // remove is case insensitive too
+ if (entry != null)
+ headers.remove(entry.getKey()); // ensures correct case
+ return (T) this;
+ }
+
+ public Map<String, String> headers() {
+ return headers;
+ }
+
+ private String getHeaderCaseInsensitive(String name) {
+ Validate.notNull(name, "Header name must not be null");
+ // quick evals for common case of title case, lower case, then scan for mixed
+ String value = headers.get(name);
+ if (value == null)
+ value = headers.get(name.toLowerCase());
+ if (value == null) {
+ Map.Entry<String, String> entry = scanHeaders(name);
+ if (entry != null)
+ value = entry.getValue();
+ }
+ return value;
+ }
+
+ private Map.Entry<String, String> scanHeaders(String name) {
+ String lc = name.toLowerCase();
+ for (Map.Entry<String, String> entry : headers.entrySet()) {
+ if (entry.getKey().toLowerCase().equals(lc))
+ return entry;
+ }
+ return null;
+ }
+
+ public String cookie(String name) {
+ Validate.notNull(name, "Cookie name must not be null");
+ return cookies.get(name);
+ }
+
+ public T cookie(String name, String value) {
+ Validate.notEmpty(name, "Cookie name must not be empty");
+ Validate.notNull(value, "Cookie value must not be null");
+ cookies.put(name, value);
+ return (T) this;
+ }
+
+ public boolean hasCookie(String name) {
+ Validate.notEmpty("Cookie name must not be empty");
+ return cookies.containsKey(name);
+ }
+
+ public T removeCookie(String name) {
+ Validate.notEmpty("Cookie name must not be empty");
+ cookies.remove(name);
+ return (T) this;
+ }
+
+ public Map<String, String> cookies() {
+ return cookies;
+ }
+ }
+
+ public static class Request extends Base<Connection.Request> implements Connection.Request {
+ private int timeoutMilliseconds;
+ private boolean followRedirects;
+ private Collection<Connection.KeyVal> data;
+ private boolean ignoreHttpErrors = false;
+ private boolean ignoreContentType = false;
+ private Parser parser;
+
+ private Request() {
+ timeoutMilliseconds = 3000;
+ followRedirects = true;
+ data = new ArrayList<Connection.KeyVal>();
+ method = Connection.Method.GET;
+ headers.put("Accept-Encoding", "gzip");
+ parser = Parser.htmlParser();
+ }
+
+ public int timeout() {
+ return timeoutMilliseconds;
+ }
+
+ public Request timeout(int millis) {
+ Validate.isTrue(millis >= 0, "Timeout milliseconds must be 0 (infinite) or greater");
+ timeoutMilliseconds = millis;
+ return this;
+ }
+
+ public boolean followRedirects() {
+ return followRedirects;
+ }
+
+ public Connection.Request followRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ return this;
+ }
+
+ public boolean ignoreHttpErrors() {
+ return ignoreHttpErrors;
+ }
+
+ public Connection.Request ignoreHttpErrors(boolean ignoreHttpErrors) {
+ this.ignoreHttpErrors = ignoreHttpErrors;
+ return this;
+ }
+
+ public boolean ignoreContentType() {
+ return ignoreContentType;
+ }
+
+ public Connection.Request ignoreContentType(boolean ignoreContentType) {
+ this.ignoreContentType = ignoreContentType;
+ return this;
+ }
+
+ public Request data(Connection.KeyVal keyval) {
+ Validate.notNull(keyval, "Key val must not be null");
+ data.add(keyval);
+ return this;
+ }
+
+ public Collection<Connection.KeyVal> data() {
+ return data;
+ }
+
+ public Request parser(Parser parser) {
+ this.parser = parser;
+ return this;
+ }
+
+ public Parser parser() {
+ return parser;
+ }
+ }
+
+ public static class Response extends Base<Connection.Response> implements Connection.Response {
+ private static final int MAX_REDIRECTS = 20;
+ private int statusCode;
+ private String statusMessage;
+ private ByteBuffer byteData;
+ private String charset;
+ private String contentType;
+ private boolean executed = false;
+ private int numRedirects = 0;
+ private Connection.Request req;
+
+ Response() {
+ super();
+ }
+
+ private Response(Response previousResponse) throws IOException {
+ super();
+ if (previousResponse != null) {
+ numRedirects = previousResponse.numRedirects + 1;
+ if (numRedirects >= MAX_REDIRECTS)
+ throw new IOException(String.format("Too many redirects occurred trying to load URL %s", previousResponse.url()));
+ }
+ }
+
+ static Response execute(Connection.Request req) throws IOException {
+ return execute(req, null);
+ }
+
+ static Response execute(Connection.Request req, Response previousResponse) throws IOException {
+ Validate.notNull(req, "Request must not be null");
+ String protocol = req.url().getProtocol();
+ Validate
+ .isTrue(protocol.equals("http") || protocol.equals("https"), "Only http & https protocols supported");
+
+ // set up the request for execution
+ if (req.method() == Connection.Method.GET && req.data().size() > 0)
+ serialiseRequestUrl(req); // appends query string
+ HttpURLConnection conn = createConnection(req);
+ conn.connect();
+ if (req.method() == Connection.Method.POST)
+ writePost(req.data(), conn.getOutputStream());
+
+ int status = conn.getResponseCode();
+ boolean needsRedirect = false;
+ if (status != HttpURLConnection.HTTP_OK) {
+ if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER)
+ needsRedirect = true;
+ else if (!req.ignoreHttpErrors())
+ throw new IOException(status + " error loading URL " + req.url().toString());
+ }
+ Response res = new Response(previousResponse);
+ res.setupFromConnection(conn, previousResponse);
+ if (needsRedirect && req.followRedirects()) {
+ req.method(Method.GET); // always redirect with a get. any data param from original req are dropped.
+ req.data().clear();
+ req.url(new URL(req.url(), res.header("Location")));
+ for (Map.Entry<String, String> cookie : res.cookies.entrySet()) { // add response cookies to request (for e.g. login posts)
+ req.cookie(cookie.getKey(), cookie.getValue());
+ }
+ return execute(req, res);
+ }
+ res.req = req;
+
+ InputStream bodyStream = null;
+ InputStream dataStream = null;
+ try {
+ dataStream = conn.getErrorStream() != null ? conn.getErrorStream() : conn.getInputStream();
+ bodyStream = res.hasHeader("Content-Encoding") && res.header("Content-Encoding").equalsIgnoreCase("gzip") ?
+ new BufferedInputStream(new GZIPInputStream(dataStream)) :
+ new BufferedInputStream(dataStream);
+
+ res.byteData = DataUtil.readToByteBuffer(bodyStream);
+ res.charset = DataUtil.getCharsetFromContentType(res.contentType); // may be null, readInputStream deals with it
+ } finally {
+ if (bodyStream != null) bodyStream.close();
+ if (dataStream != null) dataStream.close();
+ }
+
+ res.executed = true;
+ return res;
+ }
+
+ public int statusCode() {
+ return statusCode;
+ }
+
+ public String statusMessage() {
+ return statusMessage;
+ }
+
+ public String charset() {
+ return charset;
+ }
+
+ public String contentType() {
+ return contentType;
+ }
+
+ public Document parse() throws IOException {
+ Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before parsing response");
+ if (!req.ignoreContentType() && (contentType == null || !(contentType.startsWith("text/") || contentType.startsWith("application/xml") || contentType.startsWith("application/xhtml+xml"))))
+ throw new IOException(String.format("Unhandled content type \"%s\" on URL %s. Must be text/*, application/xml, or application/xhtml+xml",
+ contentType, url.toString()));
+ Document doc = DataUtil.parseByteData(byteData, charset, url.toExternalForm(), req.parser());
+ byteData.rewind();
+ charset = doc.outputSettings().charset().name(); // update charset from meta-equiv, possibly
+ return doc;
+ }
+
+ public String body() {
+ Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body");
+ // charset gets set from header on execute, and from meta-equiv on parse. parse may not have happened yet
+ String body;
+ if (charset == null)
+ body = Charset.forName(DataUtil.defaultCharset).decode(byteData).toString();
+ else
+ body = Charset.forName(charset).decode(byteData).toString();
+ byteData.rewind();
+ return body;
+ }
+
+ public byte[] bodyAsBytes() {
+ Validate.isTrue(executed, "Request must be executed (with .execute(), .get(), or .post() before getting response body");
+ return byteData.array();
+ }
+
+ // set up connection defaults, and details from request
+ private static HttpURLConnection createConnection(Connection.Request req) throws IOException {
+ HttpURLConnection conn = (HttpURLConnection) req.url().openConnection();
+ conn.setRequestMethod(req.method().name());
+ conn.setInstanceFollowRedirects(false); // don't rely on native redirection support
+ conn.setConnectTimeout(req.timeout());
+ conn.setReadTimeout(req.timeout());
+ if (req.method() == Method.POST)
+ conn.setDoOutput(true);
+ if (req.cookies().size() > 0)
+ conn.addRequestProperty("Cookie", getRequestCookieString(req));
+ for (Map.Entry<String, String> header : req.headers().entrySet()) {
+ conn.addRequestProperty(header.getKey(), header.getValue());
+ }
+ return conn;
+ }
+
+ // set up url, method, header, cookies
+ private void setupFromConnection(HttpURLConnection conn, Connection.Response previousResponse) throws IOException {
+ method = Connection.Method.valueOf(conn.getRequestMethod());
+ url = conn.getURL();
+ statusCode = conn.getResponseCode();
+ statusMessage = conn.getResponseMessage();
+ contentType = conn.getContentType();
+
+ Map<String, List<String>> resHeaders = conn.getHeaderFields();
+ processResponseHeaders(resHeaders);
+
+ // if from a redirect, map previous response cookies into this response
+ if (previousResponse != null) {
+ for (Map.Entry<String, String> prevCookie : previousResponse.cookies().entrySet()) {
+ if (!hasCookie(prevCookie.getKey()))
+ cookie(prevCookie.getKey(), prevCookie.getValue());
+ }
+ }
+ }
+
+ void processResponseHeaders(Map<String, List<String>> resHeaders) {
+ for (Map.Entry<String, List<String>> entry : resHeaders.entrySet()) {
+ String name = entry.getKey();
+ if (name == null)
+ continue; // http/1.1 line
+
+ List<String> values = entry.getValue();
+ if (name.equalsIgnoreCase("Set-Cookie")) {
+ for (String value : values) {
+ if (value == null)
+ continue;
+ TokenQueue cd = new TokenQueue(value);
+ String cookieName = cd.chompTo("=").trim();
+ String cookieVal = cd.consumeTo(";").trim();
+ if (cookieVal == null)
+ cookieVal = "";
+ // ignores path, date, domain, secure et al. req'd?
+ // name not blank, value not null
+ if (cookieName != null && cookieName.length() > 0)
+ cookie(cookieName, cookieVal);
+ }
+ } else { // only take the first instance of each header
+ if (!values.isEmpty())
+ header(name, values.get(0));
+ }
+ }
+ }
+
+ private static void writePost(Collection<Connection.KeyVal> data, OutputStream outputStream) throws IOException {
+ OutputStreamWriter w = new OutputStreamWriter(outputStream, DataUtil.defaultCharset);
+ boolean first = true;
+ for (Connection.KeyVal keyVal : data) {
+ if (!first)
+ w.append('&');
+ else
+ first = false;
+
+ w.write(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharset));
+ w.write('=');
+ w.write(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharset));
+ }
+ w.close();
+ }
+
+ private static String getRequestCookieString(Connection.Request req) {
+ StringBuilder sb = new StringBuilder();
+ boolean first = true;
+ for (Map.Entry<String, String> cookie : req.cookies().entrySet()) {
+ if (!first)
+ sb.append("; ");
+ else
+ first = false;
+ sb.append(cookie.getKey()).append('=').append(cookie.getValue());
+ // todo: spec says only ascii, no escaping / encoding defined. validate on set? or escape somehow here?
+ }
+ return sb.toString();
+ }
+
+ // for get url reqs, serialise the data map into the url
+ private static void serialiseRequestUrl(Connection.Request req) throws IOException {
+ URL in = req.url();
+ StringBuilder url = new StringBuilder();
+ boolean first = true;
+ // reconstitute the query, ready for appends
+ url
+ .append(in.getProtocol())
+ .append("://")
+ .append(in.getAuthority()) // includes host, port
+ .append(in.getPath())
+ .append("?");
+ if (in.getQuery() != null) {
+ url.append(in.getQuery());
+ first = false;
+ }
+ for (Connection.KeyVal keyVal : req.data()) {
+ if (!first)
+ url.append('&');
+ else
+ first = false;
+ url
+ .append(URLEncoder.encode(keyVal.key(), DataUtil.defaultCharset))
+ .append('=')
+ .append(URLEncoder.encode(keyVal.value(), DataUtil.defaultCharset));
+ }
+ req.url(new URL(url.toString()));
+ req.data().clear(); // moved into url as get params
+ }
+ }
+
+ public static class KeyVal implements Connection.KeyVal {
+ private String key;
+ private String value;
+
+ public static KeyVal create(String key, String value) {
+ Validate.notEmpty(key, "Data key must not be empty");
+ Validate.notNull(value, "Data value must not be null");
+ return new KeyVal(key, value);
+ }
+
+ private KeyVal(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public KeyVal key(String key) {
+ Validate.notEmpty(key, "Data key must not be empty");
+ this.key = key;
+ return this;
+ }
+
+ public String key() {
+ return key;
+ }
+
+ public KeyVal value(String value) {
+ Validate.notNull(value, "Data value must not be null");
+ this.value = value;
+ return this;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return key + "=" + value;
+ }
+ }
+}
diff --git a/server/src/org/jsoup/helper/StringUtil.java b/server/src/org/jsoup/helper/StringUtil.java
new file mode 100644
index 0000000000..071a92c7a5
--- /dev/null
+++ b/server/src/org/jsoup/helper/StringUtil.java
@@ -0,0 +1,140 @@
+package org.jsoup.helper;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * A minimal String utility class. Designed for internal jsoup use only.
+ */
+public final class StringUtil {
+ // memoised padding up to 10
+ private static final String[] padding = {"", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "};
+
+ /**
+ * Join a collection of strings by a seperator
+ * @param strings collection of string objects
+ * @param sep string to place between strings
+ * @return joined string
+ */
+ public static String join(Collection strings, String sep) {
+ return join(strings.iterator(), sep);
+ }
+
+ /**
+ * Join a collection of strings by a seperator
+ * @param strings iterator of string objects
+ * @param sep string to place between strings
+ * @return joined string
+ */
+ public static String join(Iterator strings, String sep) {
+ if (!strings.hasNext())
+ return "";
+
+ String start = strings.next().toString();
+ if (!strings.hasNext()) // only one, avoid builder
+ return start;
+
+ StringBuilder sb = new StringBuilder(64).append(start);
+ while (strings.hasNext()) {
+ sb.append(sep);
+ sb.append(strings.next());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns space padding
+ * @param width amount of padding desired
+ * @return string of spaces * width
+ */
+ public static String padding(int width) {
+ if (width < 0)
+ throw new IllegalArgumentException("width must be > 0");
+
+ if (width < padding.length)
+ return padding[width];
+
+ char[] out = new char[width];
+ for (int i = 0; i < width; i++)
+ out[i] = ' ';
+ return String.valueOf(out);
+ }
+
+ /**
+ * Tests if a string is blank: null, emtpy, or only whitespace (" ", \r\n, \t, etc)
+ * @param string string to test
+ * @return if string is blank
+ */
+ public static boolean isBlank(String string) {
+ if (string == null || string.length() == 0)
+ return true;
+
+ int l = string.length();
+ for (int i = 0; i < l; i++) {
+ if (!StringUtil.isWhitespace(string.codePointAt(i)))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Tests if a string is numeric, i.e. contains only digit characters
+ * @param string string to test
+ * @return true if only digit chars, false if empty or null or contains non-digit chrs
+ */
+ public static boolean isNumeric(String string) {
+ if (string == null || string.length() == 0)
+ return false;
+
+ int l = string.length();
+ for (int i = 0; i < l; i++) {
+ if (!Character.isDigit(string.codePointAt(i)))
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Tests if a code point is "whitespace" as defined in the HTML spec.
+ * @param c code point to test
+ * @return true if code point is whitespace, false otherwise
+ */
+ public static boolean isWhitespace(int c){
+ return c == ' ' || c == '\t' || c == '\n' || c == '\f' || c == '\r';
+ }
+
+ public static String normaliseWhitespace(String string) {
+ StringBuilder sb = new StringBuilder(string.length());
+
+ boolean lastWasWhite = false;
+ boolean modified = false;
+
+ int l = string.length();
+ for (int i = 0; i < l; i++) {
+ int c = string.codePointAt(i);
+ if (isWhitespace(c)) {
+ if (lastWasWhite) {
+ modified = true;
+ continue;
+ }
+ if (c != ' ')
+ modified = true;
+ sb.append(' ');
+ lastWasWhite = true;
+ }
+ else {
+ sb.appendCodePoint(c);
+ lastWasWhite = false;
+ }
+ }
+ return modified ? sb.toString() : string;
+ }
+
+ public static boolean in(String needle, String... haystack) {
+ for (String hay : haystack) {
+ if (hay.equals(needle))
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/server/src/org/jsoup/helper/Validate.java b/server/src/org/jsoup/helper/Validate.java
new file mode 100644
index 0000000000..814bcc3a40
--- /dev/null
+++ b/server/src/org/jsoup/helper/Validate.java
@@ -0,0 +1,112 @@
+package org.jsoup.helper;
+
+/**
+ * Simple validation methods. Designed for jsoup internal use
+ */
+public final class Validate {
+
+ private Validate() {}
+
+ /**
+ * Validates that the object is not null
+ * @param obj object to test
+ */
+ public static void notNull(Object obj) {
+ if (obj == null)
+ throw new IllegalArgumentException("Object must not be null");
+ }
+
+ /**
+ * Validates that the object is not null
+ * @param obj object to test
+ * @param msg message to output if validation fails
+ */
+ public static void notNull(Object obj, String msg) {
+ if (obj == null)
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ * Validates that the value is true
+ * @param val object to test
+ */
+ public static void isTrue(boolean val) {
+ if (!val)
+ throw new IllegalArgumentException("Must be true");
+ }
+
+ /**
+ * Validates that the value is true
+ * @param val object to test
+ * @param msg message to output if validation fails
+ */
+ public static void isTrue(boolean val, String msg) {
+ if (!val)
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ * Validates that the value is false
+ * @param val object to test
+ */
+ public static void isFalse(boolean val) {
+ if (val)
+ throw new IllegalArgumentException("Must be false");
+ }
+
+ /**
+ * Validates that the value is false
+ * @param val object to test
+ * @param msg message to output if validation fails
+ */
+ public static void isFalse(boolean val, String msg) {
+ if (val)
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ * Validates that the array contains no null elements
+ * @param objects the array to test
+ */
+ public static void noNullElements(Object[] objects) {
+ noNullElements(objects, "Array must not contain any null objects");
+ }
+
+ /**
+ * Validates that the array contains no null elements
+ * @param objects the array to test
+ * @param msg message to output if validation fails
+ */
+ public static void noNullElements(Object[] objects, String msg) {
+ for (Object obj : objects)
+ if (obj == null)
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ * Validates that the string is not empty
+ * @param string the string to test
+ */
+ public static void notEmpty(String string) {
+ if (string == null || string.length() == 0)
+ throw new IllegalArgumentException("String must not be empty");
+ }
+
+ /**
+ * Validates that the string is not empty
+ * @param string the string to test
+ * @param msg message to output if validation fails
+ */
+ public static void notEmpty(String string, String msg) {
+ if (string == null || string.length() == 0)
+ throw new IllegalArgumentException(msg);
+ }
+
+ /**
+ Cause a failure.
+ @param msg message to output.
+ */
+ public static void fail(String msg) {
+ throw new IllegalArgumentException(msg);
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Attribute.java b/server/src/org/jsoup/nodes/Attribute.java
new file mode 100644
index 0000000000..02eb29db83
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Attribute.java
@@ -0,0 +1,131 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.Validate;
+
+import java.util.Map;
+
+/**
+ A single key + value attribute. Keys are trimmed and normalised to lower-case.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class Attribute implements Map.Entry<String, String>, Cloneable {
+ private String key;
+ private String value;
+
+ /**
+ * Create a new attribute from unencoded (raw) key and value.
+ * @param key attribute key
+ * @param value attribute value
+ * @see #createFromEncoded
+ */
+ public Attribute(String key, String value) {
+ Validate.notEmpty(key);
+ Validate.notNull(value);
+ this.key = key.trim().toLowerCase();
+ this.value = value;
+ }
+
+ /**
+ Get the attribute key.
+ @return the attribute key
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ Set the attribute key. Gets normalised as per the constructor method.
+ @param key the new key; must not be null
+ */
+ public void setKey(String key) {
+ Validate.notEmpty(key);
+ this.key = key.trim().toLowerCase();
+ }
+
+ /**
+ Get the attribute value.
+ @return the attribute value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ Set the attribute value.
+ @param value the new attribute value; must not be null
+ */
+ public String setValue(String value) {
+ Validate.notNull(value);
+ String old = this.value;
+ this.value = value;
+ return old;
+ }
+
+ /**
+ Get the HTML representation of this attribute; e.g. {@code href="index.html"}.
+ @return HTML
+ */
+ public String html() {
+ return key + "=\"" + Entities.escape(value, (new Document("")).outputSettings()) + "\"";
+ }
+
+ protected void html(StringBuilder accum, Document.OutputSettings out) {
+ accum
+ .append(key)
+ .append("=\"")
+ .append(Entities.escape(value, out))
+ .append("\"");
+ }
+
+ /**
+ Get the string representation of this attribute, implemented as {@link #html()}.
+ @return string
+ */
+ public String toString() {
+ return html();
+ }
+
+ /**
+ * Create a new Attribute from an unencoded key and a HTML attribute encoded value.
+ * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars.
+ * @param encodedValue HTML attribute encoded value
+ * @return attribute
+ */
+ public static Attribute createFromEncoded(String unencodedKey, String encodedValue) {
+ String value = Entities.unescape(encodedValue, true);
+ return new Attribute(unencodedKey, value);
+ }
+
+ protected boolean isDataAttribute() {
+ return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Attribute)) return false;
+
+ Attribute attribute = (Attribute) o;
+
+ if (key != null ? !key.equals(attribute.key) : attribute.key != null) return false;
+ if (value != null ? !value.equals(attribute.value) : attribute.value != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = key != null ? key.hashCode() : 0;
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public Attribute clone() {
+ try {
+ return (Attribute) super.clone(); // only fields are immutable strings key and value, so no more deep copy required
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Attributes.java b/server/src/org/jsoup/nodes/Attributes.java
new file mode 100644
index 0000000000..9436750fc9
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Attributes.java
@@ -0,0 +1,249 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.Validate;
+
+import java.util.*;
+
+/**
+ * The attributes of an Element.
+ * <p/>
+ * Attributes are treated as a map: there can be only one value associated with an attribute key.
+ * <p/>
+ * Attribute key and value comparisons are done case insensitively, and keys are normalised to
+ * lower-case.
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ */
+public class Attributes implements Iterable<Attribute>, Cloneable {
+ protected static final String dataPrefix = "data-";
+
+ private LinkedHashMap<String, Attribute> attributes = null;
+ // linked hash map to preserve insertion order.
+ // null be default as so many elements have no attributes -- saves a good chunk of memory
+
+ /**
+ Get an attribute value by key.
+ @param key the attribute key
+ @return the attribute value if set; or empty string if not set.
+ @see #hasKey(String)
+ */
+ public String get(String key) {
+ Validate.notEmpty(key);
+
+ if (attributes == null)
+ return "";
+
+ Attribute attr = attributes.get(key.toLowerCase());
+ return attr != null ? attr.getValue() : "";
+ }
+
+ /**
+ Set a new attribute, or replace an existing one by key.
+ @param key attribute key
+ @param value attribute value
+ */
+ public void put(String key, String value) {
+ Attribute attr = new Attribute(key, value);
+ put(attr);
+ }
+
+ /**
+ Set a new attribute, or replace an existing one by key.
+ @param attribute attribute
+ */
+ public void put(Attribute attribute) {
+ Validate.notNull(attribute);
+ if (attributes == null)
+ attributes = new LinkedHashMap<String, Attribute>(2);
+ attributes.put(attribute.getKey(), attribute);
+ }
+
+ /**
+ Remove an attribute by key.
+ @param key attribute key to remove
+ */
+ public void remove(String key) {
+ Validate.notEmpty(key);
+ if (attributes == null)
+ return;
+ attributes.remove(key.toLowerCase());
+ }
+
+ /**
+ Tests if these attributes contain an attribute with this key.
+ @param key key to check for
+ @return true if key exists, false otherwise
+ */
+ public boolean hasKey(String key) {
+ return attributes != null && attributes.containsKey(key.toLowerCase());
+ }
+
+ /**
+ Get the number of attributes in this set.
+ @return size
+ */
+ public int size() {
+ if (attributes == null)
+ return 0;
+ return attributes.size();
+ }
+
+ /**
+ Add all the attributes from the incoming set to this set.
+ @param incoming attributes to add to these attributes.
+ */
+ public void addAll(Attributes incoming) {
+ if (incoming.size() == 0)
+ return;
+ if (attributes == null)
+ attributes = new LinkedHashMap<String, Attribute>(incoming.size());
+ attributes.putAll(incoming.attributes);
+ }
+
+ public Iterator<Attribute> iterator() {
+ return asList().iterator();
+ }
+
+ /**
+ Get the attributes as a List, for iteration. Do not modify the keys of the attributes via this view, as changes
+ to keys will not be recognised in the containing set.
+ @return an view of the attributes as a List.
+ */
+ public List<Attribute> asList() {
+ if (attributes == null)
+ return Collections.emptyList();
+
+ List<Attribute> list = new ArrayList<Attribute>(attributes.size());
+ for (Map.Entry<String, Attribute> entry : attributes.entrySet()) {
+ list.add(entry.getValue());
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ /**
+ * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys
+ * starting with {@code data-}.
+ * @return map of custom data attributes.
+ */
+ public Map<String, String> dataset() {
+ return new Dataset();
+ }
+
+ /**
+ Get the HTML representation of these attributes.
+ @return HTML
+ */
+ public String html() {
+ StringBuilder accum = new StringBuilder();
+ html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used
+ return accum.toString();
+ }
+
+ void html(StringBuilder accum, Document.OutputSettings out) {
+ if (attributes == null)
+ return;
+
+ for (Map.Entry<String, Attribute> entry : attributes.entrySet()) {
+ Attribute attribute = entry.getValue();
+ accum.append(" ");
+ attribute.html(accum, out);
+ }
+ }
+
+ public String toString() {
+ return html();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Attributes)) return false;
+
+ Attributes that = (Attributes) o;
+
+ if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return attributes != null ? attributes.hashCode() : 0;
+ }
+
+ @Override
+ public Attributes clone() {
+ if (attributes == null)
+ return new Attributes();
+
+ Attributes clone;
+ try {
+ clone = (Attributes) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ clone.attributes = new LinkedHashMap<String, Attribute>(attributes.size());
+ for (Attribute attribute: this)
+ clone.attributes.put(attribute.getKey(), attribute.clone());
+ return clone;
+ }
+
+ private class Dataset extends AbstractMap<String, String> {
+
+ private Dataset() {
+ if (attributes == null)
+ attributes = new LinkedHashMap<String, Attribute>(2);
+ }
+
+ public Set<Entry<String, String>> entrySet() {
+ return new EntrySet();
+ }
+
+ @Override
+ public String put(String key, String value) {
+ String dataKey = dataKey(key);
+ String oldValue = hasKey(dataKey) ? attributes.get(dataKey).getValue() : null;
+ Attribute attr = new Attribute(dataKey, value);
+ attributes.put(dataKey, attr);
+ return oldValue;
+ }
+
+ private class EntrySet extends AbstractSet<Map.Entry<String, String>> {
+ public Iterator<Map.Entry<String, String>> iterator() {
+ return new DatasetIterator();
+ }
+
+ public int size() {
+ int count = 0;
+ Iterator iter = new DatasetIterator();
+ while (iter.hasNext())
+ count++;
+ return count;
+ }
+ }
+
+ private class DatasetIterator implements Iterator<Map.Entry<String, String>> {
+ private Iterator<Attribute> attrIter = attributes.values().iterator();
+ private Attribute attr;
+ public boolean hasNext() {
+ while (attrIter.hasNext()) {
+ attr = attrIter.next();
+ if (attr.isDataAttribute()) return true;
+ }
+ return false;
+ }
+
+ public Entry<String, String> next() {
+ return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
+ }
+
+ public void remove() {
+ attributes.remove(attr.getKey());
+ }
+ }
+ }
+
+ private static String dataKey(String key) {
+ return dataPrefix + key;
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Comment.java b/server/src/org/jsoup/nodes/Comment.java
new file mode 100644
index 0000000000..37fd4368fa
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Comment.java
@@ -0,0 +1,46 @@
+package org.jsoup.nodes;
+
+/**
+ A comment node.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class Comment extends Node {
+ private static final String COMMENT_KEY = "comment";
+
+ /**
+ Create a new comment node.
+ @param data The contents of the comment
+ @param baseUri base URI
+ */
+ public Comment(String data, String baseUri) {
+ super(baseUri);
+ attributes.put(COMMENT_KEY, data);
+ }
+
+ public String nodeName() {
+ return "#comment";
+ }
+
+ /**
+ Get the contents of the comment.
+ @return comment content
+ */
+ public String getData() {
+ return attributes.get(COMMENT_KEY);
+ }
+
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ if (out.prettyPrint())
+ indent(accum, depth, out);
+ accum
+ .append("<!--")
+ .append(getData())
+ .append("-->");
+ }
+
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {}
+
+ public String toString() {
+ return outerHtml();
+ }
+}
diff --git a/server/src/org/jsoup/nodes/DataNode.java b/server/src/org/jsoup/nodes/DataNode.java
new file mode 100644
index 0000000000..a64f56f0a4
--- /dev/null
+++ b/server/src/org/jsoup/nodes/DataNode.java
@@ -0,0 +1,62 @@
+package org.jsoup.nodes;
+
+/**
+ A data node, for contents of style, script tags etc, where contents should not show in text().
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class DataNode extends Node{
+ private static final String DATA_KEY = "data";
+
+ /**
+ Create a new DataNode.
+ @param data data contents
+ @param baseUri base URI
+ */
+ public DataNode(String data, String baseUri) {
+ super(baseUri);
+ attributes.put(DATA_KEY, data);
+ }
+
+ public String nodeName() {
+ return "#data";
+ }
+
+ /**
+ Get the data contents of this node. Will be unescaped and with original new lines, space etc.
+ @return data
+ */
+ public String getWholeData() {
+ return attributes.get(DATA_KEY);
+ }
+
+ /**
+ * Set the data contents of this node.
+ * @param data unencoded data
+ * @return this node, for chaining
+ */
+ public DataNode setWholeData(String data) {
+ attributes.put(DATA_KEY, data);
+ return this;
+ }
+
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ accum.append(getWholeData()); // data is not escaped in return from data nodes, so " in script, style is plain
+ }
+
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {}
+
+ public String toString() {
+ return outerHtml();
+ }
+
+ /**
+ Create a new DataNode from HTML encoded data.
+ @param encodedData encoded data
+ @param baseUri bass URI
+ @return new DataNode
+ */
+ public static DataNode createFromEncoded(String encodedData, String baseUri) {
+ String data = Entities.unescape(encodedData);
+ return new DataNode(data, baseUri);
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Document.java b/server/src/org/jsoup/nodes/Document.java
new file mode 100644
index 0000000000..adb371ce14
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Document.java
@@ -0,0 +1,350 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.parser.Tag;
+import org.jsoup.select.Elements;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ A HTML Document.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class Document extends Element {
+ private OutputSettings outputSettings = new OutputSettings();
+ private QuirksMode quirksMode = QuirksMode.noQuirks;
+
+ /**
+ Create a new, empty Document.
+ @param baseUri base URI of document
+ @see org.jsoup.Jsoup#parse
+ @see #createShell
+ */
+ public Document(String baseUri) {
+ super(Tag.valueOf("#root"), baseUri);
+ }
+
+ /**
+ Create a valid, empty shell of a document, suitable for adding more elements to.
+ @param baseUri baseUri of document
+ @return document with html, head, and body elements.
+ */
+ static public Document createShell(String baseUri) {
+ Validate.notNull(baseUri);
+
+ Document doc = new Document(baseUri);
+ Element html = doc.appendElement("html");
+ html.appendElement("head");
+ html.appendElement("body");
+
+ return doc;
+ }
+
+ /**
+ Accessor to the document's {@code head} element.
+ @return {@code head}
+ */
+ public Element head() {
+ return findFirstElementByTagName("head", this);
+ }
+
+ /**
+ Accessor to the document's {@code body} element.
+ @return {@code body}
+ */
+ public Element body() {
+ return findFirstElementByTagName("body", this);
+ }
+
+ /**
+ Get the string contents of the document's {@code title} element.
+ @return Trimmed title, or empty string if none set.
+ */
+ public String title() {
+ Element titleEl = getElementsByTag("title").first();
+ return titleEl != null ? titleEl.text().trim() : "";
+ }
+
+ /**
+ Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if
+ not present
+ @param title string to set as title
+ */
+ public void title(String title) {
+ Validate.notNull(title);
+ Element titleEl = getElementsByTag("title").first();
+ if (titleEl == null) { // add to head
+ head().appendElement("title").text(title);
+ } else {
+ titleEl.text(title);
+ }
+ }
+
+ /**
+ Create a new Element, with this document's base uri. Does not make the new element a child of this document.
+ @param tagName element tag name (e.g. {@code a})
+ @return new element
+ */
+ public Element createElement(String tagName) {
+ return new Element(Tag.valueOf(tagName), this.baseUri());
+ }
+
+ /**
+ Normalise the document. This happens after the parse phase so generally does not need to be called.
+ Moves any text content that is not in the body element into the body.
+ @return this document after normalisation
+ */
+ public Document normalise() {
+ Element htmlEl = findFirstElementByTagName("html", this);
+ if (htmlEl == null)
+ htmlEl = appendElement("html");
+ if (head() == null)
+ htmlEl.prependElement("head");
+ if (body() == null)
+ htmlEl.appendElement("body");
+
+ // pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care
+ // of. do in inverse order to maintain text order.
+ normaliseTextNodes(head());
+ normaliseTextNodes(htmlEl);
+ normaliseTextNodes(this);
+
+ normaliseStructure("head", htmlEl);
+ normaliseStructure("body", htmlEl);
+
+ return this;
+ }
+
+ // does not recurse.
+ private void normaliseTextNodes(Element element) {
+ List<Node> toMove = new ArrayList<Node>();
+ for (Node node: element.childNodes) {
+ if (node instanceof TextNode) {
+ TextNode tn = (TextNode) node;
+ if (!tn.isBlank())
+ toMove.add(tn);
+ }
+ }
+
+ for (int i = toMove.size()-1; i >= 0; i--) {
+ Node node = toMove.get(i);
+ element.removeChild(node);
+ body().prependChild(new TextNode(" ", ""));
+ body().prependChild(node);
+ }
+ }
+
+ // merge multiple <head> or <body> contents into one, delete the remainder, and ensure they are owned by <html>
+ private void normaliseStructure(String tag, Element htmlEl) {
+ Elements elements = this.getElementsByTag(tag);
+ Element master = elements.first(); // will always be available as created above if not existent
+ if (elements.size() > 1) { // dupes, move contents to master
+ List<Node> toMove = new ArrayList<Node>();
+ for (int i = 1; i < elements.size(); i++) {
+ Node dupe = elements.get(i);
+ for (Node node : dupe.childNodes)
+ toMove.add(node);
+ dupe.remove();
+ }
+
+ for (Node dupe : toMove)
+ master.appendChild(dupe);
+ }
+ // ensure parented by <html>
+ if (!master.parent().equals(htmlEl)) {
+ htmlEl.appendChild(master); // includes remove()
+ }
+ }
+
+ // fast method to get first by tag name, used for html, head, body finders
+ private Element findFirstElementByTagName(String tag, Node node) {
+ if (node.nodeName().equals(tag))
+ return (Element) node;
+ else {
+ for (Node child: node.childNodes) {
+ Element found = findFirstElementByTagName(tag, child);
+ if (found != null)
+ return found;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String outerHtml() {
+ return super.html(); // no outer wrapper tag
+ }
+
+ /**
+ Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared.
+ @param text unencoded text
+ @return this document
+ */
+ @Override
+ public Element text(String text) {
+ body().text(text); // overridden to not nuke doc structure
+ return this;
+ }
+
+ @Override
+ public String nodeName() {
+ return "#document";
+ }
+
+ @Override
+ public Document clone() {
+ Document clone = (Document) super.clone();
+ clone.outputSettings = this.outputSettings.clone();
+ return clone;
+ }
+
+ /**
+ * A Document's output settings control the form of the text() and html() methods.
+ */
+ public static class OutputSettings implements Cloneable {
+ private Entities.EscapeMode escapeMode = Entities.EscapeMode.base;
+ private Charset charset = Charset.forName("UTF-8");
+ private CharsetEncoder charsetEncoder = charset.newEncoder();
+ private boolean prettyPrint = true;
+ private int indentAmount = 1;
+
+ public OutputSettings() {}
+
+ /**
+ * Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML
+ * entities and escapes other characters as numbered entities for maximum compatibility; or <code>extended</code>,
+ * which uses the complete set of HTML named entities.
+ * <p>
+ * The default escape mode is <code>base</code>.
+ * @return the document's current escape mode
+ */
+ public Entities.EscapeMode escapeMode() {
+ return escapeMode;
+ }
+
+ /**
+ * Set the document's escape mode
+ * @param escapeMode the new escape mode to use
+ * @return the document's output settings, for chaining
+ */
+ public OutputSettings escapeMode(Entities.EscapeMode escapeMode) {
+ this.escapeMode = escapeMode;
+ return this;
+ }
+
+ /**
+ * Get the document's current output charset, which is used to control which characters are escaped when
+ * generating HTML (via the <code>html()</code> methods), and which are kept intact.
+ * <p>
+ * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the
+ * input charset. Otherwise, it defaults to UTF-8.
+ * @return the document's current charset.
+ */
+ public Charset charset() {
+ return charset;
+ }
+
+ /**
+ * Update the document's output charset.
+ * @param charset the new charset to use.
+ * @return the document's output settings, for chaining
+ */
+ public OutputSettings charset(Charset charset) {
+ // todo: this should probably update the doc's meta charset
+ this.charset = charset;
+ charsetEncoder = charset.newEncoder();
+ return this;
+ }
+
+ /**
+ * Update the document's output charset.
+ * @param charset the new charset (by name) to use.
+ * @return the document's output settings, for chaining
+ */
+ public OutputSettings charset(String charset) {
+ charset(Charset.forName(charset));
+ return this;
+ }
+
+ CharsetEncoder encoder() {
+ return charsetEncoder;
+ }
+
+ /**
+ * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format
+ * the output, and the output will generally look like the input.
+ * @return if pretty printing is enabled.
+ */
+ public boolean prettyPrint() {
+ return prettyPrint;
+ }
+
+ /**
+ * Enable or disable pretty printing.
+ * @param pretty new pretty print setting
+ * @return this, for chaining
+ */
+ public OutputSettings prettyPrint(boolean pretty) {
+ prettyPrint = pretty;
+ return this;
+ }
+
+ /**
+ * Get the current tag indent amount, used when pretty printing.
+ * @return the current indent amount
+ */
+ public int indentAmount() {
+ return indentAmount;
+ }
+
+ /**
+ * Set the indent amount for pretty printing
+ * @param indentAmount number of spaces to use for indenting each level. Must be >= 0.
+ * @return this, for chaining
+ */
+ public OutputSettings indentAmount(int indentAmount) {
+ Validate.isTrue(indentAmount >= 0);
+ this.indentAmount = indentAmount;
+ return this;
+ }
+
+ @Override
+ public OutputSettings clone() {
+ OutputSettings clone;
+ try {
+ clone = (OutputSettings) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ clone.charset(charset.name()); // new charset and charset encoder
+ clone.escapeMode = Entities.EscapeMode.valueOf(escapeMode.name());
+ // indentAmount, prettyPrint are primitives so object.clone() will handle
+ return clone;
+ }
+ }
+
+ /**
+ * Get the document's current output settings.
+ * @return the document's current output settings.
+ */
+ public OutputSettings outputSettings() {
+ return outputSettings;
+ }
+
+ public enum QuirksMode {
+ noQuirks, quirks, limitedQuirks;
+ }
+
+ public QuirksMode quirksMode() {
+ return quirksMode;
+ }
+
+ public Document quirksMode(QuirksMode quirksMode) {
+ this.quirksMode = quirksMode;
+ return this;
+ }
+}
+
diff --git a/server/src/org/jsoup/nodes/DocumentType.java b/server/src/org/jsoup/nodes/DocumentType.java
new file mode 100644
index 0000000000..f8c79f0d18
--- /dev/null
+++ b/server/src/org/jsoup/nodes/DocumentType.java
@@ -0,0 +1,46 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+
+/**
+ * A {@code <!DOCTPYE>} node.
+ */
+public class DocumentType extends Node {
+ // todo: quirk mode from publicId and systemId
+
+ /**
+ * Create a new doctype element.
+ * @param name the doctype's name
+ * @param publicId the doctype's public ID
+ * @param systemId the doctype's system ID
+ * @param baseUri the doctype's base URI
+ */
+ public DocumentType(String name, String publicId, String systemId, String baseUri) {
+ super(baseUri);
+
+ Validate.notEmpty(name);
+ attr("name", name);
+ attr("publicId", publicId);
+ attr("systemId", systemId);
+ }
+
+ @Override
+ public String nodeName() {
+ return "#doctype";
+ }
+
+ @Override
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ accum.append("<!DOCTYPE ").append(attr("name"));
+ if (!StringUtil.isBlank(attr("publicId")))
+ accum.append(" PUBLIC \"").append(attr("publicId")).append("\"");
+ if (!StringUtil.isBlank(attr("systemId")))
+ accum.append(" \"").append(attr("systemId")).append("\"");
+ accum.append('>');
+ }
+
+ @Override
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Element.java b/server/src/org/jsoup/nodes/Element.java
new file mode 100644
index 0000000000..5c1894c934
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Element.java
@@ -0,0 +1,1119 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+import org.jsoup.parser.Parser;
+import org.jsoup.parser.Tag;
+import org.jsoup.select.Collector;
+import org.jsoup.select.Elements;
+import org.jsoup.select.Evaluator;
+import org.jsoup.select.Selector;
+
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * A HTML element consists of a tag name, attributes, and child nodes (including text nodes and
+ * other elements).
+ *
+ * From an Element, you can extract data, traverse the node graph, and manipulate the HTML.
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ */
+public class Element extends Node {
+ private Tag tag;
+ private Set<String> classNames;
+
+ /**
+ * Create a new, standalone Element. (Standalone in that is has no parent.)
+ *
+ * @param tag tag of this element
+ * @param baseUri the base URI
+ * @param attributes initial attributes
+ * @see #appendChild(Node)
+ * @see #appendElement(String)
+ */
+ public Element(Tag tag, String baseUri, Attributes attributes) {
+ super(baseUri, attributes);
+
+ Validate.notNull(tag);
+ this.tag = tag;
+ }
+
+ /**
+ * Create a new Element from a tag and a base URI.
+ *
+ * @param tag element tag
+ * @param baseUri the base URI of this element. It is acceptable for the base URI to be an empty
+ * string, but not null.
+ * @see Tag#valueOf(String)
+ */
+ public Element(Tag tag, String baseUri) {
+ this(tag, baseUri, new Attributes());
+ }
+
+ @Override
+ public String nodeName() {
+ return tag.getName();
+ }
+
+ /**
+ * Get the name of the tag for this element. E.g. {@code div}
+ *
+ * @return the tag name
+ */
+ public String tagName() {
+ return tag.getName();
+ }
+
+ /**
+ * Change the tag of this element. For example, convert a {@code <span>} to a {@code <div>} with
+ * {@code el.tagName("div");}.
+ *
+ * @param tagName new tag name for this element
+ * @return this element, for chaining
+ */
+ public Element tagName(String tagName) {
+ Validate.notEmpty(tagName, "Tag name must not be empty.");
+ tag = Tag.valueOf(tagName);
+ return this;
+ }
+
+ /**
+ * Get the Tag for this element.
+ *
+ * @return the tag object
+ */
+ public Tag tag() {
+ return tag;
+ }
+
+ /**
+ * Test if this element is a block-level element. (E.g. {@code <div> == true} or an inline element
+ * {@code <p> == false}).
+ *
+ * @return true if block, false if not (and thus inline)
+ */
+ public boolean isBlock() {
+ return tag.isBlock();
+ }
+
+ /**
+ * Get the {@code id} attribute of this element.
+ *
+ * @return The id attribute, if present, or an empty string if not.
+ */
+ public String id() {
+ String id = attr("id");
+ return id == null ? "" : id;
+ }
+
+ /**
+ * Set an attribute value on this element. If this element already has an attribute with the
+ * key, its value is updated; otherwise, a new attribute is added.
+ *
+ * @return this element
+ */
+ public Element attr(String attributeKey, String attributeValue) {
+ super.attr(attributeKey, attributeValue);
+ return this;
+ }
+
+ /**
+ * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key
+ * starting with "data-" is included the dataset.
+ * <p>
+ * E.g., the element {@code <div data-package="jsoup" data-language="Java" class="group">...} has the dataset
+ * {@code package=jsoup, language=java}.
+ * <p>
+ * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected
+ * in the other map.
+ * <p>
+ * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector.
+ * @return a map of {@code key=value} custom data attributes.
+ */
+ public Map<String, String> dataset() {
+ return attributes.dataset();
+ }
+
+ @Override
+ public final Element parent() {
+ return (Element) parentNode;
+ }
+
+ /**
+ * Get this element's parent and ancestors, up to the document root.
+ * @return this element's stack of parents, closest first.
+ */
+ public Elements parents() {
+ Elements parents = new Elements();
+ accumulateParents(this, parents);
+ return parents;
+ }
+
+ private static void accumulateParents(Element el, Elements parents) {
+ Element parent = el.parent();
+ if (parent != null && !parent.tagName().equals("#root")) {
+ parents.add(parent);
+ accumulateParents(parent, parents);
+ }
+ }
+
+ /**
+ * Get a child element of this element, by its 0-based index number.
+ * <p/>
+ * Note that an element can have both mixed Nodes and Elements as children. This method inspects
+ * a filtered list of children that are elements, and the index is based on that filtered list.
+ *
+ * @param index the index number of the element to retrieve
+ * @return the child element, if it exists, or {@code null} if absent.
+ * @see #childNode(int)
+ */
+ public Element child(int index) {
+ return children().get(index);
+ }
+
+ /**
+ * Get this element's child elements.
+ * <p/>
+ * This is effectively a filter on {@link #childNodes()} to get Element nodes.
+ * @return child elements. If this element has no children, returns an
+ * empty list.
+ * @see #childNodes()
+ */
+ public Elements children() {
+ // create on the fly rather than maintaining two lists. if gets slow, memoize, and mark dirty on change
+ List<Element> elements = new ArrayList<Element>();
+ for (Node node : childNodes) {
+ if (node instanceof Element)
+ elements.add((Element) node);
+ }
+ return new Elements(elements);
+ }
+
+ /**
+ * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated.
+ * <p/>
+ * This is effectively a filter on {@link #childNodes()} to get Text nodes.
+ * @return child text nodes. If this element has no text nodes, returns an
+ * empty list.
+ * <p/>
+ * For example, with the input HTML: {@code <p>One <span>Two</span> Three <br> Four</p>} with the {@code p} element selected:
+ * <ul>
+ * <li>{@code p.text()} = {@code "One Two Three Four"}</li>
+ * <li>{@code p.ownText()} = {@code "One Three Four"}</li>
+ * <li>{@code p.children()} = {@code Elements[<span>, <br>]}</li>
+ * <li>{@code p.childNodes()} = {@code List<Node>["One ", <span>, " Three ", <br>, " Four"]}</li>
+ * <li>{@code p.textNodes()} = {@code List<TextNode>["One ", " Three ", " Four"]}</li>
+ * </ul>
+ */
+ public List<TextNode> textNodes() {
+ List<TextNode> textNodes = new ArrayList<TextNode>();
+ for (Node node : childNodes) {
+ if (node instanceof TextNode)
+ textNodes.add((TextNode) node);
+ }
+ return Collections.unmodifiableList(textNodes);
+ }
+
+ /**
+ * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated.
+ * <p/>
+ * This is effectively a filter on {@link #childNodes()} to get Data nodes.
+ * @return child data nodes. If this element has no data nodes, returns an
+ * empty list.
+ * @see #data()
+ */
+ public List<DataNode> dataNodes() {
+ List<DataNode> dataNodes = new ArrayList<DataNode>();
+ for (Node node : childNodes) {
+ if (node instanceof DataNode)
+ dataNodes.add((DataNode) node);
+ }
+ return Collections.unmodifiableList(dataNodes);
+ }
+
+ /**
+ * Find elements that match the {@link Selector} CSS query, with this element as the starting context. Matched elements
+ * may include this element, or any of its children.
+ * <p/>
+ * This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because
+ * multiple filters can be combined, e.g.:
+ * <ul>
+ * <li>{@code el.select("a[href]")} - finds links ({@code a} tags with {@code href} attributes)
+ * <li>{@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely)
+ * </ul>
+ * <p/>
+ * See the query syntax documentation in {@link org.jsoup.select.Selector}.
+ *
+ * @param cssQuery a {@link Selector} CSS-like query
+ * @return elements that match the query (empty if none match)
+ * @see org.jsoup.select.Selector
+ */
+ public Elements select(String cssQuery) {
+ return Selector.select(cssQuery, this);
+ }
+
+ /**
+ * Add a node child node to this element.
+ *
+ * @param child node to add. Must not already have a parent.
+ * @return this element, so that you can add more child nodes or elements.
+ */
+ public Element appendChild(Node child) {
+ Validate.notNull(child);
+
+ addChildren(child);
+ return this;
+ }
+
+ /**
+ * Add a node to the start of this element's children.
+ *
+ * @param child node to add. Must not already have a parent.
+ * @return this element, so that you can add more child nodes or elements.
+ */
+ public Element prependChild(Node child) {
+ Validate.notNull(child);
+
+ addChildren(0, child);
+ return this;
+ }
+
+ /**
+ * Create a new element by tag name, and add it as the last child.
+ *
+ * @param tagName the name of the tag (e.g. {@code div}).
+ * @return the new element, to allow you to add content to it, e.g.:
+ * {@code parent.appendElement("h1").attr("id", "header").text("Welcome");}
+ */
+ public Element appendElement(String tagName) {
+ Element child = new Element(Tag.valueOf(tagName), baseUri());
+ appendChild(child);
+ return child;
+ }
+
+ /**
+ * Create a new element by tag name, and add it as the first child.
+ *
+ * @param tagName the name of the tag (e.g. {@code div}).
+ * @return the new element, to allow you to add content to it, e.g.:
+ * {@code parent.prependElement("h1").attr("id", "header").text("Welcome");}
+ */
+ public Element prependElement(String tagName) {
+ Element child = new Element(Tag.valueOf(tagName), baseUri());
+ prependChild(child);
+ return child;
+ }
+
+ /**
+ * Create and append a new TextNode to this element.
+ *
+ * @param text the unencoded text to add
+ * @return this element
+ */
+ public Element appendText(String text) {
+ TextNode node = new TextNode(text, baseUri());
+ appendChild(node);
+ return this;
+ }
+
+ /**
+ * Create and prepend a new TextNode to this element.
+ *
+ * @param text the unencoded text to add
+ * @return this element
+ */
+ public Element prependText(String text) {
+ TextNode node = new TextNode(text, baseUri());
+ prependChild(node);
+ return this;
+ }
+
+ /**
+ * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children.
+ * @param html HTML to add inside this element, after the existing HTML
+ * @return this element
+ * @see #html(String)
+ */
+ public Element append(String html) {
+ Validate.notNull(html);
+
+ List<Node> nodes = Parser.parseFragment(html, this, baseUri());
+ addChildren(nodes.toArray(new Node[nodes.size()]));
+ return this;
+ }
+
+ /**
+ * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children.
+ * @param html HTML to add inside this element, before the existing HTML
+ * @return this element
+ * @see #html(String)
+ */
+ public Element prepend(String html) {
+ Validate.notNull(html);
+
+ List<Node> nodes = Parser.parseFragment(html, this, baseUri());
+ addChildren(0, nodes.toArray(new Node[nodes.size()]));
+ return this;
+ }
+
+ /**
+ * Insert the specified HTML into the DOM before this element (i.e. as a preceding sibling).
+ *
+ * @param html HTML to add before this element
+ * @return this element, for chaining
+ * @see #after(String)
+ */
+ @Override
+ public Element before(String html) {
+ return (Element) super.before(html);
+ }
+
+ /**
+ * Insert the specified node into the DOM before this node (i.e. as a preceding sibling).
+ * @param node to add before this element
+ * @return this Element, for chaining
+ * @see #after(Node)
+ */
+ @Override
+ public Element before(Node node) {
+ return (Element) super.before(node);
+ }
+
+ /**
+ * Insert the specified HTML into the DOM after this element (i.e. as a following sibling).
+ *
+ * @param html HTML to add after this element
+ * @return this element, for chaining
+ * @see #before(String)
+ */
+ @Override
+ public Element after(String html) {
+ return (Element) super.after(html);
+ }
+
+ /**
+ * Insert the specified node into the DOM after this node (i.e. as a following sibling).
+ * @param node to add after this element
+ * @return this element, for chaining
+ * @see #before(Node)
+ */
+ @Override
+ public Element after(Node node) {
+ return (Element) super.after(node);
+ }
+
+ /**
+ * Remove all of the element's child nodes. Any attributes are left as-is.
+ * @return this element
+ */
+ public Element empty() {
+ childNodes.clear();
+ return this;
+ }
+
+ /**
+ * Wrap the supplied HTML around this element.
+ *
+ * @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
+ * @return this element, for chaining.
+ */
+ @Override
+ public Element wrap(String html) {
+ return (Element) super.wrap(html);
+ }
+
+ /**
+ * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling
+ * of itself, so will not be included in the returned list.
+ * @return sibling elements
+ */
+ public Elements siblingElements() {
+ if (parentNode == null)
+ return new Elements(0);
+
+ List<Element> elements = parent().children();
+ Elements siblings = new Elements(elements.size() - 1);
+ for (Element el: elements)
+ if (el != this)
+ siblings.add(el);
+ return siblings;
+ }
+
+ /**
+ * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s,
+ * the {@code nextElementSibling} of the first {@code p} is the second {@code p}.
+ * <p/>
+ * This is similar to {@link #nextSibling()}, but specifically finds only Elements
+ * @return the next element, or null if there is no next element
+ * @see #previousElementSibling()
+ */
+ public Element nextElementSibling() {
+ if (parentNode == null) return null;
+ List<Element> siblings = parent().children();
+ Integer index = indexInList(this, siblings);
+ Validate.notNull(index);
+ if (siblings.size() > index+1)
+ return siblings.get(index+1);
+ else
+ return null;
+ }
+
+ /**
+ * Gets the previous element sibling of this element.
+ * @return the previous element, or null if there is no previous element
+ * @see #nextElementSibling()
+ */
+ public Element previousElementSibling() {
+ if (parentNode == null) return null;
+ List<Element> siblings = parent().children();
+ Integer index = indexInList(this, siblings);
+ Validate.notNull(index);
+ if (index > 0)
+ return siblings.get(index-1);
+ else
+ return null;
+ }
+
+ /**
+ * Gets the first element sibling of this element.
+ * @return the first sibling that is an element (aka the parent's first element child)
+ */
+ public Element firstElementSibling() {
+ // todo: should firstSibling() exclude this?
+ List<Element> siblings = parent().children();
+ return siblings.size() > 1 ? siblings.get(0) : null;
+ }
+
+ /**
+ * Get the list index of this element in its element sibling list. I.e. if this is the first element
+ * sibling, returns 0.
+ * @return position in element sibling list
+ */
+ public Integer elementSiblingIndex() {
+ if (parent() == null) return 0;
+ return indexInList(this, parent().children());
+ }
+
+ /**
+ * Gets the last element sibling of this element
+ * @return the last sibling that is an element (aka the parent's last element child)
+ */
+ public Element lastElementSibling() {
+ List<Element> siblings = parent().children();
+ return siblings.size() > 1 ? siblings.get(siblings.size() - 1) : null;
+ }
+
+ private static <E extends Element> Integer indexInList(Element search, List<E> elements) {
+ Validate.notNull(search);
+ Validate.notNull(elements);
+
+ for (int i = 0; i < elements.size(); i++) {
+ E element = elements.get(i);
+ if (element.equals(search))
+ return i;
+ }
+ return null;
+ }
+
+ // DOM type methods
+
+ /**
+ * Finds elements, including and recursively under this element, with the specified tag name.
+ * @param tagName The tag name to search for (case insensitively).
+ * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match.
+ */
+ public Elements getElementsByTag(String tagName) {
+ Validate.notEmpty(tagName);
+ tagName = tagName.toLowerCase().trim();
+
+ return Collector.collect(new Evaluator.Tag(tagName), this);
+ }
+
+ /**
+ * Find an element by ID, including or under this element.
+ * <p>
+ * Note that this finds the first matching ID, starting with this element. If you search down from a different
+ * starting point, it is possible to find a different element by ID. For unique element by ID within a Document,
+ * use {@link Document#getElementById(String)}
+ * @param id The ID to search for.
+ * @return The first matching element by ID, starting with this element, or null if none found.
+ */
+ public Element getElementById(String id) {
+ Validate.notEmpty(id);
+
+ Elements elements = Collector.collect(new Evaluator.Id(id), this);
+ if (elements.size() > 0)
+ return elements.get(0);
+ else
+ return null;
+ }
+
+ /**
+ * Find elements that have this class, including or under this element. Case insensitive.
+ * <p>
+ * Elements can have multiple classes (e.g. {@code <div class="header round first">}. This method
+ * checks each class, so you can find the above with {@code el.getElementsByClass("header");}.
+ *
+ * @param className the name of the class to search for.
+ * @return elements with the supplied class name, empty if none
+ * @see #hasClass(String)
+ * @see #classNames()
+ */
+ public Elements getElementsByClass(String className) {
+ Validate.notEmpty(className);
+
+ return Collector.collect(new Evaluator.Class(className), this);
+ }
+
+ /**
+ * Find elements that have a named attribute set. Case insensitive.
+ *
+ * @param key name of the attribute, e.g. {@code href}
+ * @return elements that have this attribute, empty if none
+ */
+ public Elements getElementsByAttribute(String key) {
+ Validate.notEmpty(key);
+ key = key.trim().toLowerCase();
+
+ return Collector.collect(new Evaluator.Attribute(key), this);
+ }
+
+ /**
+ * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements
+ * that have HTML5 datasets.
+ * @param keyPrefix name prefix of the attribute e.g. {@code data-}
+ * @return elements that have attribute names that start with with the prefix, empty if none.
+ */
+ public Elements getElementsByAttributeStarting(String keyPrefix) {
+ Validate.notEmpty(keyPrefix);
+ keyPrefix = keyPrefix.trim().toLowerCase();
+
+ return Collector.collect(new Evaluator.AttributeStarting(keyPrefix), this);
+ }
+
+ /**
+ * Find elements that have an attribute with the specific value. Case insensitive.
+ *
+ * @param key name of the attribute
+ * @param value value of the attribute
+ * @return elements that have this attribute with this value, empty if none
+ */
+ public Elements getElementsByAttributeValue(String key, String value) {
+ return Collector.collect(new Evaluator.AttributeWithValue(key, value), this);
+ }
+
+ /**
+ * Find elements that either do not have this attribute, or have it with a different value. Case insensitive.
+ *
+ * @param key name of the attribute
+ * @param value value of the attribute
+ * @return elements that do not have a matching attribute
+ */
+ public Elements getElementsByAttributeValueNot(String key, String value) {
+ return Collector.collect(new Evaluator.AttributeWithValueNot(key, value), this);
+ }
+
+ /**
+ * Find elements that have attributes that start with the value prefix. Case insensitive.
+ *
+ * @param key name of the attribute
+ * @param valuePrefix start of attribute value
+ * @return elements that have attributes that start with the value prefix
+ */
+ public Elements getElementsByAttributeValueStarting(String key, String valuePrefix) {
+ return Collector.collect(new Evaluator.AttributeWithValueStarting(key, valuePrefix), this);
+ }
+
+ /**
+ * Find elements that have attributes that end with the value suffix. Case insensitive.
+ *
+ * @param key name of the attribute
+ * @param valueSuffix end of the attribute value
+ * @return elements that have attributes that end with the value suffix
+ */
+ public Elements getElementsByAttributeValueEnding(String key, String valueSuffix) {
+ return Collector.collect(new Evaluator.AttributeWithValueEnding(key, valueSuffix), this);
+ }
+
+ /**
+ * Find elements that have attributes whose value contains the match string. Case insensitive.
+ *
+ * @param key name of the attribute
+ * @param match substring of value to search for
+ * @return elements that have attributes containing this text
+ */
+ public Elements getElementsByAttributeValueContaining(String key, String match) {
+ return Collector.collect(new Evaluator.AttributeWithValueContaining(key, match), this);
+ }
+
+ /**
+ * Find elements that have attributes whose values match the supplied regular expression.
+ * @param key name of the attribute
+ * @param pattern compiled regular expression to match against attribute values
+ * @return elements that have attributes matching this regular expression
+ */
+ public Elements getElementsByAttributeValueMatching(String key, Pattern pattern) {
+ return Collector.collect(new Evaluator.AttributeWithValueMatching(key, pattern), this);
+
+ }
+
+ /**
+ * Find elements that have attributes whose values match the supplied regular expression.
+ * @param key name of the attribute
+ * @param regex regular expression to match against attribute values. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options.
+ * @return elements that have attributes matching this regular expression
+ */
+ public Elements getElementsByAttributeValueMatching(String key, String regex) {
+ Pattern pattern;
+ try {
+ pattern = Pattern.compile(regex);
+ } catch (PatternSyntaxException e) {
+ throw new IllegalArgumentException("Pattern syntax error: " + regex, e);
+ }
+ return getElementsByAttributeValueMatching(key, pattern);
+ }
+
+ /**
+ * Find elements whose sibling index is less than the supplied index.
+ * @param index 0-based index
+ * @return elements less than index
+ */
+ public Elements getElementsByIndexLessThan(int index) {
+ return Collector.collect(new Evaluator.IndexLessThan(index), this);
+ }
+
+ /**
+ * Find elements whose sibling index is greater than the supplied index.
+ * @param index 0-based index
+ * @return elements greater than index
+ */
+ public Elements getElementsByIndexGreaterThan(int index) {
+ return Collector.collect(new Evaluator.IndexGreaterThan(index), this);
+ }
+
+ /**
+ * Find elements whose sibling index is equal to the supplied index.
+ * @param index 0-based index
+ * @return elements equal to index
+ */
+ public Elements getElementsByIndexEquals(int index) {
+ return Collector.collect(new Evaluator.IndexEquals(index), this);
+ }
+
+ /**
+ * Find elements that contain the specified string. The search is case insensitive. The text may appear directly
+ * in the element, or in any of its descendants.
+ * @param searchText to look for in the element's text
+ * @return elements that contain the string, case insensitive.
+ * @see Element#text()
+ */
+ public Elements getElementsContainingText(String searchText) {
+ return Collector.collect(new Evaluator.ContainsText(searchText), this);
+ }
+
+ /**
+ * Find elements that directly contain the specified string. The search is case insensitive. The text must appear directly
+ * in the element, not in any of its descendants.
+ * @param searchText to look for in the element's own text
+ * @return elements that contain the string, case insensitive.
+ * @see Element#ownText()
+ */
+ public Elements getElementsContainingOwnText(String searchText) {
+ return Collector.collect(new Evaluator.ContainsOwnText(searchText), this);
+ }
+
+ /**
+ * Find elements whose text matches the supplied regular expression.
+ * @param pattern regular expression to match text against
+ * @return elements matching the supplied regular expression.
+ * @see Element#text()
+ */
+ public Elements getElementsMatchingText(Pattern pattern) {
+ return Collector.collect(new Evaluator.Matches(pattern), this);
+ }
+
+ /**
+ * Find elements whose text matches the supplied regular expression.
+ * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options.
+ * @return elements matching the supplied regular expression.
+ * @see Element#text()
+ */
+ public Elements getElementsMatchingText(String regex) {
+ Pattern pattern;
+ try {
+ pattern = Pattern.compile(regex);
+ } catch (PatternSyntaxException e) {
+ throw new IllegalArgumentException("Pattern syntax error: " + regex, e);
+ }
+ return getElementsMatchingText(pattern);
+ }
+
+ /**
+ * Find elements whose own text matches the supplied regular expression.
+ * @param pattern regular expression to match text against
+ * @return elements matching the supplied regular expression.
+ * @see Element#ownText()
+ */
+ public Elements getElementsMatchingOwnText(Pattern pattern) {
+ return Collector.collect(new Evaluator.MatchesOwn(pattern), this);
+ }
+
+ /**
+ * Find elements whose text matches the supplied regular expression.
+ * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options.
+ * @return elements matching the supplied regular expression.
+ * @see Element#ownText()
+ */
+ public Elements getElementsMatchingOwnText(String regex) {
+ Pattern pattern;
+ try {
+ pattern = Pattern.compile(regex);
+ } catch (PatternSyntaxException e) {
+ throw new IllegalArgumentException("Pattern syntax error: " + regex, e);
+ }
+ return getElementsMatchingOwnText(pattern);
+ }
+
+ /**
+ * Find all elements under this element (including self, and children of children).
+ *
+ * @return all elements
+ */
+ public Elements getAllElements() {
+ return Collector.collect(new Evaluator.AllElements(), this);
+ }
+
+ /**
+ * Gets the combined text of this element and all its children.
+ * <p>
+ * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.text()} returns {@code "Hello there now!"}
+ *
+ * @return unencoded text, or empty string if none.
+ * @see #ownText()
+ * @see #textNodes()
+ */
+ public String text() {
+ StringBuilder sb = new StringBuilder();
+ text(sb);
+ return sb.toString().trim();
+ }
+
+ private void text(StringBuilder accum) {
+ appendWhitespaceIfBr(this, accum);
+
+ for (Node child : childNodes) {
+ if (child instanceof TextNode) {
+ TextNode textNode = (TextNode) child;
+ appendNormalisedText(accum, textNode);
+ } else if (child instanceof Element) {
+ Element element = (Element) child;
+ if (accum.length() > 0 && element.isBlock() && !TextNode.lastCharIsWhitespace(accum))
+ accum.append(" ");
+ element.text(accum);
+ }
+ }
+ }
+
+ /**
+ * Gets the text owned by this element only; does not get the combined text of all children.
+ * <p>
+ * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.ownText()} returns {@code "Hello now!"},
+ * whereas {@code p.text()} returns {@code "Hello there now!"}.
+ * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element.
+ *
+ * @return unencoded text, or empty string if none.
+ * @see #text()
+ * @see #textNodes()
+ */
+ public String ownText() {
+ StringBuilder sb = new StringBuilder();
+ ownText(sb);
+ return sb.toString().trim();
+ }
+
+ private void ownText(StringBuilder accum) {
+ for (Node child : childNodes) {
+ if (child instanceof TextNode) {
+ TextNode textNode = (TextNode) child;
+ appendNormalisedText(accum, textNode);
+ } else if (child instanceof Element) {
+ appendWhitespaceIfBr((Element) child, accum);
+ }
+ }
+ }
+
+ private void appendNormalisedText(StringBuilder accum, TextNode textNode) {
+ String text = textNode.getWholeText();
+
+ if (!preserveWhitespace()) {
+ text = TextNode.normaliseWhitespace(text);
+ if (TextNode.lastCharIsWhitespace(accum))
+ text = TextNode.stripLeadingWhitespace(text);
+ }
+ accum.append(text);
+ }
+
+ private static void appendWhitespaceIfBr(Element element, StringBuilder accum) {
+ if (element.tag.getName().equals("br") && !TextNode.lastCharIsWhitespace(accum))
+ accum.append(" ");
+ }
+
+ boolean preserveWhitespace() {
+ return tag.preserveWhitespace() || parent() != null && parent().preserveWhitespace();
+ }
+
+ /**
+ * Set the text of this element. Any existing contents (text or elements) will be cleared
+ * @param text unencoded text
+ * @return this element
+ */
+ public Element text(String text) {
+ Validate.notNull(text);
+
+ empty();
+ TextNode textNode = new TextNode(text, baseUri);
+ appendChild(textNode);
+
+ return this;
+ }
+
+ /**
+ Test if this element has any text content (that is not just whitespace).
+ @return true if element has non-blank text content.
+ */
+ public boolean hasText() {
+ for (Node child: childNodes) {
+ if (child instanceof TextNode) {
+ TextNode textNode = (TextNode) child;
+ if (!textNode.isBlank())
+ return true;
+ } else if (child instanceof Element) {
+ Element el = (Element) child;
+ if (el.hasText())
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag.
+ * @return the data, or empty string if none
+ *
+ * @see #dataNodes()
+ */
+ public String data() {
+ StringBuilder sb = new StringBuilder();
+
+ for (Node childNode : childNodes) {
+ if (childNode instanceof DataNode) {
+ DataNode data = (DataNode) childNode;
+ sb.append(data.getWholeData());
+ } else if (childNode instanceof Element) {
+ Element element = (Element) childNode;
+ String elementData = element.data();
+ sb.append(elementData);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Gets the literal value of this element's "class" attribute, which may include multiple class names, space
+ * separated. (E.g. on <code>&lt;div class="header gray"></code> returns, "<code>header gray</code>")
+ * @return The literal class attribute, or <b>empty string</b> if no class attribute set.
+ */
+ public String className() {
+ return attr("class");
+ }
+
+ /**
+ * Get all of the element's class names. E.g. on element {@code <div class="header gray"}>},
+ * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to
+ * the backing {@code class} attribute; use the {@link #classNames(java.util.Set)} method to persist them.
+ * @return set of classnames, empty if no class attribute
+ */
+ public Set<String> classNames() {
+ if (classNames == null) {
+ String[] names = className().split("\\s+");
+ classNames = new LinkedHashSet<String>(Arrays.asList(names));
+ }
+ return classNames;
+ }
+
+ /**
+ Set the element's {@code class} attribute to the supplied class names.
+ @param classNames set of classes
+ @return this element, for chaining
+ */
+ public Element classNames(Set<String> classNames) {
+ Validate.notNull(classNames);
+ attributes.put("class", StringUtil.join(classNames, " "));
+ return this;
+ }
+
+ /**
+ * Tests if this element has a class. Case insensitive.
+ * @param className name of class to check for
+ * @return true if it does, false if not
+ */
+ public boolean hasClass(String className) {
+ Set<String> classNames = classNames();
+ for (String name : classNames) {
+ if (className.equalsIgnoreCase(name))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ Add a class name to this element's {@code class} attribute.
+ @param className class name to add
+ @return this element
+ */
+ public Element addClass(String className) {
+ Validate.notNull(className);
+
+ Set<String> classes = classNames();
+ classes.add(className);
+ classNames(classes);
+
+ return this;
+ }
+
+ /**
+ Remove a class name from this element's {@code class} attribute.
+ @param className class name to remove
+ @return this element
+ */
+ public Element removeClass(String className) {
+ Validate.notNull(className);
+
+ Set<String> classes = classNames();
+ classes.remove(className);
+ classNames(classes);
+
+ return this;
+ }
+
+ /**
+ Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it.
+ @param className class name to toggle
+ @return this element
+ */
+ public Element toggleClass(String className) {
+ Validate.notNull(className);
+
+ Set<String> classes = classNames();
+ if (classes.contains(className))
+ classes.remove(className);
+ else
+ classes.add(className);
+ classNames(classes);
+
+ return this;
+ }
+
+ /**
+ * Get the value of a form element (input, textarea, etc).
+ * @return the value of the form element, or empty string if not set.
+ */
+ public String val() {
+ if (tagName().equals("textarea"))
+ return text();
+ else
+ return attr("value");
+ }
+
+ /**
+ * Set the value of a form element (input, textarea, etc).
+ * @param value value to set
+ * @return this element (for chaining)
+ */
+ public Element val(String value) {
+ if (tagName().equals("textarea"))
+ text(value);
+ else
+ attr("value", value);
+ return this;
+ }
+
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ if (accum.length() > 0 && out.prettyPrint() && (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock())))
+ indent(accum, depth, out);
+ accum
+ .append("<")
+ .append(tagName());
+ attributes.html(accum, out);
+
+ if (childNodes.isEmpty() && tag.isSelfClosing())
+ accum.append(" />");
+ else
+ accum.append(">");
+ }
+
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {
+ if (!(childNodes.isEmpty() && tag.isSelfClosing())) {
+ if (out.prettyPrint() && !childNodes.isEmpty() && tag.formatAsBlock())
+ indent(accum, depth, out);
+ accum.append("</").append(tagName()).append(">");
+ }
+ }
+
+ /**
+ * Retrieves the element's inner HTML. E.g. on a {@code <div>} with one empty {@code <p>}, would return
+ * {@code <p></p>}. (Whereas {@link #outerHtml()} would return {@code <div><p></p></div>}.)
+ *
+ * @return String of HTML.
+ * @see #outerHtml()
+ */
+ public String html() {
+ StringBuilder accum = new StringBuilder();
+ html(accum);
+ return accum.toString().trim();
+ }
+
+ private void html(StringBuilder accum) {
+ for (Node node : childNodes)
+ node.outerHtml(accum);
+ }
+
+ /**
+ * Set this element's inner HTML. Clears the existing HTML first.
+ * @param html HTML to parse and set into this element
+ * @return this element
+ * @see #append(String)
+ */
+ public Element html(String html) {
+ empty();
+ append(html);
+ return this;
+ }
+
+ public String toString() {
+ return outerHtml();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o;
+ }
+
+ @Override
+ public int hashCode() {
+ // todo: fixup, not very useful
+ int result = super.hashCode();
+ result = 31 * result + (tag != null ? tag.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public Element clone() {
+ Element clone = (Element) super.clone();
+ clone.classNames(); // creates linked set of class names from class attribute
+ return clone;
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Entities.java b/server/src/org/jsoup/nodes/Entities.java
new file mode 100644
index 0000000000..0ae83e1fc0
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Entities.java
@@ -0,0 +1,184 @@
+package org.jsoup.nodes;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.CharsetEncoder;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML entities, and escape routines.
+ * Source: <a href="http://www.w3.org/TR/html5/named-character-references.html#named-character-references">W3C HTML
+ * named character references</a>.
+ */
+public class Entities {
+ public enum EscapeMode {
+ /** Restricted entities suitable for XHTML output: lt, gt, amp, apos, and quot only. */
+ xhtml(xhtmlByVal),
+ /** Default HTML output entities. */
+ base(baseByVal),
+ /** Complete HTML entities. */
+ extended(fullByVal);
+
+ private Map<Character, String> map;
+
+ EscapeMode(Map<Character, String> map) {
+ this.map = map;
+ }
+
+ public Map<Character, String> getMap() {
+ return map;
+ }
+ }
+
+ private static final Map<String, Character> full;
+ private static final Map<Character, String> xhtmlByVal;
+ private static final Map<Character, String> baseByVal;
+ private static final Map<Character, String> fullByVal;
+ private static final Pattern unescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);?");
+ private static final Pattern strictUnescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);");
+
+ private Entities() {}
+
+ /**
+ * Check if the input is a known named entity
+ * @param name the possible entity name (e.g. "lt" or "amp"
+ * @return true if a known named entity
+ */
+ public static boolean isNamedEntity(String name) {
+ return full.containsKey(name);
+ }
+
+ /**
+ * Get the Character value of the named entity
+ * @param name named entity (e.g. "lt" or "amp")
+ * @return the Character value of the named entity (e.g. '<' or '&')
+ */
+ public static Character getCharacterByName(String name) {
+ return full.get(name);
+ }
+
+ static String escape(String string, Document.OutputSettings out) {
+ return escape(string, out.encoder(), out.escapeMode());
+ }
+
+ static String escape(String string, CharsetEncoder encoder, EscapeMode escapeMode) {
+ StringBuilder accum = new StringBuilder(string.length() * 2);
+ Map<Character, String> map = escapeMode.getMap();
+
+ for (int pos = 0; pos < string.length(); pos++) {
+ Character c = string.charAt(pos);
+ if (map.containsKey(c))
+ accum.append('&').append(map.get(c)).append(';');
+ else if (encoder.canEncode(c))
+ accum.append(c.charValue());
+ else
+ accum.append("&#").append((int) c).append(';');
+ }
+
+ return accum.toString();
+ }
+
+ static String unescape(String string) {
+ return unescape(string, false);
+ }
+
+ /**
+ * Unescape the input string.
+ * @param string
+ * @param strict if "strict" (that is, requires trailing ';' char, otherwise that's optional)
+ * @return
+ */
+ static String unescape(String string, boolean strict) {
+ // todo: change this method to use Tokeniser.consumeCharacterReference
+ if (!string.contains("&"))
+ return string;
+
+ Matcher m = strict? strictUnescapePattern.matcher(string) : unescapePattern.matcher(string); // &(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]\\d*);?
+ StringBuffer accum = new StringBuffer(string.length()); // pity matcher can't use stringbuilder, avoid syncs
+ // todo: replace m.appendReplacement with own impl, so StringBuilder and quoteReplacement not required
+
+ while (m.find()) {
+ int charval = -1;
+ String num = m.group(3);
+ if (num != null) {
+ try {
+ int base = m.group(2) != null ? 16 : 10; // 2 is hex indicator
+ charval = Integer.valueOf(num, base);
+ } catch (NumberFormatException e) {
+ } // skip
+ } else {
+ String name = m.group(1);
+ if (full.containsKey(name))
+ charval = full.get(name);
+ }
+
+ if (charval != -1 || charval > 0xFFFF) { // out of range
+ String c = Character.toString((char) charval);
+ m.appendReplacement(accum, Matcher.quoteReplacement(c));
+ } else {
+ m.appendReplacement(accum, Matcher.quoteReplacement(m.group(0))); // replace with original string
+ }
+ }
+ m.appendTail(accum);
+ return accum.toString();
+ }
+
+ // xhtml has restricted entities
+ private static final Object[][] xhtmlArray = {
+ {"quot", 0x00022},
+ {"amp", 0x00026},
+ {"apos", 0x00027},
+ {"lt", 0x0003C},
+ {"gt", 0x0003E}
+ };
+
+ static {
+ xhtmlByVal = new HashMap<Character, String>();
+ baseByVal = toCharacterKey(loadEntities("entities-base.properties")); // most common / default
+ full = loadEntities("entities-full.properties"); // extended and overblown.
+ fullByVal = toCharacterKey(full);
+
+ for (Object[] entity : xhtmlArray) {
+ Character c = Character.valueOf((char) ((Integer) entity[1]).intValue());
+ xhtmlByVal.put(c, ((String) entity[0]));
+ }
+ }
+
+ private static Map<String, Character> loadEntities(String filename) {
+ Properties properties = new Properties();
+ Map<String, Character> entities = new HashMap<String, Character>();
+ try {
+ InputStream in = Entities.class.getResourceAsStream(filename);
+ properties.load(in);
+ in.close();
+ } catch (IOException e) {
+ throw new MissingResourceException("Error loading entities resource: " + e.getMessage(), "Entities", filename);
+ }
+
+ for (Map.Entry entry: properties.entrySet()) {
+ Character val = Character.valueOf((char) Integer.parseInt((String) entry.getValue(), 16));
+ String name = (String) entry.getKey();
+ entities.put(name, val);
+ }
+ return entities;
+ }
+
+ private static Map<Character, String> toCharacterKey(Map<String, Character> inMap) {
+ Map<Character, String> outMap = new HashMap<Character, String>();
+ for (Map.Entry<String, Character> entry: inMap.entrySet()) {
+ Character character = entry.getValue();
+ String name = entry.getKey();
+
+ if (outMap.containsKey(character)) {
+ // dupe, prefer the lower case version
+ if (name.toLowerCase().equals(name))
+ outMap.put(character, name);
+ } else {
+ outMap.put(character, name);
+ }
+ }
+ return outMap;
+ }
+}
diff --git a/server/src/org/jsoup/nodes/Node.java b/server/src/org/jsoup/nodes/Node.java
new file mode 100644
index 0000000000..eb2b40ee73
--- /dev/null
+++ b/server/src/org/jsoup/nodes/Node.java
@@ -0,0 +1,615 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+import org.jsoup.parser.Parser;
+import org.jsoup.select.NodeTraversor;
+import org.jsoup.select.NodeVisitor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ The base, abstract Node model. Elements, Documents, Comments etc are all Node instances.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public abstract class Node implements Cloneable {
+ Node parentNode;
+ List<Node> childNodes;
+ Attributes attributes;
+ String baseUri;
+ int siblingIndex;
+
+ /**
+ Create a new Node.
+ @param baseUri base URI
+ @param attributes attributes (not null, but may be empty)
+ */
+ protected Node(String baseUri, Attributes attributes) {
+ Validate.notNull(baseUri);
+ Validate.notNull(attributes);
+
+ childNodes = new ArrayList<Node>(4);
+ this.baseUri = baseUri.trim();
+ this.attributes = attributes;
+ }
+
+ protected Node(String baseUri) {
+ this(baseUri, new Attributes());
+ }
+
+ /**
+ * Default constructor. Doesn't setup base uri, children, or attributes; use with caution.
+ */
+ protected Node() {
+ childNodes = Collections.emptyList();
+ attributes = null;
+ }
+
+ /**
+ Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof).
+ @return node name
+ */
+ public abstract String nodeName();
+
+ /**
+ * Get an attribute's value by its key.
+ * <p/>
+ * To get an absolute URL from an attribute that may be a relative URL, prefix the key with <code><b>abs</b></code>,
+ * which is a shortcut to the {@link #absUrl} method.
+ * E.g.: <blockquote><code>String url = a.attr("abs:href");</code></blockquote>
+ * @param attributeKey The attribute key.
+ * @return The attribute, or empty string if not present (to avoid nulls).
+ * @see #attributes()
+ * @see #hasAttr(String)
+ * @see #absUrl(String)
+ */
+ public String attr(String attributeKey) {
+ Validate.notNull(attributeKey);
+
+ if (attributes.hasKey(attributeKey))
+ return attributes.get(attributeKey);
+ else if (attributeKey.toLowerCase().startsWith("abs:"))
+ return absUrl(attributeKey.substring("abs:".length()));
+ else return "";
+ }
+
+ /**
+ * Get all of the element's attributes.
+ * @return attributes (which implements iterable, in same order as presented in original HTML).
+ */
+ public Attributes attributes() {
+ return attributes;
+ }
+
+ /**
+ * Set an attribute (key=value). If the attribute already exists, it is replaced.
+ * @param attributeKey The attribute key.
+ * @param attributeValue The attribute value.
+ * @return this (for chaining)
+ */
+ public Node attr(String attributeKey, String attributeValue) {
+ attributes.put(attributeKey, attributeValue);
+ return this;
+ }
+
+ /**
+ * Test if this element has an attribute.
+ * @param attributeKey The attribute key to check.
+ * @return true if the attribute exists, false if not.
+ */
+ public boolean hasAttr(String attributeKey) {
+ Validate.notNull(attributeKey);
+
+ if (attributeKey.toLowerCase().startsWith("abs:")) {
+ String key = attributeKey.substring("abs:".length());
+ if (attributes.hasKey(key) && !absUrl(key).equals(""))
+ return true;
+ }
+ return attributes.hasKey(attributeKey);
+ }
+
+ /**
+ * Remove an attribute from this element.
+ * @param attributeKey The attribute to remove.
+ * @return this (for chaining)
+ */
+ public Node removeAttr(String attributeKey) {
+ Validate.notNull(attributeKey);
+ attributes.remove(attributeKey);
+ return this;
+ }
+
+ /**
+ Get the base URI of this node.
+ @return base URI
+ */
+ public String baseUri() {
+ return baseUri;
+ }
+
+ /**
+ Update the base URI of this node and all of its descendants.
+ @param baseUri base URI to set
+ */
+ public void setBaseUri(final String baseUri) {
+ Validate.notNull(baseUri);
+
+ traverse(new NodeVisitor() {
+ public void head(Node node, int depth) {
+ node.baseUri = baseUri;
+ }
+
+ public void tail(Node node, int depth) {
+ }
+ });
+ }
+
+ /**
+ * Get an absolute URL from a URL attribute that may be relative (i.e. an <code>&lt;a href></code> or
+ * <code>&lt;img src></code>).
+ * <p/>
+ * E.g.: <code>String absUrl = linkEl.absUrl("href");</code>
+ * <p/>
+ * If the attribute value is already absolute (i.e. it starts with a protocol, like
+ * <code>http://</code> or <code>https://</code> etc), and it successfully parses as a URL, the attribute is
+ * returned directly. Otherwise, it is treated as a URL relative to the element's {@link #baseUri}, and made
+ * absolute using that.
+ * <p/>
+ * As an alternate, you can use the {@link #attr} method with the <code>abs:</code> prefix, e.g.:
+ * <code>String absUrl = linkEl.attr("abs:href");</code>
+ *
+ * @param attributeKey The attribute key
+ * @return An absolute URL if one could be made, or an empty string (not null) if the attribute was missing or
+ * could not be made successfully into a URL.
+ * @see #attr
+ * @see java.net.URL#URL(java.net.URL, String)
+ */
+ public String absUrl(String attributeKey) {
+ Validate.notEmpty(attributeKey);
+
+ String relUrl = attr(attributeKey);
+ if (!hasAttr(attributeKey)) {
+ return ""; // nothing to make absolute with
+ } else {
+ URL base;
+ try {
+ try {
+ base = new URL(baseUri);
+ } catch (MalformedURLException e) {
+ // the base is unsuitable, but the attribute may be abs on its own, so try that
+ URL abs = new URL(relUrl);
+ return abs.toExternalForm();
+ }
+ // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired
+ if (relUrl.startsWith("?"))
+ relUrl = base.getPath() + relUrl;
+ URL abs = new URL(base, relUrl);
+ return abs.toExternalForm();
+ } catch (MalformedURLException e) {
+ return "";
+ }
+ }
+ }
+
+ /**
+ Get a child node by index
+ @param index index of child node
+ @return the child node at this index.
+ */
+ public Node childNode(int index) {
+ return childNodes.get(index);
+ }
+
+ /**
+ Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes
+ themselves can be manipulated.
+ @return list of children. If no children, returns an empty list.
+ */
+ public List<Node> childNodes() {
+ return Collections.unmodifiableList(childNodes);
+ }
+
+ protected Node[] childNodesAsArray() {
+ return childNodes.toArray(new Node[childNodes().size()]);
+ }
+
+ /**
+ Gets this node's parent node.
+ @return parent node; or null if no parent.
+ */
+ public Node parent() {
+ return parentNode;
+ }
+
+ /**
+ * Gets the Document associated with this Node.
+ * @return the Document associated with this Node, or null if there is no such Document.
+ */
+ public Document ownerDocument() {
+ if (this instanceof Document)
+ return (Document) this;
+ else if (parentNode == null)
+ return null;
+ else
+ return parentNode.ownerDocument();
+ }
+
+ /**
+ * Remove (delete) this node from the DOM tree. If this node has children, they are also removed.
+ */
+ public void remove() {
+ Validate.notNull(parentNode);
+ parentNode.removeChild(this);
+ }
+
+ /**
+ * Insert the specified HTML into the DOM before this node (i.e. as a preceding sibling).
+ * @param html HTML to add before this node
+ * @return this node, for chaining
+ * @see #after(String)
+ */
+ public Node before(String html) {
+ addSiblingHtml(siblingIndex(), html);
+ return this;
+ }
+
+ /**
+ * Insert the specified node into the DOM before this node (i.e. as a preceding sibling).
+ * @param node to add before this node
+ * @return this node, for chaining
+ * @see #after(Node)
+ */
+ public Node before(Node node) {
+ Validate.notNull(node);
+ Validate.notNull(parentNode);
+
+ parentNode.addChildren(siblingIndex(), node);
+ return this;
+ }
+
+ /**
+ * Insert the specified HTML into the DOM after this node (i.e. as a following sibling).
+ * @param html HTML to add after this node
+ * @return this node, for chaining
+ * @see #before(String)
+ */
+ public Node after(String html) {
+ addSiblingHtml(siblingIndex()+1, html);
+ return this;
+ }
+
+ /**
+ * Insert the specified node into the DOM after this node (i.e. as a following sibling).
+ * @param node to add after this node
+ * @return this node, for chaining
+ * @see #before(Node)
+ */
+ public Node after(Node node) {
+ Validate.notNull(node);
+ Validate.notNull(parentNode);
+
+ parentNode.addChildren(siblingIndex()+1, node);
+ return this;
+ }
+
+ private void addSiblingHtml(int index, String html) {
+ Validate.notNull(html);
+ Validate.notNull(parentNode);
+
+ Element context = parent() instanceof Element ? (Element) parent() : null;
+ List<Node> nodes = Parser.parseFragment(html, context, baseUri());
+ parentNode.addChildren(index, nodes.toArray(new Node[nodes.size()]));
+ }
+
+ /**
+ Wrap the supplied HTML around this node.
+ @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
+ @return this node, for chaining.
+ */
+ public Node wrap(String html) {
+ Validate.notEmpty(html);
+
+ Element context = parent() instanceof Element ? (Element) parent() : null;
+ List<Node> wrapChildren = Parser.parseFragment(html, context, baseUri());
+ Node wrapNode = wrapChildren.get(0);
+ if (wrapNode == null || !(wrapNode instanceof Element)) // nothing to wrap with; noop
+ return null;
+
+ Element wrap = (Element) wrapNode;
+ Element deepest = getDeepChild(wrap);
+ parentNode.replaceChild(this, wrap);
+ deepest.addChildren(this);
+
+ // remainder (unbalanced wrap, like <div></div><p></p> -- The <p> is remainder
+ if (wrapChildren.size() > 0) {
+ for (int i = 0; i < wrapChildren.size(); i++) {
+ Node remainder = wrapChildren.get(i);
+ remainder.parentNode.removeChild(remainder);
+ wrap.appendChild(remainder);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Removes this node from the DOM, and moves its children up into the node's parent. This has the effect of dropping
+ * the node but keeping its children.
+ * <p/>
+ * For example, with the input html:<br/>
+ * {@code <div>One <span>Two <b>Three</b></span></div>}<br/>
+ * Calling {@code element.unwrap()} on the {@code span} element will result in the html:<br/>
+ * {@code <div>One Two <b>Three</b></div>}<br/>
+ * and the {@code "Two "} {@link TextNode} being returned.
+ * @return the first child of this node, after the node has been unwrapped. Null if the node had no children.
+ * @see #remove()
+ * @see #wrap(String)
+ */
+ public Node unwrap() {
+ Validate.notNull(parentNode);
+
+ int index = siblingIndex;
+ Node firstChild = childNodes.size() > 0 ? childNodes.get(0) : null;
+ parentNode.addChildren(index, this.childNodesAsArray());
+ this.remove();
+
+ return firstChild;
+ }
+
+ private Element getDeepChild(Element el) {
+ List<Element> children = el.children();
+ if (children.size() > 0)
+ return getDeepChild(children.get(0));
+ else
+ return el;
+ }
+
+ /**
+ * Replace this node in the DOM with the supplied node.
+ * @param in the node that will will replace the existing node.
+ */
+ public void replaceWith(Node in) {
+ Validate.notNull(in);
+ Validate.notNull(parentNode);
+ parentNode.replaceChild(this, in);
+ }
+
+ protected void setParentNode(Node parentNode) {
+ if (this.parentNode != null)
+ this.parentNode.removeChild(this);
+ this.parentNode = parentNode;
+ }
+
+ protected void replaceChild(Node out, Node in) {
+ Validate.isTrue(out.parentNode == this);
+ Validate.notNull(in);
+ if (in.parentNode != null)
+ in.parentNode.removeChild(in);
+
+ Integer index = out.siblingIndex();
+ childNodes.set(index, in);
+ in.parentNode = this;
+ in.setSiblingIndex(index);
+ out.parentNode = null;
+ }
+
+ protected void removeChild(Node out) {
+ Validate.isTrue(out.parentNode == this);
+ int index = out.siblingIndex();
+ childNodes.remove(index);
+ reindexChildren();
+ out.parentNode = null;
+ }
+
+ protected void addChildren(Node... children) {
+ //most used. short circuit addChildren(int), which hits reindex children and array copy
+ for (Node child: children) {
+ reparentChild(child);
+ childNodes.add(child);
+ child.setSiblingIndex(childNodes.size()-1);
+ }
+ }
+
+ protected void addChildren(int index, Node... children) {
+ Validate.noNullElements(children);
+ for (int i = children.length - 1; i >= 0; i--) {
+ Node in = children[i];
+ reparentChild(in);
+ childNodes.add(index, in);
+ }
+ reindexChildren();
+ }
+
+ private void reparentChild(Node child) {
+ if (child.parentNode != null)
+ child.parentNode.removeChild(child);
+ child.setParentNode(this);
+ }
+
+ private void reindexChildren() {
+ for (int i = 0; i < childNodes.size(); i++) {
+ childNodes.get(i).setSiblingIndex(i);
+ }
+ }
+
+ /**
+ Retrieves this node's sibling nodes. Similar to {@link #childNodes() node.parent.childNodes()}, but does not
+ include this node (a node is not a sibling of itself).
+ @return node siblings. If the node has no parent, returns an empty list.
+ */
+ public List<Node> siblingNodes() {
+ if (parentNode == null)
+ return Collections.emptyList();
+
+ List<Node> nodes = parentNode.childNodes;
+ List<Node> siblings = new ArrayList<Node>(nodes.size() - 1);
+ for (Node node: nodes)
+ if (node != this)
+ siblings.add(node);
+ return siblings;
+ }
+
+ /**
+ Get this node's next sibling.
+ @return next sibling, or null if this is the last sibling
+ */
+ public Node nextSibling() {
+ if (parentNode == null)
+ return null; // root
+
+ List<Node> siblings = parentNode.childNodes;
+ Integer index = siblingIndex();
+ Validate.notNull(index);
+ if (siblings.size() > index+1)
+ return siblings.get(index+1);
+ else
+ return null;
+ }
+
+ /**
+ Get this node's previous sibling.
+ @return the previous sibling, or null if this is the first sibling
+ */
+ public Node previousSibling() {
+ if (parentNode == null)
+ return null; // root
+
+ List<Node> siblings = parentNode.childNodes;
+ Integer index = siblingIndex();
+ Validate.notNull(index);
+ if (index > 0)
+ return siblings.get(index-1);
+ else
+ return null;
+ }
+
+ /**
+ * Get the list index of this node in its node sibling list. I.e. if this is the first node
+ * sibling, returns 0.
+ * @return position in node sibling list
+ * @see org.jsoup.nodes.Element#elementSiblingIndex()
+ */
+ public int siblingIndex() {
+ return siblingIndex;
+ }
+
+ protected void setSiblingIndex(int siblingIndex) {
+ this.siblingIndex = siblingIndex;
+ }
+
+ /**
+ * Perform a depth-first traversal through this node and its descendants.
+ * @param nodeVisitor the visitor callbacks to perform on each node
+ * @return this node, for chaining
+ */
+ public Node traverse(NodeVisitor nodeVisitor) {
+ Validate.notNull(nodeVisitor);
+ NodeTraversor traversor = new NodeTraversor(nodeVisitor);
+ traversor.traverse(this);
+ return this;
+ }
+
+ /**
+ Get the outer HTML of this node.
+ @return HTML
+ */
+ public String outerHtml() {
+ StringBuilder accum = new StringBuilder(128);
+ outerHtml(accum);
+ return accum.toString();
+ }
+
+ protected void outerHtml(StringBuilder accum) {
+ new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this);
+ }
+
+ // if this node has no document (or parent), retrieve the default output settings
+ private Document.OutputSettings getOutputSettings() {
+ return ownerDocument() != null ? ownerDocument().outputSettings() : (new Document("")).outputSettings();
+ }
+
+ /**
+ Get the outer HTML of this node.
+ @param accum accumulator to place HTML into
+ */
+ abstract void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out);
+
+ abstract void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out);
+
+ public String toString() {
+ return outerHtml();
+ }
+
+ protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) {
+ accum.append("\n").append(StringUtil.padding(depth * out.indentAmount()));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ // todo: have nodes hold a child index, compare against that and parent (not children)
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = parentNode != null ? parentNode.hashCode() : 0;
+ // not children, or will block stack as they go back up to parent)
+ result = 31 * result + (attributes != null ? attributes.hashCode() : 0);
+ return result;
+ }
+
+ /**
+ * Create a stand-alone, deep copy of this node, and all of its children. The cloned node will have no siblings or
+ * parent node. As a stand-alone object, any changes made to the clone or any of its children will not impact the
+ * original node.
+ * <p>
+ * The cloned node may be adopted into another Document or node structure using {@link Element#appendChild(Node)}.
+ * @return stand-alone cloned node
+ */
+ @Override
+ public Node clone() {
+ return doClone(null); // splits for orphan
+ }
+
+ protected Node doClone(Node parent) {
+ Node clone;
+ try {
+ clone = (Node) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+
+ clone.parentNode = parent; // can be null, to create an orphan split
+ clone.siblingIndex = parent == null ? 0 : siblingIndex;
+ clone.attributes = attributes != null ? attributes.clone() : null;
+ clone.baseUri = baseUri;
+ clone.childNodes = new ArrayList<Node>(childNodes.size());
+ for (Node child: childNodes)
+ clone.childNodes.add(child.doClone(clone)); // clone() creates orphans, doClone() keeps parent
+
+ return clone;
+ }
+
+ private static class OuterHtmlVisitor implements NodeVisitor {
+ private StringBuilder accum;
+ private Document.OutputSettings out;
+
+ OuterHtmlVisitor(StringBuilder accum, Document.OutputSettings out) {
+ this.accum = accum;
+ this.out = out;
+ }
+
+ public void head(Node node, int depth) {
+ node.outerHtmlHead(accum, depth, out);
+ }
+
+ public void tail(Node node, int depth) {
+ if (!node.nodeName().equals("#text")) // saves a void hit.
+ node.outerHtmlTail(accum, depth, out);
+ }
+ }
+}
diff --git a/server/src/org/jsoup/nodes/TextNode.java b/server/src/org/jsoup/nodes/TextNode.java
new file mode 100644
index 0000000000..9fd0feac8f
--- /dev/null
+++ b/server/src/org/jsoup/nodes/TextNode.java
@@ -0,0 +1,175 @@
+package org.jsoup.nodes;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+
+/**
+ A text node.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class TextNode extends Node {
+ /*
+ TextNode is a node, and so by default comes with attributes and children. The attributes are seldom used, but use
+ memory, and the child nodes are never used. So we don't have them, and override accessors to attributes to create
+ them as needed on the fly.
+ */
+ private static final String TEXT_KEY = "text";
+ String text;
+
+ /**
+ Create a new TextNode representing the supplied (unencoded) text).
+
+ @param text raw text
+ @param baseUri base uri
+ @see #createFromEncoded(String, String)
+ */
+ public TextNode(String text, String baseUri) {
+ this.baseUri = baseUri;
+ this.text = text;
+ }
+
+ public String nodeName() {
+ return "#text";
+ }
+
+ /**
+ * Get the text content of this text node.
+ * @return Unencoded, normalised text.
+ * @see TextNode#getWholeText()
+ */
+ public String text() {
+ return normaliseWhitespace(getWholeText());
+ }
+
+ /**
+ * Set the text content of this text node.
+ * @param text unencoded text
+ * @return this, for chaining
+ */
+ public TextNode text(String text) {
+ this.text = text;
+ if (attributes != null)
+ attributes.put(TEXT_KEY, text);
+ return this;
+ }
+
+ /**
+ Get the (unencoded) text of this text node, including any newlines and spaces present in the original.
+ @return text
+ */
+ public String getWholeText() {
+ return attributes == null ? text : attributes.get(TEXT_KEY);
+ }
+
+ /**
+ Test if this text node is blank -- that is, empty or only whitespace (including newlines).
+ @return true if this document is empty or only whitespace, false if it contains any text content.
+ */
+ public boolean isBlank() {
+ return StringUtil.isBlank(getWholeText());
+ }
+
+ /**
+ * Split this text node into two nodes at the specified string offset. After splitting, this node will contain the
+ * original text up to the offset, and will have a new text node sibling containing the text after the offset.
+ * @param offset string offset point to split node at.
+ * @return the newly created text node containing the text after the offset.
+ */
+ public TextNode splitText(int offset) {
+ Validate.isTrue(offset >= 0, "Split offset must be not be negative");
+ Validate.isTrue(offset < text.length(), "Split offset must not be greater than current text length");
+
+ String head = getWholeText().substring(0, offset);
+ String tail = getWholeText().substring(offset);
+ text(head);
+ TextNode tailNode = new TextNode(tail, this.baseUri());
+ if (parent() != null)
+ parent().addChildren(siblingIndex()+1, tailNode);
+
+ return tailNode;
+ }
+
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ String html = Entities.escape(getWholeText(), out);
+ if (out.prettyPrint() && parent() instanceof Element && !((Element) parent()).preserveWhitespace()) {
+ html = normaliseWhitespace(html);
+ }
+
+ if (out.prettyPrint() && siblingIndex() == 0 && parentNode instanceof Element && ((Element) parentNode).tag().formatAsBlock() && !isBlank())
+ indent(accum, depth, out);
+ accum.append(html);
+ }
+
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {}
+
+ public String toString() {
+ return outerHtml();
+ }
+
+ /**
+ * Create a new TextNode from HTML encoded (aka escaped) data.
+ * @param encodedText Text containing encoded HTML (e.g. &amp;lt;)
+ * @return TextNode containing unencoded data (e.g. &lt;)
+ */
+ public static TextNode createFromEncoded(String encodedText, String baseUri) {
+ String text = Entities.unescape(encodedText);
+ return new TextNode(text, baseUri);
+ }
+
+ static String normaliseWhitespace(String text) {
+ text = StringUtil.normaliseWhitespace(text);
+ return text;
+ }
+
+ static String stripLeadingWhitespace(String text) {
+ return text.replaceFirst("^\\s+", "");
+ }
+
+ static boolean lastCharIsWhitespace(StringBuilder sb) {
+ return sb.length() != 0 && sb.charAt(sb.length() - 1) == ' ';
+ }
+
+ // attribute fiddling. create on first access.
+ private void ensureAttributes() {
+ if (attributes == null) {
+ attributes = new Attributes();
+ attributes.put(TEXT_KEY, text);
+ }
+ }
+
+ @Override
+ public String attr(String attributeKey) {
+ ensureAttributes();
+ return super.attr(attributeKey);
+ }
+
+ @Override
+ public Attributes attributes() {
+ ensureAttributes();
+ return super.attributes();
+ }
+
+ @Override
+ public Node attr(String attributeKey, String attributeValue) {
+ ensureAttributes();
+ return super.attr(attributeKey, attributeValue);
+ }
+
+ @Override
+ public boolean hasAttr(String attributeKey) {
+ ensureAttributes();
+ return super.hasAttr(attributeKey);
+ }
+
+ @Override
+ public Node removeAttr(String attributeKey) {
+ ensureAttributes();
+ return super.removeAttr(attributeKey);
+ }
+
+ @Override
+ public String absUrl(String attributeKey) {
+ ensureAttributes();
+ return super.absUrl(attributeKey);
+ }
+}
diff --git a/server/src/org/jsoup/nodes/XmlDeclaration.java b/server/src/org/jsoup/nodes/XmlDeclaration.java
new file mode 100644
index 0000000000..80d4a0152f
--- /dev/null
+++ b/server/src/org/jsoup/nodes/XmlDeclaration.java
@@ -0,0 +1,48 @@
+package org.jsoup.nodes;
+
+/**
+ An XML Declaration.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class XmlDeclaration extends Node {
+ private static final String DECL_KEY = "declaration";
+ private final boolean isProcessingInstruction; // <! if true, <? if false, declaration (and last data char should be ?)
+
+ /**
+ Create a new XML declaration
+ @param data data
+ @param baseUri base uri
+ @param isProcessingInstruction is processing instruction
+ */
+ public XmlDeclaration(String data, String baseUri, boolean isProcessingInstruction) {
+ super(baseUri);
+ attributes.put(DECL_KEY, data);
+ this.isProcessingInstruction = isProcessingInstruction;
+ }
+
+ public String nodeName() {
+ return "#declaration";
+ }
+
+ /**
+ Get the unencoded XML declaration.
+ @return XML declaration
+ */
+ public String getWholeDeclaration() {
+ return attributes.get(DECL_KEY);
+ }
+
+ void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
+ accum
+ .append("<")
+ .append(isProcessingInstruction ? "!" : "?")
+ .append(getWholeDeclaration())
+ .append(">");
+ }
+
+ void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {}
+
+ public String toString() {
+ return outerHtml();
+ }
+}
diff --git a/server/src/org/jsoup/nodes/entities-base.properties b/server/src/org/jsoup/nodes/entities-base.properties
new file mode 100644
index 0000000000..3d1d11e6c4
--- /dev/null
+++ b/server/src/org/jsoup/nodes/entities-base.properties
@@ -0,0 +1,106 @@
+AElig=000C6
+AMP=00026
+Aacute=000C1
+Acirc=000C2
+Agrave=000C0
+Aring=000C5
+Atilde=000C3
+Auml=000C4
+COPY=000A9
+Ccedil=000C7
+ETH=000D0
+Eacute=000C9
+Ecirc=000CA
+Egrave=000C8
+Euml=000CB
+GT=0003E
+Iacute=000CD
+Icirc=000CE
+Igrave=000CC
+Iuml=000CF
+LT=0003C
+Ntilde=000D1
+Oacute=000D3
+Ocirc=000D4
+Ograve=000D2
+Oslash=000D8
+Otilde=000D5
+Ouml=000D6
+QUOT=00022
+REG=000AE
+THORN=000DE
+Uacute=000DA
+Ucirc=000DB
+Ugrave=000D9
+Uuml=000DC
+Yacute=000DD
+aacute=000E1
+acirc=000E2
+acute=000B4
+aelig=000E6
+agrave=000E0
+amp=00026
+aring=000E5
+atilde=000E3
+auml=000E4
+brvbar=000A6
+ccedil=000E7
+cedil=000B8
+cent=000A2
+copy=000A9
+curren=000A4
+deg=000B0
+divide=000F7
+eacute=000E9
+ecirc=000EA
+egrave=000E8
+eth=000F0
+euml=000EB
+frac12=000BD
+frac14=000BC
+frac34=000BE
+gt=0003E
+iacute=000ED
+icirc=000EE
+iexcl=000A1
+igrave=000EC
+iquest=000BF
+iuml=000EF
+laquo=000AB
+lt=0003C
+macr=000AF
+micro=000B5
+middot=000B7
+nbsp=000A0
+not=000AC
+ntilde=000F1
+oacute=000F3
+ocirc=000F4
+ograve=000F2
+ordf=000AA
+ordm=000BA
+oslash=000F8
+otilde=000F5
+ouml=000F6
+para=000B6
+plusmn=000B1
+pound=000A3
+quot=00022
+raquo=000BB
+reg=000AE
+sect=000A7
+shy=000AD
+sup1=000B9
+sup2=000B2
+sup3=000B3
+szlig=000DF
+thorn=000FE
+times=000D7
+uacute=000FA
+ucirc=000FB
+ugrave=000F9
+uml=000A8
+uuml=000FC
+yacute=000FD
+yen=000A5
+yuml=000FF
diff --git a/server/src/org/jsoup/nodes/entities-full.properties b/server/src/org/jsoup/nodes/entities-full.properties
new file mode 100644
index 0000000000..92f124f408
--- /dev/null
+++ b/server/src/org/jsoup/nodes/entities-full.properties
@@ -0,0 +1,2032 @@
+AElig=000C6
+AMP=00026
+Aacute=000C1
+Abreve=00102
+Acirc=000C2
+Acy=00410
+Afr=1D504
+Agrave=000C0
+Alpha=00391
+Amacr=00100
+And=02A53
+Aogon=00104
+Aopf=1D538
+ApplyFunction=02061
+Aring=000C5
+Ascr=1D49C
+Assign=02254
+Atilde=000C3
+Auml=000C4
+Backslash=02216
+Barv=02AE7
+Barwed=02306
+Bcy=00411
+Because=02235
+Bernoullis=0212C
+Beta=00392
+Bfr=1D505
+Bopf=1D539
+Breve=002D8
+Bscr=0212C
+Bumpeq=0224E
+CHcy=00427
+COPY=000A9
+Cacute=00106
+Cap=022D2
+CapitalDifferentialD=02145
+Cayleys=0212D
+Ccaron=0010C
+Ccedil=000C7
+Ccirc=00108
+Cconint=02230
+Cdot=0010A
+Cedilla=000B8
+CenterDot=000B7
+Cfr=0212D
+Chi=003A7
+CircleDot=02299
+CircleMinus=02296
+CirclePlus=02295
+CircleTimes=02297
+ClockwiseContourIntegral=02232
+CloseCurlyDoubleQuote=0201D
+CloseCurlyQuote=02019
+Colon=02237
+Colone=02A74
+Congruent=02261
+Conint=0222F
+ContourIntegral=0222E
+Copf=02102
+Coproduct=02210
+CounterClockwiseContourIntegral=02233
+Cross=02A2F
+Cscr=1D49E
+Cup=022D3
+CupCap=0224D
+DD=02145
+DDotrahd=02911
+DJcy=00402
+DScy=00405
+DZcy=0040F
+Dagger=02021
+Darr=021A1
+Dashv=02AE4
+Dcaron=0010E
+Dcy=00414
+Del=02207
+Delta=00394
+Dfr=1D507
+DiacriticalAcute=000B4
+DiacriticalDot=002D9
+DiacriticalDoubleAcute=002DD
+DiacriticalGrave=00060
+DiacriticalTilde=002DC
+Diamond=022C4
+DifferentialD=02146
+Dopf=1D53B
+Dot=000A8
+DotDot=020DC
+DotEqual=02250
+DoubleContourIntegral=0222F
+DoubleDot=000A8
+DoubleDownArrow=021D3
+DoubleLeftArrow=021D0
+DoubleLeftRightArrow=021D4
+DoubleLeftTee=02AE4
+DoubleLongLeftArrow=027F8
+DoubleLongLeftRightArrow=027FA
+DoubleLongRightArrow=027F9
+DoubleRightArrow=021D2
+DoubleRightTee=022A8
+DoubleUpArrow=021D1
+DoubleUpDownArrow=021D5
+DoubleVerticalBar=02225
+DownArrow=02193
+DownArrowBar=02913
+DownArrowUpArrow=021F5
+DownBreve=00311
+DownLeftRightVector=02950
+DownLeftTeeVector=0295E
+DownLeftVector=021BD
+DownLeftVectorBar=02956
+DownRightTeeVector=0295F
+DownRightVector=021C1
+DownRightVectorBar=02957
+DownTee=022A4
+DownTeeArrow=021A7
+Downarrow=021D3
+Dscr=1D49F
+Dstrok=00110
+ENG=0014A
+ETH=000D0
+Eacute=000C9
+Ecaron=0011A
+Ecirc=000CA
+Ecy=0042D
+Edot=00116
+Efr=1D508
+Egrave=000C8
+Element=02208
+Emacr=00112
+EmptySmallSquare=025FB
+EmptyVerySmallSquare=025AB
+Eogon=00118
+Eopf=1D53C
+Epsilon=00395
+Equal=02A75
+EqualTilde=02242
+Equilibrium=021CC
+Escr=02130
+Esim=02A73
+Eta=00397
+Euml=000CB
+Exists=02203
+ExponentialE=02147
+Fcy=00424
+Ffr=1D509
+FilledSmallSquare=025FC
+FilledVerySmallSquare=025AA
+Fopf=1D53D
+ForAll=02200
+Fouriertrf=02131
+Fscr=02131
+GJcy=00403
+GT=0003E
+Gamma=00393
+Gammad=003DC
+Gbreve=0011E
+Gcedil=00122
+Gcirc=0011C
+Gcy=00413
+Gdot=00120
+Gfr=1D50A
+Gg=022D9
+Gopf=1D53E
+GreaterEqual=02265
+GreaterEqualLess=022DB
+GreaterFullEqual=02267
+GreaterGreater=02AA2
+GreaterLess=02277
+GreaterSlantEqual=02A7E
+GreaterTilde=02273
+Gscr=1D4A2
+Gt=0226B
+HARDcy=0042A
+Hacek=002C7
+Hat=0005E
+Hcirc=00124
+Hfr=0210C
+HilbertSpace=0210B
+Hopf=0210D
+HorizontalLine=02500
+Hscr=0210B
+Hstrok=00126
+HumpDownHump=0224E
+HumpEqual=0224F
+IEcy=00415
+IJlig=00132
+IOcy=00401
+Iacute=000CD
+Icirc=000CE
+Icy=00418
+Idot=00130
+Ifr=02111
+Igrave=000CC
+Im=02111
+Imacr=0012A
+ImaginaryI=02148
+Implies=021D2
+Int=0222C
+Integral=0222B
+Intersection=022C2
+InvisibleComma=02063
+InvisibleTimes=02062
+Iogon=0012E
+Iopf=1D540
+Iota=00399
+Iscr=02110
+Itilde=00128
+Iukcy=00406
+Iuml=000CF
+Jcirc=00134
+Jcy=00419
+Jfr=1D50D
+Jopf=1D541
+Jscr=1D4A5
+Jsercy=00408
+Jukcy=00404
+KHcy=00425
+KJcy=0040C
+Kappa=0039A
+Kcedil=00136
+Kcy=0041A
+Kfr=1D50E
+Kopf=1D542
+Kscr=1D4A6
+LJcy=00409
+LT=0003C
+Lacute=00139
+Lambda=0039B
+Lang=027EA
+Laplacetrf=02112
+Larr=0219E
+Lcaron=0013D
+Lcedil=0013B
+Lcy=0041B
+LeftAngleBracket=027E8
+LeftArrow=02190
+LeftArrowBar=021E4
+LeftArrowRightArrow=021C6
+LeftCeiling=02308
+LeftDoubleBracket=027E6
+LeftDownTeeVector=02961
+LeftDownVector=021C3
+LeftDownVectorBar=02959
+LeftFloor=0230A
+LeftRightArrow=02194
+LeftRightVector=0294E
+LeftTee=022A3
+LeftTeeArrow=021A4
+LeftTeeVector=0295A
+LeftTriangle=022B2
+LeftTriangleBar=029CF
+LeftTriangleEqual=022B4
+LeftUpDownVector=02951
+LeftUpTeeVector=02960
+LeftUpVector=021BF
+LeftUpVectorBar=02958
+LeftVector=021BC
+LeftVectorBar=02952
+Leftarrow=021D0
+Leftrightarrow=021D4
+LessEqualGreater=022DA
+LessFullEqual=02266
+LessGreater=02276
+LessLess=02AA1
+LessSlantEqual=02A7D
+LessTilde=02272
+Lfr=1D50F
+Ll=022D8
+Lleftarrow=021DA
+Lmidot=0013F
+LongLeftArrow=027F5
+LongLeftRightArrow=027F7
+LongRightArrow=027F6
+Longleftarrow=027F8
+Longleftrightarrow=027FA
+Longrightarrow=027F9
+Lopf=1D543
+LowerLeftArrow=02199
+LowerRightArrow=02198
+Lscr=02112
+Lsh=021B0
+Lstrok=00141
+Lt=0226A
+Map=02905
+Mcy=0041C
+MediumSpace=0205F
+Mellintrf=02133
+Mfr=1D510
+MinusPlus=02213
+Mopf=1D544
+Mscr=02133
+Mu=0039C
+NJcy=0040A
+Nacute=00143
+Ncaron=00147
+Ncedil=00145
+Ncy=0041D
+NegativeMediumSpace=0200B
+NegativeThickSpace=0200B
+NegativeThinSpace=0200B
+NegativeVeryThinSpace=0200B
+NestedGreaterGreater=0226B
+NestedLessLess=0226A
+NewLine=0000A
+Nfr=1D511
+NoBreak=02060
+NonBreakingSpace=000A0
+Nopf=02115
+Not=02AEC
+NotCongruent=02262
+NotCupCap=0226D
+NotDoubleVerticalBar=02226
+NotElement=02209
+NotEqual=02260
+NotExists=02204
+NotGreater=0226F
+NotGreaterEqual=02271
+NotGreaterLess=02279
+NotGreaterTilde=02275
+NotLeftTriangle=022EA
+NotLeftTriangleEqual=022EC
+NotLess=0226E
+NotLessEqual=02270
+NotLessGreater=02278
+NotLessTilde=02274
+NotPrecedes=02280
+NotPrecedesSlantEqual=022E0
+NotReverseElement=0220C
+NotRightTriangle=022EB
+NotRightTriangleEqual=022ED
+NotSquareSubsetEqual=022E2
+NotSquareSupersetEqual=022E3
+NotSubsetEqual=02288
+NotSucceeds=02281
+NotSucceedsSlantEqual=022E1
+NotSupersetEqual=02289
+NotTilde=02241
+NotTildeEqual=02244
+NotTildeFullEqual=02247
+NotTildeTilde=02249
+NotVerticalBar=02224
+Nscr=1D4A9
+Ntilde=000D1
+Nu=0039D
+OElig=00152
+Oacute=000D3
+Ocirc=000D4
+Ocy=0041E
+Odblac=00150
+Ofr=1D512
+Ograve=000D2
+Omacr=0014C
+Omega=003A9
+Omicron=0039F
+Oopf=1D546
+OpenCurlyDoubleQuote=0201C
+OpenCurlyQuote=02018
+Or=02A54
+Oscr=1D4AA
+Oslash=000D8
+Otilde=000D5
+Otimes=02A37
+Ouml=000D6
+OverBar=0203E
+OverBrace=023DE
+OverBracket=023B4
+OverParenthesis=023DC
+PartialD=02202
+Pcy=0041F
+Pfr=1D513
+Phi=003A6
+Pi=003A0
+PlusMinus=000B1
+Poincareplane=0210C
+Popf=02119
+Pr=02ABB
+Precedes=0227A
+PrecedesEqual=02AAF
+PrecedesSlantEqual=0227C
+PrecedesTilde=0227E
+Prime=02033
+Product=0220F
+Proportion=02237
+Proportional=0221D
+Pscr=1D4AB
+Psi=003A8
+QUOT=00022
+Qfr=1D514
+Qopf=0211A
+Qscr=1D4AC
+RBarr=02910
+REG=000AE
+Racute=00154
+Rang=027EB
+Rarr=021A0
+Rarrtl=02916
+Rcaron=00158
+Rcedil=00156
+Rcy=00420
+Re=0211C
+ReverseElement=0220B
+ReverseEquilibrium=021CB
+ReverseUpEquilibrium=0296F
+Rfr=0211C
+Rho=003A1
+RightAngleBracket=027E9
+RightArrow=02192
+RightArrowBar=021E5
+RightArrowLeftArrow=021C4
+RightCeiling=02309
+RightDoubleBracket=027E7
+RightDownTeeVector=0295D
+RightDownVector=021C2
+RightDownVectorBar=02955
+RightFloor=0230B
+RightTee=022A2
+RightTeeArrow=021A6
+RightTeeVector=0295B
+RightTriangle=022B3
+RightTriangleBar=029D0
+RightTriangleEqual=022B5
+RightUpDownVector=0294F
+RightUpTeeVector=0295C
+RightUpVector=021BE
+RightUpVectorBar=02954
+RightVector=021C0
+RightVectorBar=02953
+Rightarrow=021D2
+Ropf=0211D
+RoundImplies=02970
+Rrightarrow=021DB
+Rscr=0211B
+Rsh=021B1
+RuleDelayed=029F4
+SHCHcy=00429
+SHcy=00428
+SOFTcy=0042C
+Sacute=0015A
+Sc=02ABC
+Scaron=00160
+Scedil=0015E
+Scirc=0015C
+Scy=00421
+Sfr=1D516
+ShortDownArrow=02193
+ShortLeftArrow=02190
+ShortRightArrow=02192
+ShortUpArrow=02191
+Sigma=003A3
+SmallCircle=02218
+Sopf=1D54A
+Sqrt=0221A
+Square=025A1
+SquareIntersection=02293
+SquareSubset=0228F
+SquareSubsetEqual=02291
+SquareSuperset=02290
+SquareSupersetEqual=02292
+SquareUnion=02294
+Sscr=1D4AE
+Star=022C6
+Sub=022D0
+Subset=022D0
+SubsetEqual=02286
+Succeeds=0227B
+SucceedsEqual=02AB0
+SucceedsSlantEqual=0227D
+SucceedsTilde=0227F
+SuchThat=0220B
+Sum=02211
+Sup=022D1
+Superset=02283
+SupersetEqual=02287
+Supset=022D1
+THORN=000DE
+TRADE=02122
+TSHcy=0040B
+TScy=00426
+Tab=00009
+Tau=003A4
+Tcaron=00164
+Tcedil=00162
+Tcy=00422
+Tfr=1D517
+Therefore=02234
+Theta=00398
+ThinSpace=02009
+Tilde=0223C
+TildeEqual=02243
+TildeFullEqual=02245
+TildeTilde=02248
+Topf=1D54B
+TripleDot=020DB
+Tscr=1D4AF
+Tstrok=00166
+Uacute=000DA
+Uarr=0219F
+Uarrocir=02949
+Ubrcy=0040E
+Ubreve=0016C
+Ucirc=000DB
+Ucy=00423
+Udblac=00170
+Ufr=1D518
+Ugrave=000D9
+Umacr=0016A
+UnderBar=0005F
+UnderBrace=023DF
+UnderBracket=023B5
+UnderParenthesis=023DD
+Union=022C3
+UnionPlus=0228E
+Uogon=00172
+Uopf=1D54C
+UpArrow=02191
+UpArrowBar=02912
+UpArrowDownArrow=021C5
+UpDownArrow=02195
+UpEquilibrium=0296E
+UpTee=022A5
+UpTeeArrow=021A5
+Uparrow=021D1
+Updownarrow=021D5
+UpperLeftArrow=02196
+UpperRightArrow=02197
+Upsi=003D2
+Upsilon=003A5
+Uring=0016E
+Uscr=1D4B0
+Utilde=00168
+Uuml=000DC
+VDash=022AB
+Vbar=02AEB
+Vcy=00412
+Vdash=022A9
+Vdashl=02AE6
+Vee=022C1
+Verbar=02016
+Vert=02016
+VerticalBar=02223
+VerticalLine=0007C
+VerticalSeparator=02758
+VerticalTilde=02240
+VeryThinSpace=0200A
+Vfr=1D519
+Vopf=1D54D
+Vscr=1D4B1
+Vvdash=022AA
+Wcirc=00174
+Wedge=022C0
+Wfr=1D51A
+Wopf=1D54E
+Wscr=1D4B2
+Xfr=1D51B
+Xi=0039E
+Xopf=1D54F
+Xscr=1D4B3
+YAcy=0042F
+YIcy=00407
+YUcy=0042E
+Yacute=000DD
+Ycirc=00176
+Ycy=0042B
+Yfr=1D51C
+Yopf=1D550
+Yscr=1D4B4
+Yuml=00178
+ZHcy=00416
+Zacute=00179
+Zcaron=0017D
+Zcy=00417
+Zdot=0017B
+ZeroWidthSpace=0200B
+Zeta=00396
+Zfr=02128
+Zopf=02124
+Zscr=1D4B5
+aacute=000E1
+abreve=00103
+ac=0223E
+acd=0223F
+acirc=000E2
+acute=000B4
+acy=00430
+aelig=000E6
+af=02061
+afr=1D51E
+agrave=000E0
+alefsym=02135
+aleph=02135
+alpha=003B1
+amacr=00101
+amalg=02A3F
+amp=00026
+and=02227
+andand=02A55
+andd=02A5C
+andslope=02A58
+andv=02A5A
+ang=02220
+ange=029A4
+angle=02220
+angmsd=02221
+angmsdaa=029A8
+angmsdab=029A9
+angmsdac=029AA
+angmsdad=029AB
+angmsdae=029AC
+angmsdaf=029AD
+angmsdag=029AE
+angmsdah=029AF
+angrt=0221F
+angrtvb=022BE
+angrtvbd=0299D
+angsph=02222
+angst=000C5
+angzarr=0237C
+aogon=00105
+aopf=1D552
+ap=02248
+apE=02A70
+apacir=02A6F
+ape=0224A
+apid=0224B
+apos=00027
+approx=02248
+approxeq=0224A
+aring=000E5
+ascr=1D4B6
+ast=0002A
+asymp=02248
+asympeq=0224D
+atilde=000E3
+auml=000E4
+awconint=02233
+awint=02A11
+bNot=02AED
+backcong=0224C
+backepsilon=003F6
+backprime=02035
+backsim=0223D
+backsimeq=022CD
+barvee=022BD
+barwed=02305
+barwedge=02305
+bbrk=023B5
+bbrktbrk=023B6
+bcong=0224C
+bcy=00431
+bdquo=0201E
+becaus=02235
+because=02235
+bemptyv=029B0
+bepsi=003F6
+bernou=0212C
+beta=003B2
+beth=02136
+between=0226C
+bfr=1D51F
+bigcap=022C2
+bigcirc=025EF
+bigcup=022C3
+bigodot=02A00
+bigoplus=02A01
+bigotimes=02A02
+bigsqcup=02A06
+bigstar=02605
+bigtriangledown=025BD
+bigtriangleup=025B3
+biguplus=02A04
+bigvee=022C1
+bigwedge=022C0
+bkarow=0290D
+blacklozenge=029EB
+blacksquare=025AA
+blacktriangle=025B4
+blacktriangledown=025BE
+blacktriangleleft=025C2
+blacktriangleright=025B8
+blank=02423
+blk12=02592
+blk14=02591
+blk34=02593
+block=02588
+bnot=02310
+bopf=1D553
+bot=022A5
+bottom=022A5
+bowtie=022C8
+boxDL=02557
+boxDR=02554
+boxDl=02556
+boxDr=02553
+boxH=02550
+boxHD=02566
+boxHU=02569
+boxHd=02564
+boxHu=02567
+boxUL=0255D
+boxUR=0255A
+boxUl=0255C
+boxUr=02559
+boxV=02551
+boxVH=0256C
+boxVL=02563
+boxVR=02560
+boxVh=0256B
+boxVl=02562
+boxVr=0255F
+boxbox=029C9
+boxdL=02555
+boxdR=02552
+boxdl=02510
+boxdr=0250C
+boxh=02500
+boxhD=02565
+boxhU=02568
+boxhd=0252C
+boxhu=02534
+boxminus=0229F
+boxplus=0229E
+boxtimes=022A0
+boxuL=0255B
+boxuR=02558
+boxul=02518
+boxur=02514
+boxv=02502
+boxvH=0256A
+boxvL=02561
+boxvR=0255E
+boxvh=0253C
+boxvl=02524
+boxvr=0251C
+bprime=02035
+breve=002D8
+brvbar=000A6
+bscr=1D4B7
+bsemi=0204F
+bsim=0223D
+bsime=022CD
+bsol=0005C
+bsolb=029C5
+bsolhsub=027C8
+bull=02022
+bullet=02022
+bump=0224E
+bumpE=02AAE
+bumpe=0224F
+bumpeq=0224F
+cacute=00107
+cap=02229
+capand=02A44
+capbrcup=02A49
+capcap=02A4B
+capcup=02A47
+capdot=02A40
+caret=02041
+caron=002C7
+ccaps=02A4D
+ccaron=0010D
+ccedil=000E7
+ccirc=00109
+ccups=02A4C
+ccupssm=02A50
+cdot=0010B
+cedil=000B8
+cemptyv=029B2
+cent=000A2
+centerdot=000B7
+cfr=1D520
+chcy=00447
+check=02713
+checkmark=02713
+chi=003C7
+cir=025CB
+cirE=029C3
+circ=002C6
+circeq=02257
+circlearrowleft=021BA
+circlearrowright=021BB
+circledR=000AE
+circledS=024C8
+circledast=0229B
+circledcirc=0229A
+circleddash=0229D
+cire=02257
+cirfnint=02A10
+cirmid=02AEF
+cirscir=029C2
+clubs=02663
+clubsuit=02663
+colon=0003A
+colone=02254
+coloneq=02254
+comma=0002C
+commat=00040
+comp=02201
+compfn=02218
+complement=02201
+complexes=02102
+cong=02245
+congdot=02A6D
+conint=0222E
+copf=1D554
+coprod=02210
+copy=000A9
+copysr=02117
+crarr=021B5
+cross=02717
+cscr=1D4B8
+csub=02ACF
+csube=02AD1
+csup=02AD0
+csupe=02AD2
+ctdot=022EF
+cudarrl=02938
+cudarrr=02935
+cuepr=022DE
+cuesc=022DF
+cularr=021B6
+cularrp=0293D
+cup=0222A
+cupbrcap=02A48
+cupcap=02A46
+cupcup=02A4A
+cupdot=0228D
+cupor=02A45
+curarr=021B7
+curarrm=0293C
+curlyeqprec=022DE
+curlyeqsucc=022DF
+curlyvee=022CE
+curlywedge=022CF
+curren=000A4
+curvearrowleft=021B6
+curvearrowright=021B7
+cuvee=022CE
+cuwed=022CF
+cwconint=02232
+cwint=02231
+cylcty=0232D
+dArr=021D3
+dHar=02965
+dagger=02020
+daleth=02138
+darr=02193
+dash=02010
+dashv=022A3
+dbkarow=0290F
+dblac=002DD
+dcaron=0010F
+dcy=00434
+dd=02146
+ddagger=02021
+ddarr=021CA
+ddotseq=02A77
+deg=000B0
+delta=003B4
+demptyv=029B1
+dfisht=0297F
+dfr=1D521
+dharl=021C3
+dharr=021C2
+diam=022C4
+diamond=022C4
+diamondsuit=02666
+diams=02666
+die=000A8
+digamma=003DD
+disin=022F2
+div=000F7
+divide=000F7
+divideontimes=022C7
+divonx=022C7
+djcy=00452
+dlcorn=0231E
+dlcrop=0230D
+dollar=00024
+dopf=1D555
+dot=002D9
+doteq=02250
+doteqdot=02251
+dotminus=02238
+dotplus=02214
+dotsquare=022A1
+doublebarwedge=02306
+downarrow=02193
+downdownarrows=021CA
+downharpoonleft=021C3
+downharpoonright=021C2
+drbkarow=02910
+drcorn=0231F
+drcrop=0230C
+dscr=1D4B9
+dscy=00455
+dsol=029F6
+dstrok=00111
+dtdot=022F1
+dtri=025BF
+dtrif=025BE
+duarr=021F5
+duhar=0296F
+dwangle=029A6
+dzcy=0045F
+dzigrarr=027FF
+eDDot=02A77
+eDot=02251
+eacute=000E9
+easter=02A6E
+ecaron=0011B
+ecir=02256
+ecirc=000EA
+ecolon=02255
+ecy=0044D
+edot=00117
+ee=02147
+efDot=02252
+efr=1D522
+eg=02A9A
+egrave=000E8
+egs=02A96
+egsdot=02A98
+el=02A99
+elinters=023E7
+ell=02113
+els=02A95
+elsdot=02A97
+emacr=00113
+empty=02205
+emptyset=02205
+emptyv=02205
+emsp13=02004
+emsp14=02005
+emsp=02003
+eng=0014B
+ensp=02002
+eogon=00119
+eopf=1D556
+epar=022D5
+eparsl=029E3
+eplus=02A71
+epsi=003B5
+epsilon=003B5
+epsiv=003F5
+eqcirc=02256
+eqcolon=02255
+eqsim=02242
+eqslantgtr=02A96
+eqslantless=02A95
+equals=0003D
+equest=0225F
+equiv=02261
+equivDD=02A78
+eqvparsl=029E5
+erDot=02253
+erarr=02971
+escr=0212F
+esdot=02250
+esim=02242
+eta=003B7
+eth=000F0
+euml=000EB
+euro=020AC
+excl=00021
+exist=02203
+expectation=02130
+exponentiale=02147
+fallingdotseq=02252
+fcy=00444
+female=02640
+ffilig=0FB03
+fflig=0FB00
+ffllig=0FB04
+ffr=1D523
+filig=0FB01
+flat=0266D
+fllig=0FB02
+fltns=025B1
+fnof=00192
+fopf=1D557
+forall=02200
+fork=022D4
+forkv=02AD9
+fpartint=02A0D
+frac12=000BD
+frac13=02153
+frac14=000BC
+frac15=02155
+frac16=02159
+frac18=0215B
+frac23=02154
+frac25=02156
+frac34=000BE
+frac35=02157
+frac38=0215C
+frac45=02158
+frac56=0215A
+frac58=0215D
+frac78=0215E
+frasl=02044
+frown=02322
+fscr=1D4BB
+gE=02267
+gEl=02A8C
+gacute=001F5
+gamma=003B3
+gammad=003DD
+gap=02A86
+gbreve=0011F
+gcirc=0011D
+gcy=00433
+gdot=00121
+ge=02265
+gel=022DB
+geq=02265
+geqq=02267
+geqslant=02A7E
+ges=02A7E
+gescc=02AA9
+gesdot=02A80
+gesdoto=02A82
+gesdotol=02A84
+gesles=02A94
+gfr=1D524
+gg=0226B
+ggg=022D9
+gimel=02137
+gjcy=00453
+gl=02277
+glE=02A92
+gla=02AA5
+glj=02AA4
+gnE=02269
+gnap=02A8A
+gnapprox=02A8A
+gne=02A88
+gneq=02A88
+gneqq=02269
+gnsim=022E7
+gopf=1D558
+grave=00060
+gscr=0210A
+gsim=02273
+gsime=02A8E
+gsiml=02A90
+gt=0003E
+gtcc=02AA7
+gtcir=02A7A
+gtdot=022D7
+gtlPar=02995
+gtquest=02A7C
+gtrapprox=02A86
+gtrarr=02978
+gtrdot=022D7
+gtreqless=022DB
+gtreqqless=02A8C
+gtrless=02277
+gtrsim=02273
+hArr=021D4
+hairsp=0200A
+half=000BD
+hamilt=0210B
+hardcy=0044A
+harr=02194
+harrcir=02948
+harrw=021AD
+hbar=0210F
+hcirc=00125
+hearts=02665
+heartsuit=02665
+hellip=02026
+hercon=022B9
+hfr=1D525
+hksearow=02925
+hkswarow=02926
+hoarr=021FF
+homtht=0223B
+hookleftarrow=021A9
+hookrightarrow=021AA
+hopf=1D559
+horbar=02015
+hscr=1D4BD
+hslash=0210F
+hstrok=00127
+hybull=02043
+hyphen=02010
+iacute=000ED
+ic=02063
+icirc=000EE
+icy=00438
+iecy=00435
+iexcl=000A1
+iff=021D4
+ifr=1D526
+igrave=000EC
+ii=02148
+iiiint=02A0C
+iiint=0222D
+iinfin=029DC
+iiota=02129
+ijlig=00133
+imacr=0012B
+image=02111
+imagline=02110
+imagpart=02111
+imath=00131
+imof=022B7
+imped=001B5
+in=02208
+incare=02105
+infin=0221E
+infintie=029DD
+inodot=00131
+int=0222B
+intcal=022BA
+integers=02124
+intercal=022BA
+intlarhk=02A17
+intprod=02A3C
+iocy=00451
+iogon=0012F
+iopf=1D55A
+iota=003B9
+iprod=02A3C
+iquest=000BF
+iscr=1D4BE
+isin=02208
+isinE=022F9
+isindot=022F5
+isins=022F4
+isinsv=022F3
+isinv=02208
+it=02062
+itilde=00129
+iukcy=00456
+iuml=000EF
+jcirc=00135
+jcy=00439
+jfr=1D527
+jmath=00237
+jopf=1D55B
+jscr=1D4BF
+jsercy=00458
+jukcy=00454
+kappa=003BA
+kappav=003F0
+kcedil=00137
+kcy=0043A
+kfr=1D528
+kgreen=00138
+khcy=00445
+kjcy=0045C
+kopf=1D55C
+kscr=1D4C0
+lAarr=021DA
+lArr=021D0
+lAtail=0291B
+lBarr=0290E
+lE=02266
+lEg=02A8B
+lHar=02962
+lacute=0013A
+laemptyv=029B4
+lagran=02112
+lambda=003BB
+lang=027E8
+langd=02991
+langle=027E8
+lap=02A85
+laquo=000AB
+larr=02190
+larrb=021E4
+larrbfs=0291F
+larrfs=0291D
+larrhk=021A9
+larrlp=021AB
+larrpl=02939
+larrsim=02973
+larrtl=021A2
+lat=02AAB
+latail=02919
+late=02AAD
+lbarr=0290C
+lbbrk=02772
+lbrace=0007B
+lbrack=0005B
+lbrke=0298B
+lbrksld=0298F
+lbrkslu=0298D
+lcaron=0013E
+lcedil=0013C
+lceil=02308
+lcub=0007B
+lcy=0043B
+ldca=02936
+ldquo=0201C
+ldquor=0201E
+ldrdhar=02967
+ldrushar=0294B
+ldsh=021B2
+le=02264
+leftarrow=02190
+leftarrowtail=021A2
+leftharpoondown=021BD
+leftharpoonup=021BC
+leftleftarrows=021C7
+leftrightarrow=02194
+leftrightarrows=021C6
+leftrightharpoons=021CB
+leftrightsquigarrow=021AD
+leftthreetimes=022CB
+leg=022DA
+leq=02264
+leqq=02266
+leqslant=02A7D
+les=02A7D
+lescc=02AA8
+lesdot=02A7F
+lesdoto=02A81
+lesdotor=02A83
+lesges=02A93
+lessapprox=02A85
+lessdot=022D6
+lesseqgtr=022DA
+lesseqqgtr=02A8B
+lessgtr=02276
+lesssim=02272
+lfisht=0297C
+lfloor=0230A
+lfr=1D529
+lg=02276
+lgE=02A91
+lhard=021BD
+lharu=021BC
+lharul=0296A
+lhblk=02584
+ljcy=00459
+ll=0226A
+llarr=021C7
+llcorner=0231E
+llhard=0296B
+lltri=025FA
+lmidot=00140
+lmoust=023B0
+lmoustache=023B0
+lnE=02268
+lnap=02A89
+lnapprox=02A89
+lne=02A87
+lneq=02A87
+lneqq=02268
+lnsim=022E6
+loang=027EC
+loarr=021FD
+lobrk=027E6
+longleftarrow=027F5
+longleftrightarrow=027F7
+longmapsto=027FC
+longrightarrow=027F6
+looparrowleft=021AB
+looparrowright=021AC
+lopar=02985
+lopf=1D55D
+loplus=02A2D
+lotimes=02A34
+lowast=02217
+lowbar=0005F
+loz=025CA
+lozenge=025CA
+lozf=029EB
+lpar=00028
+lparlt=02993
+lrarr=021C6
+lrcorner=0231F
+lrhar=021CB
+lrhard=0296D
+lrm=0200E
+lrtri=022BF
+lsaquo=02039
+lscr=1D4C1
+lsh=021B0
+lsim=02272
+lsime=02A8D
+lsimg=02A8F
+lsqb=0005B
+lsquo=02018
+lsquor=0201A
+lstrok=00142
+lt=0003C
+ltcc=02AA6
+ltcir=02A79
+ltdot=022D6
+lthree=022CB
+ltimes=022C9
+ltlarr=02976
+ltquest=02A7B
+ltrPar=02996
+ltri=025C3
+ltrie=022B4
+ltrif=025C2
+lurdshar=0294A
+luruhar=02966
+mDDot=0223A
+macr=000AF
+male=02642
+malt=02720
+maltese=02720
+map=021A6
+mapsto=021A6
+mapstodown=021A7
+mapstoleft=021A4
+mapstoup=021A5
+marker=025AE
+mcomma=02A29
+mcy=0043C
+mdash=02014
+measuredangle=02221
+mfr=1D52A
+mho=02127
+micro=000B5
+mid=02223
+midast=0002A
+midcir=02AF0
+middot=000B7
+minus=02212
+minusb=0229F
+minusd=02238
+minusdu=02A2A
+mlcp=02ADB
+mldr=02026
+mnplus=02213
+models=022A7
+mopf=1D55E
+mp=02213
+mscr=1D4C2
+mstpos=0223E
+mu=003BC
+multimap=022B8
+mumap=022B8
+nLeftarrow=021CD
+nLeftrightarrow=021CE
+nRightarrow=021CF
+nVDash=022AF
+nVdash=022AE
+nabla=02207
+nacute=00144
+nap=02249
+napos=00149
+napprox=02249
+natur=0266E
+natural=0266E
+naturals=02115
+nbsp=000A0
+ncap=02A43
+ncaron=00148
+ncedil=00146
+ncong=02247
+ncup=02A42
+ncy=0043D
+ndash=02013
+ne=02260
+neArr=021D7
+nearhk=02924
+nearr=02197
+nearrow=02197
+nequiv=02262
+nesear=02928
+nexist=02204
+nexists=02204
+nfr=1D52B
+nge=02271
+ngeq=02271
+ngsim=02275
+ngt=0226F
+ngtr=0226F
+nhArr=021CE
+nharr=021AE
+nhpar=02AF2
+ni=0220B
+nis=022FC
+nisd=022FA
+niv=0220B
+njcy=0045A
+nlArr=021CD
+nlarr=0219A
+nldr=02025
+nle=02270
+nleftarrow=0219A
+nleftrightarrow=021AE
+nleq=02270
+nless=0226E
+nlsim=02274
+nlt=0226E
+nltri=022EA
+nltrie=022EC
+nmid=02224
+nopf=1D55F
+not=000AC
+notin=02209
+notinva=02209
+notinvb=022F7
+notinvc=022F6
+notni=0220C
+notniva=0220C
+notnivb=022FE
+notnivc=022FD
+npar=02226
+nparallel=02226
+npolint=02A14
+npr=02280
+nprcue=022E0
+nprec=02280
+nrArr=021CF
+nrarr=0219B
+nrightarrow=0219B
+nrtri=022EB
+nrtrie=022ED
+nsc=02281
+nsccue=022E1
+nscr=1D4C3
+nshortmid=02224
+nshortparallel=02226
+nsim=02241
+nsime=02244
+nsimeq=02244
+nsmid=02224
+nspar=02226
+nsqsube=022E2
+nsqsupe=022E3
+nsub=02284
+nsube=02288
+nsubseteq=02288
+nsucc=02281
+nsup=02285
+nsupe=02289
+nsupseteq=02289
+ntgl=02279
+ntilde=000F1
+ntlg=02278
+ntriangleleft=022EA
+ntrianglelefteq=022EC
+ntriangleright=022EB
+ntrianglerighteq=022ED
+nu=003BD
+num=00023
+numero=02116
+numsp=02007
+nvDash=022AD
+nvHarr=02904
+nvdash=022AC
+nvinfin=029DE
+nvlArr=02902
+nvrArr=02903
+nwArr=021D6
+nwarhk=02923
+nwarr=02196
+nwarrow=02196
+nwnear=02927
+oS=024C8
+oacute=000F3
+oast=0229B
+ocir=0229A
+ocirc=000F4
+ocy=0043E
+odash=0229D
+odblac=00151
+odiv=02A38
+odot=02299
+odsold=029BC
+oelig=00153
+ofcir=029BF
+ofr=1D52C
+ogon=002DB
+ograve=000F2
+ogt=029C1
+ohbar=029B5
+ohm=003A9
+oint=0222E
+olarr=021BA
+olcir=029BE
+olcross=029BB
+oline=0203E
+olt=029C0
+omacr=0014D
+omega=003C9
+omicron=003BF
+omid=029B6
+ominus=02296
+oopf=1D560
+opar=029B7
+operp=029B9
+oplus=02295
+or=02228
+orarr=021BB
+ord=02A5D
+order=02134
+orderof=02134
+ordf=000AA
+ordm=000BA
+origof=022B6
+oror=02A56
+orslope=02A57
+orv=02A5B
+oscr=02134
+oslash=000F8
+osol=02298
+otilde=000F5
+otimes=02297
+otimesas=02A36
+ouml=000F6
+ovbar=0233D
+par=02225
+para=000B6
+parallel=02225
+parsim=02AF3
+parsl=02AFD
+part=02202
+pcy=0043F
+percnt=00025
+period=0002E
+permil=02030
+perp=022A5
+pertenk=02031
+pfr=1D52D
+phi=003C6
+phiv=003D5
+phmmat=02133
+phone=0260E
+pi=003C0
+pitchfork=022D4
+piv=003D6
+planck=0210F
+planckh=0210E
+plankv=0210F
+plus=0002B
+plusacir=02A23
+plusb=0229E
+pluscir=02A22
+plusdo=02214
+plusdu=02A25
+pluse=02A72
+plusmn=000B1
+plussim=02A26
+plustwo=02A27
+pm=000B1
+pointint=02A15
+popf=1D561
+pound=000A3
+pr=0227A
+prE=02AB3
+prap=02AB7
+prcue=0227C
+pre=02AAF
+prec=0227A
+precapprox=02AB7
+preccurlyeq=0227C
+preceq=02AAF
+precnapprox=02AB9
+precneqq=02AB5
+precnsim=022E8
+precsim=0227E
+prime=02032
+primes=02119
+prnE=02AB5
+prnap=02AB9
+prnsim=022E8
+prod=0220F
+profalar=0232E
+profline=02312
+profsurf=02313
+prop=0221D
+propto=0221D
+prsim=0227E
+prurel=022B0
+pscr=1D4C5
+psi=003C8
+puncsp=02008
+qfr=1D52E
+qint=02A0C
+qopf=1D562
+qprime=02057
+qscr=1D4C6
+quaternions=0210D
+quatint=02A16
+quest=0003F
+questeq=0225F
+quot=00022
+rAarr=021DB
+rArr=021D2
+rAtail=0291C
+rBarr=0290F
+rHar=02964
+racute=00155
+radic=0221A
+raemptyv=029B3
+rang=027E9
+rangd=02992
+range=029A5
+rangle=027E9
+raquo=000BB
+rarr=02192
+rarrap=02975
+rarrb=021E5
+rarrbfs=02920
+rarrc=02933
+rarrfs=0291E
+rarrhk=021AA
+rarrlp=021AC
+rarrpl=02945
+rarrsim=02974
+rarrtl=021A3
+rarrw=0219D
+ratail=0291A
+ratio=02236
+rationals=0211A
+rbarr=0290D
+rbbrk=02773
+rbrace=0007D
+rbrack=0005D
+rbrke=0298C
+rbrksld=0298E
+rbrkslu=02990
+rcaron=00159
+rcedil=00157
+rceil=02309
+rcub=0007D
+rcy=00440
+rdca=02937
+rdldhar=02969
+rdquo=0201D
+rdquor=0201D
+rdsh=021B3
+real=0211C
+realine=0211B
+realpart=0211C
+reals=0211D
+rect=025AD
+reg=000AE
+rfisht=0297D
+rfloor=0230B
+rfr=1D52F
+rhard=021C1
+rharu=021C0
+rharul=0296C
+rho=003C1
+rhov=003F1
+rightarrow=02192
+rightarrowtail=021A3
+rightharpoondown=021C1
+rightharpoonup=021C0
+rightleftarrows=021C4
+rightleftharpoons=021CC
+rightrightarrows=021C9
+rightsquigarrow=0219D
+rightthreetimes=022CC
+ring=002DA
+risingdotseq=02253
+rlarr=021C4
+rlhar=021CC
+rlm=0200F
+rmoust=023B1
+rmoustache=023B1
+rnmid=02AEE
+roang=027ED
+roarr=021FE
+robrk=027E7
+ropar=02986
+ropf=1D563
+roplus=02A2E
+rotimes=02A35
+rpar=00029
+rpargt=02994
+rppolint=02A12
+rrarr=021C9
+rsaquo=0203A
+rscr=1D4C7
+rsh=021B1
+rsqb=0005D
+rsquo=02019
+rsquor=02019
+rthree=022CC
+rtimes=022CA
+rtri=025B9
+rtrie=022B5
+rtrif=025B8
+rtriltri=029CE
+ruluhar=02968
+rx=0211E
+sacute=0015B
+sbquo=0201A
+sc=0227B
+scE=02AB4
+scap=02AB8
+scaron=00161
+sccue=0227D
+sce=02AB0
+scedil=0015F
+scirc=0015D
+scnE=02AB6
+scnap=02ABA
+scnsim=022E9
+scpolint=02A13
+scsim=0227F
+scy=00441
+sdot=022C5
+sdotb=022A1
+sdote=02A66
+seArr=021D8
+searhk=02925
+searr=02198
+searrow=02198
+sect=000A7
+semi=0003B
+seswar=02929
+setminus=02216
+setmn=02216
+sext=02736
+sfr=1D530
+sfrown=02322
+sharp=0266F
+shchcy=00449
+shcy=00448
+shortmid=02223
+shortparallel=02225
+shy=000AD
+sigma=003C3
+sigmaf=003C2
+sigmav=003C2
+sim=0223C
+simdot=02A6A
+sime=02243
+simeq=02243
+simg=02A9E
+simgE=02AA0
+siml=02A9D
+simlE=02A9F
+simne=02246
+simplus=02A24
+simrarr=02972
+slarr=02190
+smallsetminus=02216
+smashp=02A33
+smeparsl=029E4
+smid=02223
+smile=02323
+smt=02AAA
+smte=02AAC
+softcy=0044C
+sol=0002F
+solb=029C4
+solbar=0233F
+sopf=1D564
+spades=02660
+spadesuit=02660
+spar=02225
+sqcap=02293
+sqcup=02294
+sqsub=0228F
+sqsube=02291
+sqsubset=0228F
+sqsubseteq=02291
+sqsup=02290
+sqsupe=02292
+sqsupset=02290
+sqsupseteq=02292
+squ=025A1
+square=025A1
+squarf=025AA
+squf=025AA
+srarr=02192
+sscr=1D4C8
+ssetmn=02216
+ssmile=02323
+sstarf=022C6
+star=02606
+starf=02605
+straightepsilon=003F5
+straightphi=003D5
+strns=000AF
+sub=02282
+subE=02AC5
+subdot=02ABD
+sube=02286
+subedot=02AC3
+submult=02AC1
+subnE=02ACB
+subne=0228A
+subplus=02ABF
+subrarr=02979
+subset=02282
+subseteq=02286
+subseteqq=02AC5
+subsetneq=0228A
+subsetneqq=02ACB
+subsim=02AC7
+subsub=02AD5
+subsup=02AD3
+succ=0227B
+succapprox=02AB8
+succcurlyeq=0227D
+succeq=02AB0
+succnapprox=02ABA
+succneqq=02AB6
+succnsim=022E9
+succsim=0227F
+sum=02211
+sung=0266A
+sup1=000B9
+sup2=000B2
+sup3=000B3
+sup=02283
+supE=02AC6
+supdot=02ABE
+supdsub=02AD8
+supe=02287
+supedot=02AC4
+suphsol=027C9
+suphsub=02AD7
+suplarr=0297B
+supmult=02AC2
+supnE=02ACC
+supne=0228B
+supplus=02AC0
+supset=02283
+supseteq=02287
+supseteqq=02AC6
+supsetneq=0228B
+supsetneqq=02ACC
+supsim=02AC8
+supsub=02AD4
+supsup=02AD6
+swArr=021D9
+swarhk=02926
+swarr=02199
+swarrow=02199
+swnwar=0292A
+szlig=000DF
+target=02316
+tau=003C4
+tbrk=023B4
+tcaron=00165
+tcedil=00163
+tcy=00442
+tdot=020DB
+telrec=02315
+tfr=1D531
+there4=02234
+therefore=02234
+theta=003B8
+thetasym=003D1
+thetav=003D1
+thickapprox=02248
+thicksim=0223C
+thinsp=02009
+thkap=02248
+thksim=0223C
+thorn=000FE
+tilde=002DC
+times=000D7
+timesb=022A0
+timesbar=02A31
+timesd=02A30
+tint=0222D
+toea=02928
+top=022A4
+topbot=02336
+topcir=02AF1
+topf=1D565
+topfork=02ADA
+tosa=02929
+tprime=02034
+trade=02122
+triangle=025B5
+triangledown=025BF
+triangleleft=025C3
+trianglelefteq=022B4
+triangleq=0225C
+triangleright=025B9
+trianglerighteq=022B5
+tridot=025EC
+trie=0225C
+triminus=02A3A
+triplus=02A39
+trisb=029CD
+tritime=02A3B
+trpezium=023E2
+tscr=1D4C9
+tscy=00446
+tshcy=0045B
+tstrok=00167
+twixt=0226C
+twoheadleftarrow=0219E
+twoheadrightarrow=021A0
+uArr=021D1
+uHar=02963
+uacute=000FA
+uarr=02191
+ubrcy=0045E
+ubreve=0016D
+ucirc=000FB
+ucy=00443
+udarr=021C5
+udblac=00171
+udhar=0296E
+ufisht=0297E
+ufr=1D532
+ugrave=000F9
+uharl=021BF
+uharr=021BE
+uhblk=02580
+ulcorn=0231C
+ulcorner=0231C
+ulcrop=0230F
+ultri=025F8
+umacr=0016B
+uml=000A8
+uogon=00173
+uopf=1D566
+uparrow=02191
+updownarrow=02195
+upharpoonleft=021BF
+upharpoonright=021BE
+uplus=0228E
+upsi=003C5
+upsih=003D2
+upsilon=003C5
+upuparrows=021C8
+urcorn=0231D
+urcorner=0231D
+urcrop=0230E
+uring=0016F
+urtri=025F9
+uscr=1D4CA
+utdot=022F0
+utilde=00169
+utri=025B5
+utrif=025B4
+uuarr=021C8
+uuml=000FC
+uwangle=029A7
+vArr=021D5
+vBar=02AE8
+vBarv=02AE9
+vDash=022A8
+vangrt=0299C
+varepsilon=003F5
+varkappa=003F0
+varnothing=02205
+varphi=003D5
+varpi=003D6
+varpropto=0221D
+varr=02195
+varrho=003F1
+varsigma=003C2
+vartheta=003D1
+vartriangleleft=022B2
+vartriangleright=022B3
+vcy=00432
+vdash=022A2
+vee=02228
+veebar=022BB
+veeeq=0225A
+vellip=022EE
+verbar=0007C
+vert=0007C
+vfr=1D533
+vltri=022B2
+vopf=1D567
+vprop=0221D
+vrtri=022B3
+vscr=1D4CB
+vzigzag=0299A
+wcirc=00175
+wedbar=02A5F
+wedge=02227
+wedgeq=02259
+weierp=02118
+wfr=1D534
+wopf=1D568
+wp=02118
+wr=02240
+wreath=02240
+wscr=1D4CC
+xcap=022C2
+xcirc=025EF
+xcup=022C3
+xdtri=025BD
+xfr=1D535
+xhArr=027FA
+xharr=027F7
+xi=003BE
+xlArr=027F8
+xlarr=027F5
+xmap=027FC
+xnis=022FB
+xodot=02A00
+xopf=1D569
+xoplus=02A01
+xotime=02A02
+xrArr=027F9
+xrarr=027F6
+xscr=1D4CD
+xsqcup=02A06
+xuplus=02A04
+xutri=025B3
+xvee=022C1
+xwedge=022C0
+yacute=000FD
+yacy=0044F
+ycirc=00177
+ycy=0044B
+yen=000A5
+yfr=1D536
+yicy=00457
+yopf=1D56A
+yscr=1D4CE
+yucy=0044E
+yuml=000FF
+zacute=0017A
+zcaron=0017E
+zcy=00437
+zdot=0017C
+zeetrf=02128
+zeta=003B6
+zfr=1D537
+zhcy=00436
+zigrarr=021DD
+zopf=1D56B
+zscr=1D4CF
+zwj=0200D
+zwnj=0200C
diff --git a/server/src/org/jsoup/nodes/package-info.java b/server/src/org/jsoup/nodes/package-info.java
new file mode 100644
index 0000000000..24b12803ff
--- /dev/null
+++ b/server/src/org/jsoup/nodes/package-info.java
@@ -0,0 +1,4 @@
+/**
+ HTML document structure nodes.
+ */
+package org.jsoup.nodes; \ No newline at end of file
diff --git a/server/src/org/jsoup/package-info.java b/server/src/org/jsoup/package-info.java
new file mode 100644
index 0000000000..49526116b4
--- /dev/null
+++ b/server/src/org/jsoup/package-info.java
@@ -0,0 +1,4 @@
+/**
+ Contains the main {@link org.jsoup.Jsoup} class, which provides convenient static access to the jsoup functionality.
+ */
+package org.jsoup; \ No newline at end of file
diff --git a/server/src/org/jsoup/parser/CharacterReader.java b/server/src/org/jsoup/parser/CharacterReader.java
new file mode 100644
index 0000000000..b549a571a0
--- /dev/null
+++ b/server/src/org/jsoup/parser/CharacterReader.java
@@ -0,0 +1,230 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.Validate;
+
+/**
+ CharacterReader consumes tokens off a string. To replace the old TokenQueue.
+ */
+class CharacterReader {
+ static final char EOF = (char) -1;
+
+ private final String input;
+ private final int length;
+ private int pos = 0;
+ private int mark = 0;
+
+ CharacterReader(String input) {
+ Validate.notNull(input);
+ input = input.replaceAll("\r\n?", "\n"); // normalise carriage returns to newlines
+
+ this.input = input;
+ this.length = input.length();
+ }
+
+ int pos() {
+ return pos;
+ }
+
+ boolean isEmpty() {
+ return pos >= length;
+ }
+
+ char current() {
+ return isEmpty() ? EOF : input.charAt(pos);
+ }
+
+ char consume() {
+ char val = isEmpty() ? EOF : input.charAt(pos);
+ pos++;
+ return val;
+ }
+
+ void unconsume() {
+ pos--;
+ }
+
+ void advance() {
+ pos++;
+ }
+
+ void mark() {
+ mark = pos;
+ }
+
+ void rewindToMark() {
+ pos = mark;
+ }
+
+ String consumeAsString() {
+ return input.substring(pos, pos++);
+ }
+
+ String consumeTo(char c) {
+ int offset = input.indexOf(c, pos);
+ if (offset != -1) {
+ String consumed = input.substring(pos, offset);
+ pos += consumed.length();
+ return consumed;
+ } else {
+ return consumeToEnd();
+ }
+ }
+
+ String consumeTo(String seq) {
+ int offset = input.indexOf(seq, pos);
+ if (offset != -1) {
+ String consumed = input.substring(pos, offset);
+ pos += consumed.length();
+ return consumed;
+ } else {
+ return consumeToEnd();
+ }
+ }
+
+ String consumeToAny(char... seq) {
+ int start = pos;
+
+ OUTER: while (!isEmpty()) {
+ char c = input.charAt(pos);
+ for (char seek : seq) {
+ if (seek == c)
+ break OUTER;
+ }
+ pos++;
+ }
+
+ return pos > start ? input.substring(start, pos) : "";
+ }
+
+ String consumeToEnd() {
+ String data = input.substring(pos, input.length());
+ pos = input.length();
+ return data;
+ }
+
+ String consumeLetterSequence() {
+ int start = pos;
+ while (!isEmpty()) {
+ char c = input.charAt(pos);
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
+ pos++;
+ else
+ break;
+ }
+
+ return input.substring(start, pos);
+ }
+
+ String consumeLetterThenDigitSequence() {
+ int start = pos;
+ while (!isEmpty()) {
+ char c = input.charAt(pos);
+ if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
+ pos++;
+ else
+ break;
+ }
+ while (!isEmpty()) {
+ char c = input.charAt(pos);
+ if (c >= '0' && c <= '9')
+ pos++;
+ else
+ break;
+ }
+
+ return input.substring(start, pos);
+ }
+
+ String consumeHexSequence() {
+ int start = pos;
+ while (!isEmpty()) {
+ char c = input.charAt(pos);
+ if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))
+ pos++;
+ else
+ break;
+ }
+ return input.substring(start, pos);
+ }
+
+ String consumeDigitSequence() {
+ int start = pos;
+ while (!isEmpty()) {
+ char c = input.charAt(pos);
+ if (c >= '0' && c <= '9')
+ pos++;
+ else
+ break;
+ }
+ return input.substring(start, pos);
+ }
+
+ boolean matches(char c) {
+ return !isEmpty() && input.charAt(pos) == c;
+
+ }
+
+ boolean matches(String seq) {
+ return input.startsWith(seq, pos);
+ }
+
+ boolean matchesIgnoreCase(String seq) {
+ return input.regionMatches(true, pos, seq, 0, seq.length());
+ }
+
+ boolean matchesAny(char... seq) {
+ if (isEmpty())
+ return false;
+
+ char c = input.charAt(pos);
+ for (char seek : seq) {
+ if (seek == c)
+ return true;
+ }
+ return false;
+ }
+
+ boolean matchesLetter() {
+ if (isEmpty())
+ return false;
+ char c = input.charAt(pos);
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
+ }
+
+ boolean matchesDigit() {
+ if (isEmpty())
+ return false;
+ char c = input.charAt(pos);
+ return (c >= '0' && c <= '9');
+ }
+
+ boolean matchConsume(String seq) {
+ if (matches(seq)) {
+ pos += seq.length();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ boolean matchConsumeIgnoreCase(String seq) {
+ if (matchesIgnoreCase(seq)) {
+ pos += seq.length();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ boolean containsIgnoreCase(String seq) {
+ // used to check presence of </title>, </style>. only finds consistent case.
+ String loScan = seq.toLowerCase();
+ String hiScan = seq.toUpperCase();
+ return (input.indexOf(loScan, pos) > -1) || (input.indexOf(hiScan, pos) > -1);
+ }
+
+ @Override
+ public String toString() {
+ return input.substring(pos);
+ }
+}
diff --git a/server/src/org/jsoup/parser/HtmlTreeBuilder.java b/server/src/org/jsoup/parser/HtmlTreeBuilder.java
new file mode 100644
index 0000000000..457a4c3249
--- /dev/null
+++ b/server/src/org/jsoup/parser/HtmlTreeBuilder.java
@@ -0,0 +1,672 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.DescendableLinkedList;
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.*;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * HTML Tree Builder; creates a DOM from Tokens.
+ */
+class HtmlTreeBuilder extends TreeBuilder {
+
+ private HtmlTreeBuilderState state; // the current state
+ private HtmlTreeBuilderState originalState; // original / marked state
+
+ private boolean baseUriSetFromDoc = false;
+ private Element headElement; // the current head element
+ private Element formElement; // the current form element
+ private Element contextElement; // fragment parse context -- could be null even if fragment parsing
+ private DescendableLinkedList<Element> formattingElements = new DescendableLinkedList<Element>(); // active (open) formatting elements
+ private List<Token.Character> pendingTableCharacters = new ArrayList<Token.Character>(); // chars in table to be shifted out
+
+ private boolean framesetOk = true; // if ok to go into frameset
+ private boolean fosterInserts = false; // if next inserts should be fostered
+ private boolean fragmentParsing = false; // if parsing a fragment of html
+
+ HtmlTreeBuilder() {}
+
+ @Override
+ Document parse(String input, String baseUri, ParseErrorList errors) {
+ state = HtmlTreeBuilderState.Initial;
+ return super.parse(input, baseUri, errors);
+ }
+
+ List<Node> parseFragment(String inputFragment, Element context, String baseUri, ParseErrorList errors) {
+ // context may be null
+ state = HtmlTreeBuilderState.Initial;
+ initialiseParse(inputFragment, baseUri, errors);
+ contextElement = context;
+ fragmentParsing = true;
+ Element root = null;
+
+ if (context != null) {
+ if (context.ownerDocument() != null) // quirks setup:
+ doc.quirksMode(context.ownerDocument().quirksMode());
+
+ // initialise the tokeniser state:
+ String contextTag = context.tagName();
+ if (StringUtil.in(contextTag, "title", "textarea"))
+ tokeniser.transition(TokeniserState.Rcdata);
+ else if (StringUtil.in(contextTag, "iframe", "noembed", "noframes", "style", "xmp"))
+ tokeniser.transition(TokeniserState.Rawtext);
+ else if (contextTag.equals("script"))
+ tokeniser.transition(TokeniserState.ScriptData);
+ else if (contextTag.equals(("noscript")))
+ tokeniser.transition(TokeniserState.Data); // if scripting enabled, rawtext
+ else if (contextTag.equals("plaintext"))
+ tokeniser.transition(TokeniserState.Data);
+ else
+ tokeniser.transition(TokeniserState.Data); // default
+
+ root = new Element(Tag.valueOf("html"), baseUri);
+ doc.appendChild(root);
+ stack.push(root);
+ resetInsertionMode();
+ // todo: setup form element to nearest form on context (up ancestor chain)
+ }
+
+ runParser();
+ if (context != null)
+ return root.childNodes();
+ else
+ return doc.childNodes();
+ }
+
+ @Override
+ protected boolean process(Token token) {
+ currentToken = token;
+ return this.state.process(token, this);
+ }
+
+ boolean process(Token token, HtmlTreeBuilderState state) {
+ currentToken = token;
+ return state.process(token, this);
+ }
+
+ void transition(HtmlTreeBuilderState state) {
+ this.state = state;
+ }
+
+ HtmlTreeBuilderState state() {
+ return state;
+ }
+
+ void markInsertionMode() {
+ originalState = state;
+ }
+
+ HtmlTreeBuilderState originalState() {
+ return originalState;
+ }
+
+ void framesetOk(boolean framesetOk) {
+ this.framesetOk = framesetOk;
+ }
+
+ boolean framesetOk() {
+ return framesetOk;
+ }
+
+ Document getDocument() {
+ return doc;
+ }
+
+ String getBaseUri() {
+ return baseUri;
+ }
+
+ void maybeSetBaseUri(Element base) {
+ if (baseUriSetFromDoc) // only listen to the first <base href> in parse
+ return;
+
+ String href = base.absUrl("href");
+ if (href.length() != 0) { // ignore <base target> etc
+ baseUri = href;
+ baseUriSetFromDoc = true;
+ doc.setBaseUri(href); // set on the doc so doc.createElement(Tag) will get updated base, and to update all descendants
+ }
+ }
+
+ boolean isFragmentParsing() {
+ return fragmentParsing;
+ }
+
+ void error(HtmlTreeBuilderState state) {
+ if (errors.canAddError())
+ errors.add(new ParseError(reader.pos(), "Unexpected token [%s] when in state [%s]", currentToken.tokenType(), state));
+ }
+
+ Element insert(Token.StartTag startTag) {
+ // handle empty unknown tags
+ // when the spec expects an empty tag, will directly hit insertEmpty, so won't generate fake end tag.
+ if (startTag.isSelfClosing() && !Tag.isKnownTag(startTag.name())) {
+ Element el = insertEmpty(startTag);
+ process(new Token.EndTag(el.tagName())); // ensure we get out of whatever state we are in
+ return el;
+ }
+
+ Element el = new Element(Tag.valueOf(startTag.name()), baseUri, startTag.attributes);
+ insert(el);
+ return el;
+ }
+
+ Element insert(String startTagName) {
+ Element el = new Element(Tag.valueOf(startTagName), baseUri);
+ insert(el);
+ return el;
+ }
+
+ void insert(Element el) {
+ insertNode(el);
+ stack.add(el);
+ }
+
+ Element insertEmpty(Token.StartTag startTag) {
+ Tag tag = Tag.valueOf(startTag.name());
+ Element el = new Element(tag, baseUri, startTag.attributes);
+ insertNode(el);
+ if (startTag.isSelfClosing()) {
+ tokeniser.acknowledgeSelfClosingFlag();
+ if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output
+ tag.setSelfClosing();
+ }
+ return el;
+ }
+
+ void insert(Token.Comment commentToken) {
+ Comment comment = new Comment(commentToken.getData(), baseUri);
+ insertNode(comment);
+ }
+
+ void insert(Token.Character characterToken) {
+ Node node;
+ // characters in script and style go in as datanodes, not text nodes
+ if (StringUtil.in(currentElement().tagName(), "script", "style"))
+ node = new DataNode(characterToken.getData(), baseUri);
+ else
+ node = new TextNode(characterToken.getData(), baseUri);
+ currentElement().appendChild(node); // doesn't use insertNode, because we don't foster these; and will always have a stack.
+ }
+
+ private void insertNode(Node node) {
+ // if the stack hasn't been set up yet, elements (doctype, comments) go into the doc
+ if (stack.size() == 0)
+ doc.appendChild(node);
+ else if (isFosterInserts())
+ insertInFosterParent(node);
+ else
+ currentElement().appendChild(node);
+ }
+
+ Element pop() {
+ // todo - dev, remove validation check
+ if (stack.peekLast().nodeName().equals("td") && !state.name().equals("InCell"))
+ Validate.isFalse(true, "pop td not in cell");
+ if (stack.peekLast().nodeName().equals("html"))
+ Validate.isFalse(true, "popping html!");
+ return stack.pollLast();
+ }
+
+ void push(Element element) {
+ stack.add(element);
+ }
+
+ DescendableLinkedList<Element> getStack() {
+ return stack;
+ }
+
+ boolean onStack(Element el) {
+ return isElementInQueue(stack, el);
+ }
+
+ private boolean isElementInQueue(DescendableLinkedList<Element> queue, Element element) {
+ Iterator<Element> it = queue.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == element) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Element getFromStack(String elName) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next.nodeName().equals(elName)) {
+ return next;
+ }
+ }
+ return null;
+ }
+
+ boolean removeFromStack(Element el) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == el) {
+ it.remove();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void popStackToClose(String elName) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next.nodeName().equals(elName)) {
+ it.remove();
+ break;
+ } else {
+ it.remove();
+ }
+ }
+ }
+
+ void popStackToClose(String... elNames) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (StringUtil.in(next.nodeName(), elNames)) {
+ it.remove();
+ break;
+ } else {
+ it.remove();
+ }
+ }
+ }
+
+ void popStackToBefore(String elName) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next.nodeName().equals(elName)) {
+ break;
+ } else {
+ it.remove();
+ }
+ }
+ }
+
+ void clearStackToTableContext() {
+ clearStackToContext("table");
+ }
+
+ void clearStackToTableBodyContext() {
+ clearStackToContext("tbody", "tfoot", "thead");
+ }
+
+ void clearStackToTableRowContext() {
+ clearStackToContext("tr");
+ }
+
+ private void clearStackToContext(String... nodeNames) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (StringUtil.in(next.nodeName(), nodeNames) || next.nodeName().equals("html"))
+ break;
+ else
+ it.remove();
+ }
+ }
+
+ Element aboveOnStack(Element el) {
+ assert onStack(el);
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == el) {
+ return it.next();
+ }
+ }
+ return null;
+ }
+
+ void insertOnStackAfter(Element after, Element in) {
+ int i = stack.lastIndexOf(after);
+ Validate.isTrue(i != -1);
+ stack.add(i+1, in);
+ }
+
+ void replaceOnStack(Element out, Element in) {
+ replaceInQueue(stack, out, in);
+ }
+
+ private void replaceInQueue(LinkedList<Element> queue, Element out, Element in) {
+ int i = queue.lastIndexOf(out);
+ Validate.isTrue(i != -1);
+ queue.remove(i);
+ queue.add(i, in);
+ }
+
+ void resetInsertionMode() {
+ boolean last = false;
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element node = it.next();
+ if (!it.hasNext()) {
+ last = true;
+ node = contextElement;
+ }
+ String name = node.nodeName();
+ if ("select".equals(name)) {
+ transition(HtmlTreeBuilderState.InSelect);
+ break; // frag
+ } else if (("td".equals(name) || "td".equals(name) && !last)) {
+ transition(HtmlTreeBuilderState.InCell);
+ break;
+ } else if ("tr".equals(name)) {
+ transition(HtmlTreeBuilderState.InRow);
+ break;
+ } else if ("tbody".equals(name) || "thead".equals(name) || "tfoot".equals(name)) {
+ transition(HtmlTreeBuilderState.InTableBody);
+ break;
+ } else if ("caption".equals(name)) {
+ transition(HtmlTreeBuilderState.InCaption);
+ break;
+ } else if ("colgroup".equals(name)) {
+ transition(HtmlTreeBuilderState.InColumnGroup);
+ break; // frag
+ } else if ("table".equals(name)) {
+ transition(HtmlTreeBuilderState.InTable);
+ break;
+ } else if ("head".equals(name)) {
+ transition(HtmlTreeBuilderState.InBody);
+ break; // frag
+ } else if ("body".equals(name)) {
+ transition(HtmlTreeBuilderState.InBody);
+ break;
+ } else if ("frameset".equals(name)) {
+ transition(HtmlTreeBuilderState.InFrameset);
+ break; // frag
+ } else if ("html".equals(name)) {
+ transition(HtmlTreeBuilderState.BeforeHead);
+ break; // frag
+ } else if (last) {
+ transition(HtmlTreeBuilderState.InBody);
+ break; // frag
+ }
+ }
+ }
+
+ // todo: tidy up in specific scope methods
+ private boolean inSpecificScope(String targetName, String[] baseTypes, String[] extraTypes) {
+ return inSpecificScope(new String[]{targetName}, baseTypes, extraTypes);
+ }
+
+ private boolean inSpecificScope(String[] targetNames, String[] baseTypes, String[] extraTypes) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element el = it.next();
+ String elName = el.nodeName();
+ if (StringUtil.in(elName, targetNames))
+ return true;
+ if (StringUtil.in(elName, baseTypes))
+ return false;
+ if (extraTypes != null && StringUtil.in(elName, extraTypes))
+ return false;
+ }
+ Validate.fail("Should not be reachable");
+ return false;
+ }
+
+ boolean inScope(String[] targetNames) {
+ return inSpecificScope(targetNames, new String[]{"applet", "caption", "html", "table", "td", "th", "marquee", "object"}, null);
+ }
+
+ boolean inScope(String targetName) {
+ return inScope(targetName, null);
+ }
+
+ boolean inScope(String targetName, String[] extras) {
+ return inSpecificScope(targetName, new String[]{"applet", "caption", "html", "table", "td", "th", "marquee", "object"}, extras);
+ // todo: in mathml namespace: mi, mo, mn, ms, mtext annotation-xml
+ // todo: in svg namespace: forignOjbect, desc, title
+ }
+
+ boolean inListItemScope(String targetName) {
+ return inScope(targetName, new String[]{"ol", "ul"});
+ }
+
+ boolean inButtonScope(String targetName) {
+ return inScope(targetName, new String[]{"button"});
+ }
+
+ boolean inTableScope(String targetName) {
+ return inSpecificScope(targetName, new String[]{"html", "table"}, null);
+ }
+
+ boolean inSelectScope(String targetName) {
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element el = it.next();
+ String elName = el.nodeName();
+ if (elName.equals(targetName))
+ return true;
+ if (!StringUtil.in(elName, "optgroup", "option")) // all elements except
+ return false;
+ }
+ Validate.fail("Should not be reachable");
+ return false;
+ }
+
+ void setHeadElement(Element headElement) {
+ this.headElement = headElement;
+ }
+
+ Element getHeadElement() {
+ return headElement;
+ }
+
+ boolean isFosterInserts() {
+ return fosterInserts;
+ }
+
+ void setFosterInserts(boolean fosterInserts) {
+ this.fosterInserts = fosterInserts;
+ }
+
+ Element getFormElement() {
+ return formElement;
+ }
+
+ void setFormElement(Element formElement) {
+ this.formElement = formElement;
+ }
+
+ void newPendingTableCharacters() {
+ pendingTableCharacters = new ArrayList<Token.Character>();
+ }
+
+ List<Token.Character> getPendingTableCharacters() {
+ return pendingTableCharacters;
+ }
+
+ void setPendingTableCharacters(List<Token.Character> pendingTableCharacters) {
+ this.pendingTableCharacters = pendingTableCharacters;
+ }
+
+ /**
+ 11.2.5.2 Closing elements that have implied end tags<p/>
+ When the steps below require the UA to generate implied end tags, then, while the current node is a dd element, a
+ dt element, an li element, an option element, an optgroup element, a p element, an rp element, or an rt element,
+ the UA must pop the current node off the stack of open elements.
+
+ @param excludeTag If a step requires the UA to generate implied end tags but lists an element to exclude from the
+ process, then the UA must perform the above steps as if that element was not in the above list.
+ */
+ void generateImpliedEndTags(String excludeTag) {
+ while ((excludeTag != null && !currentElement().nodeName().equals(excludeTag)) &&
+ StringUtil.in(currentElement().nodeName(), "dd", "dt", "li", "option", "optgroup", "p", "rp", "rt"))
+ pop();
+ }
+
+ void generateImpliedEndTags() {
+ generateImpliedEndTags(null);
+ }
+
+ boolean isSpecial(Element el) {
+ // todo: mathml's mi, mo, mn
+ // todo: svg's foreigObject, desc, title
+ String name = el.nodeName();
+ return StringUtil.in(name, "address", "applet", "area", "article", "aside", "base", "basefont", "bgsound",
+ "blockquote", "body", "br", "button", "caption", "center", "col", "colgroup", "command", "dd",
+ "details", "dir", "div", "dl", "dt", "embed", "fieldset", "figcaption", "figure", "footer", "form",
+ "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html",
+ "iframe", "img", "input", "isindex", "li", "link", "listing", "marquee", "menu", "meta", "nav",
+ "noembed", "noframes", "noscript", "object", "ol", "p", "param", "plaintext", "pre", "script",
+ "section", "select", "style", "summary", "table", "tbody", "td", "textarea", "tfoot", "th", "thead",
+ "title", "tr", "ul", "wbr", "xmp");
+ }
+
+ // active formatting elements
+ void pushActiveFormattingElements(Element in) {
+ int numSeen = 0;
+ Iterator<Element> iter = formattingElements.descendingIterator();
+ while (iter.hasNext()) {
+ Element el = iter.next();
+ if (el == null) // marker
+ break;
+
+ if (isSameFormattingElement(in, el))
+ numSeen++;
+
+ if (numSeen == 3) {
+ iter.remove();
+ break;
+ }
+ }
+ formattingElements.add(in);
+ }
+
+ private boolean isSameFormattingElement(Element a, Element b) {
+ // same if: same namespace, tag, and attributes. Element.equals only checks tag, might in future check children
+ return a.nodeName().equals(b.nodeName()) &&
+ // a.namespace().equals(b.namespace()) &&
+ a.attributes().equals(b.attributes());
+ // todo: namespaces
+ }
+
+ void reconstructFormattingElements() {
+ int size = formattingElements.size();
+ if (size == 0 || formattingElements.getLast() == null || onStack(formattingElements.getLast()))
+ return;
+
+ Element entry = formattingElements.getLast();
+ int pos = size - 1;
+ boolean skip = false;
+ while (true) {
+ if (pos == 0) { // step 4. if none before, skip to 8
+ skip = true;
+ break;
+ }
+ entry = formattingElements.get(--pos); // step 5. one earlier than entry
+ if (entry == null || onStack(entry)) // step 6 - neither marker nor on stack
+ break; // jump to 8, else continue back to 4
+ }
+ while(true) {
+ if (!skip) // step 7: on later than entry
+ entry = formattingElements.get(++pos);
+ Validate.notNull(entry); // should not occur, as we break at last element
+
+ // 8. create new element from element, 9 insert into current node, onto stack
+ skip = false; // can only skip increment from 4.
+ Element newEl = insert(entry.nodeName()); // todo: avoid fostering here?
+ // newEl.namespace(entry.namespace()); // todo: namespaces
+ newEl.attributes().addAll(entry.attributes());
+
+ // 10. replace entry with new entry
+ formattingElements.add(pos, newEl);
+ formattingElements.remove(pos + 1);
+
+ // 11
+ if (pos == size-1) // if not last entry in list, jump to 7
+ break;
+ }
+ }
+
+ void clearFormattingElementsToLastMarker() {
+ while (!formattingElements.isEmpty()) {
+ Element el = formattingElements.peekLast();
+ formattingElements.removeLast();
+ if (el == null)
+ break;
+ }
+ }
+
+ void removeFromActiveFormattingElements(Element el) {
+ Iterator<Element> it = formattingElements.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == el) {
+ it.remove();
+ break;
+ }
+ }
+ }
+
+ boolean isInActiveFormattingElements(Element el) {
+ return isElementInQueue(formattingElements, el);
+ }
+
+ Element getActiveFormattingElement(String nodeName) {
+ Iterator<Element> it = formattingElements.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == null) // scope marker
+ break;
+ else if (next.nodeName().equals(nodeName))
+ return next;
+ }
+ return null;
+ }
+
+ void replaceActiveFormattingElement(Element out, Element in) {
+ replaceInQueue(formattingElements, out, in);
+ }
+
+ void insertMarkerToFormattingElements() {
+ formattingElements.add(null);
+ }
+
+ void insertInFosterParent(Node in) {
+ Element fosterParent = null;
+ Element lastTable = getFromStack("table");
+ boolean isLastTableParent = false;
+ if (lastTable != null) {
+ if (lastTable.parent() != null) {
+ fosterParent = lastTable.parent();
+ isLastTableParent = true;
+ } else
+ fosterParent = aboveOnStack(lastTable);
+ } else { // no table == frag
+ fosterParent = stack.get(0);
+ }
+
+ if (isLastTableParent) {
+ Validate.notNull(lastTable); // last table cannot be null by this point.
+ lastTable.before(in);
+ }
+ else
+ fosterParent.appendChild(in);
+ }
+
+ @Override
+ public String toString() {
+ return "TreeBuilder{" +
+ "currentToken=" + currentToken +
+ ", state=" + state +
+ ", currentElement=" + currentElement() +
+ '}';
+ }
+}
diff --git a/server/src/org/jsoup/parser/HtmlTreeBuilderState.java b/server/src/org/jsoup/parser/HtmlTreeBuilderState.java
new file mode 100644
index 0000000000..ceab9faa5a
--- /dev/null
+++ b/server/src/org/jsoup/parser/HtmlTreeBuilderState.java
@@ -0,0 +1,1482 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.DescendableLinkedList;
+import org.jsoup.helper.StringUtil;
+import org.jsoup.nodes.*;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * The Tree Builder's current state. Each state embodies the processing for the state, and transitions to other states.
+ */
+enum HtmlTreeBuilderState {
+ Initial {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ return true; // ignore whitespace
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype()) {
+ // todo: parse error check on expected doctypes
+ // todo: quirk state check on doctype ids
+ Token.Doctype d = t.asDoctype();
+ DocumentType doctype = new DocumentType(d.getName(), d.getPublicIdentifier(), d.getSystemIdentifier(), tb.getBaseUri());
+ tb.getDocument().appendChild(doctype);
+ if (d.isForceQuirks())
+ tb.getDocument().quirksMode(Document.QuirksMode.quirks);
+ tb.transition(BeforeHtml);
+ } else {
+ // todo: check not iframe srcdoc
+ tb.transition(BeforeHtml);
+ return tb.process(t); // re-process token
+ }
+ return true;
+ }
+ },
+ BeforeHtml {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (isWhitespace(t)) {
+ return true; // ignore whitespace
+ } else if (t.isStartTag() && t.asStartTag().name().equals("html")) {
+ tb.insert(t.asStartTag());
+ tb.transition(BeforeHead);
+ } else if (t.isEndTag() && (StringUtil.in(t.asEndTag().name(), "head", "body", "html", "br"))) {
+ return anythingElse(t, tb);
+ } else if (t.isEndTag()) {
+ tb.error(this);
+ return false;
+ } else {
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ tb.insert("html");
+ tb.transition(BeforeHead);
+ return tb.process(t);
+ }
+ },
+ BeforeHead {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ return true;
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isStartTag() && t.asStartTag().name().equals("html")) {
+ return InBody.process(t, tb); // does not transition
+ } else if (t.isStartTag() && t.asStartTag().name().equals("head")) {
+ Element head = tb.insert(t.asStartTag());
+ tb.setHeadElement(head);
+ tb.transition(InHead);
+ } else if (t.isEndTag() && (StringUtil.in(t.asEndTag().name(), "head", "body", "html", "br"))) {
+ tb.process(new Token.StartTag("head"));
+ return tb.process(t);
+ } else if (t.isEndTag()) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.process(new Token.StartTag("head"));
+ return tb.process(t);
+ }
+ return true;
+ }
+ },
+ InHead {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ tb.insert(t.asCharacter());
+ return true;
+ }
+ switch (t.type) {
+ case Comment:
+ tb.insert(t.asComment());
+ break;
+ case Doctype:
+ tb.error(this);
+ return false;
+ case StartTag:
+ Token.StartTag start = t.asStartTag();
+ String name = start.name();
+ if (name.equals("html")) {
+ return InBody.process(t, tb);
+ } else if (StringUtil.in(name, "base", "basefont", "bgsound", "command", "link")) {
+ Element el = tb.insertEmpty(start);
+ // jsoup special: update base the frist time it is seen
+ if (name.equals("base") && el.hasAttr("href"))
+ tb.maybeSetBaseUri(el);
+ } else if (name.equals("meta")) {
+ Element meta = tb.insertEmpty(start);
+ // todo: charset switches
+ } else if (name.equals("title")) {
+ handleRcData(start, tb);
+ } else if (StringUtil.in(name, "noframes", "style")) {
+ handleRawtext(start, tb);
+ } else if (name.equals("noscript")) {
+ // else if noscript && scripting flag = true: rawtext (jsoup doesn't run script, to handle as noscript)
+ tb.insert(start);
+ tb.transition(InHeadNoscript);
+ } else if (name.equals("script")) {
+ // skips some script rules as won't execute them
+ tb.insert(start);
+ tb.tokeniser.transition(TokeniserState.ScriptData);
+ tb.markInsertionMode();
+ tb.transition(Text);
+ } else if (name.equals("head")) {
+ tb.error(this);
+ return false;
+ } else {
+ return anythingElse(t, tb);
+ }
+ break;
+ case EndTag:
+ Token.EndTag end = t.asEndTag();
+ name = end.name();
+ if (name.equals("head")) {
+ tb.pop();
+ tb.transition(AfterHead);
+ } else if (StringUtil.in(name, "body", "html", "br")) {
+ return anythingElse(t, tb);
+ } else {
+ tb.error(this);
+ return false;
+ }
+ break;
+ default:
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, TreeBuilder tb) {
+ tb.process(new Token.EndTag("head"));
+ return tb.process(t);
+ }
+ },
+ InHeadNoscript {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isDoctype()) {
+ tb.error(this);
+ } else if (t.isStartTag() && t.asStartTag().name().equals("html")) {
+ return tb.process(t, InBody);
+ } else if (t.isEndTag() && t.asEndTag().name().equals("noscript")) {
+ tb.pop();
+ tb.transition(InHead);
+ } else if (isWhitespace(t) || t.isComment() || (t.isStartTag() && StringUtil.in(t.asStartTag().name(),
+ "basefont", "bgsound", "link", "meta", "noframes", "style"))) {
+ return tb.process(t, InHead);
+ } else if (t.isEndTag() && t.asEndTag().name().equals("br")) {
+ return anythingElse(t, tb);
+ } else if ((t.isStartTag() && StringUtil.in(t.asStartTag().name(), "head", "noscript")) || t.isEndTag()) {
+ tb.error(this);
+ return false;
+ } else {
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ tb.error(this);
+ tb.process(new Token.EndTag("noscript"));
+ return tb.process(t);
+ }
+ },
+ AfterHead {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ tb.insert(t.asCharacter());
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ } else if (t.isStartTag()) {
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+ if (name.equals("html")) {
+ return tb.process(t, InBody);
+ } else if (name.equals("body")) {
+ tb.insert(startTag);
+ tb.framesetOk(false);
+ tb.transition(InBody);
+ } else if (name.equals("frameset")) {
+ tb.insert(startTag);
+ tb.transition(InFrameset);
+ } else if (StringUtil.in(name, "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title")) {
+ tb.error(this);
+ Element head = tb.getHeadElement();
+ tb.push(head);
+ tb.process(t, InHead);
+ tb.removeFromStack(head);
+ } else if (name.equals("head")) {
+ tb.error(this);
+ return false;
+ } else {
+ anythingElse(t, tb);
+ }
+ } else if (t.isEndTag()) {
+ if (StringUtil.in(t.asEndTag().name(), "body", "html")) {
+ anythingElse(t, tb);
+ } else {
+ tb.error(this);
+ return false;
+ }
+ } else {
+ anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ tb.process(new Token.StartTag("body"));
+ tb.framesetOk(true);
+ return tb.process(t);
+ }
+ },
+ InBody {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ switch (t.type) {
+ case Character: {
+ Token.Character c = t.asCharacter();
+ if (c.getData().equals(nullString)) {
+ // todo confirm that check
+ tb.error(this);
+ return false;
+ } else if (isWhitespace(c)) {
+ tb.reconstructFormattingElements();
+ tb.insert(c);
+ } else {
+ tb.reconstructFormattingElements();
+ tb.insert(c);
+ tb.framesetOk(false);
+ }
+ break;
+ }
+ case Comment: {
+ tb.insert(t.asComment());
+ break;
+ }
+ case Doctype: {
+ tb.error(this);
+ return false;
+ }
+ case StartTag:
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+ if (name.equals("html")) {
+ tb.error(this);
+ // merge attributes onto real html
+ Element html = tb.getStack().getFirst();
+ for (Attribute attribute : startTag.getAttributes()) {
+ if (!html.hasAttr(attribute.getKey()))
+ html.attributes().put(attribute);
+ }
+ } else if (StringUtil.in(name, "base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "title")) {
+ return tb.process(t, InHead);
+ } else if (name.equals("body")) {
+ tb.error(this);
+ LinkedList<Element> stack = tb.getStack();
+ if (stack.size() == 1 || (stack.size() > 2 && !stack.get(1).nodeName().equals("body"))) {
+ // only in fragment case
+ return false; // ignore
+ } else {
+ tb.framesetOk(false);
+ Element body = stack.get(1);
+ for (Attribute attribute : startTag.getAttributes()) {
+ if (!body.hasAttr(attribute.getKey()))
+ body.attributes().put(attribute);
+ }
+ }
+ } else if (name.equals("frameset")) {
+ tb.error(this);
+ LinkedList<Element> stack = tb.getStack();
+ if (stack.size() == 1 || (stack.size() > 2 && !stack.get(1).nodeName().equals("body"))) {
+ // only in fragment case
+ return false; // ignore
+ } else if (!tb.framesetOk()) {
+ return false; // ignore frameset
+ } else {
+ Element second = stack.get(1);
+ if (second.parent() != null)
+ second.remove();
+ // pop up to html element
+ while (stack.size() > 1)
+ stack.removeLast();
+ tb.insert(startTag);
+ tb.transition(InFrameset);
+ }
+ } else if (StringUtil.in(name,
+ "address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl",
+ "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol",
+ "p", "section", "summary", "ul")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ } else if (StringUtil.in(name, "h1", "h2", "h3", "h4", "h5", "h6")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ if (StringUtil.in(tb.currentElement().nodeName(), "h1", "h2", "h3", "h4", "h5", "h6")) {
+ tb.error(this);
+ tb.pop();
+ }
+ tb.insert(startTag);
+ } else if (StringUtil.in(name, "pre", "listing")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ // todo: ignore LF if next token
+ tb.framesetOk(false);
+ } else if (name.equals("form")) {
+ if (tb.getFormElement() != null) {
+ tb.error(this);
+ return false;
+ }
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ Element form = tb.insert(startTag);
+ tb.setFormElement(form);
+ } else if (name.equals("li")) {
+ tb.framesetOk(false);
+ LinkedList<Element> stack = tb.getStack();
+ for (int i = stack.size() - 1; i > 0; i--) {
+ Element el = stack.get(i);
+ if (el.nodeName().equals("li")) {
+ tb.process(new Token.EndTag("li"));
+ break;
+ }
+ if (tb.isSpecial(el) && !StringUtil.in(el.nodeName(), "address", "div", "p"))
+ break;
+ }
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ } else if (StringUtil.in(name, "dd", "dt")) {
+ tb.framesetOk(false);
+ LinkedList<Element> stack = tb.getStack();
+ for (int i = stack.size() - 1; i > 0; i--) {
+ Element el = stack.get(i);
+ if (StringUtil.in(el.nodeName(), "dd", "dt")) {
+ tb.process(new Token.EndTag(el.nodeName()));
+ break;
+ }
+ if (tb.isSpecial(el) && !StringUtil.in(el.nodeName(), "address", "div", "p"))
+ break;
+ }
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ } else if (name.equals("plaintext")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ tb.tokeniser.transition(TokeniserState.PLAINTEXT); // once in, never gets out
+ } else if (name.equals("button")) {
+ if (tb.inButtonScope("button")) {
+ // close and reprocess
+ tb.error(this);
+ tb.process(new Token.EndTag("button"));
+ tb.process(startTag);
+ } else {
+ tb.reconstructFormattingElements();
+ tb.insert(startTag);
+ tb.framesetOk(false);
+ }
+ } else if (name.equals("a")) {
+ if (tb.getActiveFormattingElement("a") != null) {
+ tb.error(this);
+ tb.process(new Token.EndTag("a"));
+
+ // still on stack?
+ Element remainingA = tb.getFromStack("a");
+ if (remainingA != null) {
+ tb.removeFromActiveFormattingElements(remainingA);
+ tb.removeFromStack(remainingA);
+ }
+ }
+ tb.reconstructFormattingElements();
+ Element a = tb.insert(startTag);
+ tb.pushActiveFormattingElements(a);
+ } else if (StringUtil.in(name,
+ "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u")) {
+ tb.reconstructFormattingElements();
+ Element el = tb.insert(startTag);
+ tb.pushActiveFormattingElements(el);
+ } else if (name.equals("nobr")) {
+ tb.reconstructFormattingElements();
+ if (tb.inScope("nobr")) {
+ tb.error(this);
+ tb.process(new Token.EndTag("nobr"));
+ tb.reconstructFormattingElements();
+ }
+ Element el = tb.insert(startTag);
+ tb.pushActiveFormattingElements(el);
+ } else if (StringUtil.in(name, "applet", "marquee", "object")) {
+ tb.reconstructFormattingElements();
+ tb.insert(startTag);
+ tb.insertMarkerToFormattingElements();
+ tb.framesetOk(false);
+ } else if (name.equals("table")) {
+ if (tb.getDocument().quirksMode() != Document.QuirksMode.quirks && tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insert(startTag);
+ tb.framesetOk(false);
+ tb.transition(InTable);
+ } else if (StringUtil.in(name, "area", "br", "embed", "img", "keygen", "wbr")) {
+ tb.reconstructFormattingElements();
+ tb.insertEmpty(startTag);
+ tb.framesetOk(false);
+ } else if (name.equals("input")) {
+ tb.reconstructFormattingElements();
+ Element el = tb.insertEmpty(startTag);
+ if (!el.attr("type").equalsIgnoreCase("hidden"))
+ tb.framesetOk(false);
+ } else if (StringUtil.in(name, "param", "source", "track")) {
+ tb.insertEmpty(startTag);
+ } else if (name.equals("hr")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.insertEmpty(startTag);
+ tb.framesetOk(false);
+ } else if (name.equals("image")) {
+ // we're not supposed to ask.
+ startTag.name("img");
+ return tb.process(startTag);
+ } else if (name.equals("isindex")) {
+ // how much do we care about the early 90s?
+ tb.error(this);
+ if (tb.getFormElement() != null)
+ return false;
+
+ tb.tokeniser.acknowledgeSelfClosingFlag();
+ tb.process(new Token.StartTag("form"));
+ if (startTag.attributes.hasKey("action")) {
+ Element form = tb.getFormElement();
+ form.attr("action", startTag.attributes.get("action"));
+ }
+ tb.process(new Token.StartTag("hr"));
+ tb.process(new Token.StartTag("label"));
+ // hope you like english.
+ String prompt = startTag.attributes.hasKey("prompt") ?
+ startTag.attributes.get("prompt") :
+ "This is a searchable index. Enter search keywords: ";
+
+ tb.process(new Token.Character(prompt));
+
+ // input
+ Attributes inputAttribs = new Attributes();
+ for (Attribute attr : startTag.attributes) {
+ if (!StringUtil.in(attr.getKey(), "name", "action", "prompt"))
+ inputAttribs.put(attr);
+ }
+ inputAttribs.put("name", "isindex");
+ tb.process(new Token.StartTag("input", inputAttribs));
+ tb.process(new Token.EndTag("label"));
+ tb.process(new Token.StartTag("hr"));
+ tb.process(new Token.EndTag("form"));
+ } else if (name.equals("textarea")) {
+ tb.insert(startTag);
+ // todo: If the next token is a U+000A LINE FEED (LF) character token, then ignore that token and move on to the next one. (Newlines at the start of textarea elements are ignored as an authoring convenience.)
+ tb.tokeniser.transition(TokeniserState.Rcdata);
+ tb.markInsertionMode();
+ tb.framesetOk(false);
+ tb.transition(Text);
+ } else if (name.equals("xmp")) {
+ if (tb.inButtonScope("p")) {
+ tb.process(new Token.EndTag("p"));
+ }
+ tb.reconstructFormattingElements();
+ tb.framesetOk(false);
+ handleRawtext(startTag, tb);
+ } else if (name.equals("iframe")) {
+ tb.framesetOk(false);
+ handleRawtext(startTag, tb);
+ } else if (name.equals("noembed")) {
+ // also handle noscript if script enabled
+ handleRawtext(startTag, tb);
+ } else if (name.equals("select")) {
+ tb.reconstructFormattingElements();
+ tb.insert(startTag);
+ tb.framesetOk(false);
+
+ HtmlTreeBuilderState state = tb.state();
+ if (state.equals(InTable) || state.equals(InCaption) || state.equals(InTableBody) || state.equals(InRow) || state.equals(InCell))
+ tb.transition(InSelectInTable);
+ else
+ tb.transition(InSelect);
+ } else if (StringUtil.in("optgroup", "option")) {
+ if (tb.currentElement().nodeName().equals("option"))
+ tb.process(new Token.EndTag("option"));
+ tb.reconstructFormattingElements();
+ tb.insert(startTag);
+ } else if (StringUtil.in("rp", "rt")) {
+ if (tb.inScope("ruby")) {
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals("ruby")) {
+ tb.error(this);
+ tb.popStackToBefore("ruby"); // i.e. close up to but not include name
+ }
+ tb.insert(startTag);
+ }
+ } else if (name.equals("math")) {
+ tb.reconstructFormattingElements();
+ // todo: handle A start tag whose tag name is "math" (i.e. foreign, mathml)
+ tb.insert(startTag);
+ tb.tokeniser.acknowledgeSelfClosingFlag();
+ } else if (name.equals("svg")) {
+ tb.reconstructFormattingElements();
+ // todo: handle A start tag whose tag name is "svg" (xlink, svg)
+ tb.insert(startTag);
+ tb.tokeniser.acknowledgeSelfClosingFlag();
+ } else if (StringUtil.in(name,
+ "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr")) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.reconstructFormattingElements();
+ tb.insert(startTag);
+ }
+ break;
+
+ case EndTag:
+ Token.EndTag endTag = t.asEndTag();
+ name = endTag.name();
+ if (name.equals("body")) {
+ if (!tb.inScope("body")) {
+ tb.error(this);
+ return false;
+ } else {
+ // todo: error if stack contains something not dd, dt, li, optgroup, option, p, rp, rt, tbody, td, tfoot, th, thead, tr, body, html
+ tb.transition(AfterBody);
+ }
+ } else if (name.equals("html")) {
+ boolean notIgnored = tb.process(new Token.EndTag("body"));
+ if (notIgnored)
+ return tb.process(endTag);
+ } else if (StringUtil.in(name,
+ "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div",
+ "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu",
+ "nav", "ol", "pre", "section", "summary", "ul")) {
+ // todo: refactor these lookups
+ if (!tb.inScope(name)) {
+ // nothing to close
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ }
+ } else if (name.equals("form")) {
+ Element currentForm = tb.getFormElement();
+ tb.setFormElement(null);
+ if (currentForm == null || !tb.inScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ // remove currentForm from stack. will shift anything under up.
+ tb.removeFromStack(currentForm);
+ }
+ } else if (name.equals("p")) {
+ if (!tb.inButtonScope(name)) {
+ tb.error(this);
+ tb.process(new Token.StartTag(name)); // if no p to close, creates an empty <p></p>
+ return tb.process(endTag);
+ } else {
+ tb.generateImpliedEndTags(name);
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ }
+ } else if (name.equals("li")) {
+ if (!tb.inListItemScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags(name);
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ }
+ } else if (StringUtil.in(name, "dd", "dt")) {
+ if (!tb.inScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags(name);
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ }
+ } else if (StringUtil.in(name, "h1", "h2", "h3", "h4", "h5", "h6")) {
+ if (!tb.inScope(new String[]{"h1", "h2", "h3", "h4", "h5", "h6"})) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags(name);
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose("h1", "h2", "h3", "h4", "h5", "h6");
+ }
+ } else if (name.equals("sarcasm")) {
+ // *sigh*
+ return anyOtherEndTag(t, tb);
+ } else if (StringUtil.in(name,
+ "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u")) {
+ // Adoption Agency Algorithm.
+ OUTER:
+ for (int i = 0; i < 8; i++) {
+ Element formatEl = tb.getActiveFormattingElement(name);
+ if (formatEl == null)
+ return anyOtherEndTag(t, tb);
+ else if (!tb.onStack(formatEl)) {
+ tb.error(this);
+ tb.removeFromActiveFormattingElements(formatEl);
+ return true;
+ } else if (!tb.inScope(formatEl.nodeName())) {
+ tb.error(this);
+ return false;
+ } else if (tb.currentElement() != formatEl)
+ tb.error(this);
+
+ Element furthestBlock = null;
+ Element commonAncestor = null;
+ boolean seenFormattingElement = false;
+ LinkedList<Element> stack = tb.getStack();
+ for (int si = 0; si < stack.size(); si++) {
+ Element el = stack.get(si);
+ if (el == formatEl) {
+ commonAncestor = stack.get(si - 1);
+ seenFormattingElement = true;
+ } else if (seenFormattingElement && tb.isSpecial(el)) {
+ furthestBlock = el;
+ break;
+ }
+ }
+ if (furthestBlock == null) {
+ tb.popStackToClose(formatEl.nodeName());
+ tb.removeFromActiveFormattingElements(formatEl);
+ return true;
+ }
+
+ // todo: Let a bookmark note the position of the formatting element in the list of active formatting elements relative to the elements on either side of it in the list.
+ // does that mean: int pos of format el in list?
+ Element node = furthestBlock;
+ Element lastNode = furthestBlock;
+ INNER:
+ for (int j = 0; j < 3; j++) {
+ if (tb.onStack(node))
+ node = tb.aboveOnStack(node);
+ if (!tb.isInActiveFormattingElements(node)) { // note no bookmark check
+ tb.removeFromStack(node);
+ continue INNER;
+ } else if (node == formatEl)
+ break INNER;
+
+ Element replacement = new Element(Tag.valueOf(node.nodeName()), tb.getBaseUri());
+ tb.replaceActiveFormattingElement(node, replacement);
+ tb.replaceOnStack(node, replacement);
+ node = replacement;
+
+ if (lastNode == furthestBlock) {
+ // todo: move the aforementioned bookmark to be immediately after the new node in the list of active formatting elements.
+ // not getting how this bookmark both straddles the element above, but is inbetween here...
+ }
+ if (lastNode.parent() != null)
+ lastNode.remove();
+ node.appendChild(lastNode);
+
+ lastNode = node;
+ }
+
+ if (StringUtil.in(commonAncestor.nodeName(), "table", "tbody", "tfoot", "thead", "tr")) {
+ if (lastNode.parent() != null)
+ lastNode.remove();
+ tb.insertInFosterParent(lastNode);
+ } else {
+ if (lastNode.parent() != null)
+ lastNode.remove();
+ commonAncestor.appendChild(lastNode);
+ }
+
+ Element adopter = new Element(Tag.valueOf(name), tb.getBaseUri());
+ Node[] childNodes = furthestBlock.childNodes().toArray(new Node[furthestBlock.childNodes().size()]);
+ for (Node childNode : childNodes) {
+ adopter.appendChild(childNode); // append will reparent. thus the clone to avoid concurrent mod.
+ }
+ furthestBlock.appendChild(adopter);
+ tb.removeFromActiveFormattingElements(formatEl);
+ // todo: insert the new element into the list of active formatting elements at the position of the aforementioned bookmark.
+ tb.removeFromStack(formatEl);
+ tb.insertOnStackAfter(furthestBlock, adopter);
+ }
+ } else if (StringUtil.in(name, "applet", "marquee", "object")) {
+ if (!tb.inScope("name")) {
+ if (!tb.inScope(name)) {
+ tb.error(this);
+ return false;
+ }
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ tb.clearFormattingElementsToLastMarker();
+ }
+ } else if (name.equals("br")) {
+ tb.error(this);
+ tb.process(new Token.StartTag("br"));
+ return false;
+ } else {
+ return anyOtherEndTag(t, tb);
+ }
+
+ break;
+ case EOF:
+ // todo: error if stack contains something not dd, dt, li, p, tbody, td, tfoot, th, thead, tr, body, html
+ // stop parsing
+ break;
+ }
+ return true;
+ }
+
+ boolean anyOtherEndTag(Token t, HtmlTreeBuilder tb) {
+ String name = t.asEndTag().name();
+ DescendableLinkedList<Element> stack = tb.getStack();
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element node = it.next();
+ if (node.nodeName().equals(name)) {
+ tb.generateImpliedEndTags(name);
+ if (!name.equals(tb.currentElement().nodeName()))
+ tb.error(this);
+ tb.popStackToClose(name);
+ break;
+ } else {
+ if (tb.isSpecial(node)) {
+ tb.error(this);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ },
+ Text {
+ // in script, style etc. normally treated as data tags
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isCharacter()) {
+ tb.insert(t.asCharacter());
+ } else if (t.isEOF()) {
+ tb.error(this);
+ // if current node is script: already started
+ tb.pop();
+ tb.transition(tb.originalState());
+ return tb.process(t);
+ } else if (t.isEndTag()) {
+ // if: An end tag whose tag name is "script" -- scripting nesting level, if evaluating scripts
+ tb.pop();
+ tb.transition(tb.originalState());
+ }
+ return true;
+ }
+ },
+ InTable {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isCharacter()) {
+ tb.newPendingTableCharacters();
+ tb.markInsertionMode();
+ tb.transition(InTableText);
+ return tb.process(t);
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ return true;
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isStartTag()) {
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+ if (name.equals("caption")) {
+ tb.clearStackToTableContext();
+ tb.insertMarkerToFormattingElements();
+ tb.insert(startTag);
+ tb.transition(InCaption);
+ } else if (name.equals("colgroup")) {
+ tb.clearStackToTableContext();
+ tb.insert(startTag);
+ tb.transition(InColumnGroup);
+ } else if (name.equals("col")) {
+ tb.process(new Token.StartTag("colgroup"));
+ return tb.process(t);
+ } else if (StringUtil.in(name, "tbody", "tfoot", "thead")) {
+ tb.clearStackToTableContext();
+ tb.insert(startTag);
+ tb.transition(InTableBody);
+ } else if (StringUtil.in(name, "td", "th", "tr")) {
+ tb.process(new Token.StartTag("tbody"));
+ return tb.process(t);
+ } else if (name.equals("table")) {
+ tb.error(this);
+ boolean processed = tb.process(new Token.EndTag("table"));
+ if (processed) // only ignored if in fragment
+ return tb.process(t);
+ } else if (StringUtil.in(name, "style", "script")) {
+ return tb.process(t, InHead);
+ } else if (name.equals("input")) {
+ if (!startTag.attributes.get("type").equalsIgnoreCase("hidden")) {
+ return anythingElse(t, tb);
+ } else {
+ tb.insertEmpty(startTag);
+ }
+ } else if (name.equals("form")) {
+ tb.error(this);
+ if (tb.getFormElement() != null)
+ return false;
+ else {
+ Element form = tb.insertEmpty(startTag);
+ tb.setFormElement(form);
+ }
+ } else {
+ return anythingElse(t, tb);
+ }
+ } else if (t.isEndTag()) {
+ Token.EndTag endTag = t.asEndTag();
+ String name = endTag.name();
+
+ if (name.equals("table")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.popStackToClose("table");
+ }
+ tb.resetInsertionMode();
+ } else if (StringUtil.in(name,
+ "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr")) {
+ tb.error(this);
+ return false;
+ } else {
+ return anythingElse(t, tb);
+ }
+ } else if (t.isEOF()) {
+ if (tb.currentElement().nodeName().equals("html"))
+ tb.error(this);
+ return true; // stops parsing
+ }
+ return anythingElse(t, tb);
+ }
+
+ boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ tb.error(this);
+ boolean processed = true;
+ if (StringUtil.in(tb.currentElement().nodeName(), "table", "tbody", "tfoot", "thead", "tr")) {
+ tb.setFosterInserts(true);
+ processed = tb.process(t, InBody);
+ tb.setFosterInserts(false);
+ } else {
+ processed = tb.process(t, InBody);
+ }
+ return processed;
+ }
+ },
+ InTableText {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ switch (t.type) {
+ case Character:
+ Token.Character c = t.asCharacter();
+ if (c.getData().equals(nullString)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.getPendingTableCharacters().add(c);
+ }
+ break;
+ default:
+ if (tb.getPendingTableCharacters().size() > 0) {
+ for (Token.Character character : tb.getPendingTableCharacters()) {
+ if (!isWhitespace(character)) {
+ // InTable anything else section:
+ tb.error(this);
+ if (StringUtil.in(tb.currentElement().nodeName(), "table", "tbody", "tfoot", "thead", "tr")) {
+ tb.setFosterInserts(true);
+ tb.process(character, InBody);
+ tb.setFosterInserts(false);
+ } else {
+ tb.process(character, InBody);
+ }
+ } else
+ tb.insert(character);
+ }
+ tb.newPendingTableCharacters();
+ }
+ tb.transition(tb.originalState());
+ return tb.process(t);
+ }
+ return true;
+ }
+ },
+ InCaption {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isEndTag() && t.asEndTag().name().equals("caption")) {
+ Token.EndTag endTag = t.asEndTag();
+ String name = endTag.name();
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals("caption"))
+ tb.error(this);
+ tb.popStackToClose("caption");
+ tb.clearFormattingElementsToLastMarker();
+ tb.transition(InTable);
+ }
+ } else if ((
+ t.isStartTag() && StringUtil.in(t.asStartTag().name(),
+ "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr") ||
+ t.isEndTag() && t.asEndTag().name().equals("table"))
+ ) {
+ tb.error(this);
+ boolean processed = tb.process(new Token.EndTag("caption"));
+ if (processed)
+ return tb.process(t);
+ } else if (t.isEndTag() && StringUtil.in(t.asEndTag().name(),
+ "body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr")) {
+ tb.error(this);
+ return false;
+ } else {
+ return tb.process(t, InBody);
+ }
+ return true;
+ }
+ },
+ InColumnGroup {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ tb.insert(t.asCharacter());
+ return true;
+ }
+ switch (t.type) {
+ case Comment:
+ tb.insert(t.asComment());
+ break;
+ case Doctype:
+ tb.error(this);
+ break;
+ case StartTag:
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+ if (name.equals("html"))
+ return tb.process(t, InBody);
+ else if (name.equals("col"))
+ tb.insertEmpty(startTag);
+ else
+ return anythingElse(t, tb);
+ break;
+ case EndTag:
+ Token.EndTag endTag = t.asEndTag();
+ name = endTag.name();
+ if (name.equals("colgroup")) {
+ if (tb.currentElement().nodeName().equals("html")) { // frag case
+ tb.error(this);
+ return false;
+ } else {
+ tb.pop();
+ tb.transition(InTable);
+ }
+ } else
+ return anythingElse(t, tb);
+ break;
+ case EOF:
+ if (tb.currentElement().nodeName().equals("html"))
+ return true; // stop parsing; frag case
+ else
+ return anythingElse(t, tb);
+ default:
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, TreeBuilder tb) {
+ boolean processed = tb.process(new Token.EndTag("colgroup"));
+ if (processed) // only ignored in frag case
+ return tb.process(t);
+ return true;
+ }
+ },
+ InTableBody {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ switch (t.type) {
+ case StartTag:
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+ if (name.equals("tr")) {
+ tb.clearStackToTableBodyContext();
+ tb.insert(startTag);
+ tb.transition(InRow);
+ } else if (StringUtil.in(name, "th", "td")) {
+ tb.error(this);
+ tb.process(new Token.StartTag("tr"));
+ return tb.process(startTag);
+ } else if (StringUtil.in(name, "caption", "col", "colgroup", "tbody", "tfoot", "thead")) {
+ return exitTableBody(t, tb);
+ } else
+ return anythingElse(t, tb);
+ break;
+ case EndTag:
+ Token.EndTag endTag = t.asEndTag();
+ name = endTag.name();
+ if (StringUtil.in(name, "tbody", "tfoot", "thead")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.clearStackToTableBodyContext();
+ tb.pop();
+ tb.transition(InTable);
+ }
+ } else if (name.equals("table")) {
+ return exitTableBody(t, tb);
+ } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html", "td", "th", "tr")) {
+ tb.error(this);
+ return false;
+ } else
+ return anythingElse(t, tb);
+ break;
+ default:
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean exitTableBody(Token t, HtmlTreeBuilder tb) {
+ if (!(tb.inTableScope("tbody") || tb.inTableScope("thead") || tb.inScope("tfoot"))) {
+ // frag case
+ tb.error(this);
+ return false;
+ }
+ tb.clearStackToTableBodyContext();
+ tb.process(new Token.EndTag(tb.currentElement().nodeName())); // tbody, tfoot, thead
+ return tb.process(t);
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ return tb.process(t, InTable);
+ }
+ },
+ InRow {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isStartTag()) {
+ Token.StartTag startTag = t.asStartTag();
+ String name = startTag.name();
+
+ if (StringUtil.in(name, "th", "td")) {
+ tb.clearStackToTableRowContext();
+ tb.insert(startTag);
+ tb.transition(InCell);
+ tb.insertMarkerToFormattingElements();
+ } else if (StringUtil.in(name, "caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr")) {
+ return handleMissingTr(t, tb);
+ } else {
+ return anythingElse(t, tb);
+ }
+ } else if (t.isEndTag()) {
+ Token.EndTag endTag = t.asEndTag();
+ String name = endTag.name();
+
+ if (name.equals("tr")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this); // frag
+ return false;
+ }
+ tb.clearStackToTableRowContext();
+ tb.pop(); // tr
+ tb.transition(InTableBody);
+ } else if (name.equals("table")) {
+ return handleMissingTr(t, tb);
+ } else if (StringUtil.in(name, "tbody", "tfoot", "thead")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ return false;
+ }
+ tb.process(new Token.EndTag("tr"));
+ return tb.process(t);
+ } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html", "td", "th")) {
+ tb.error(this);
+ return false;
+ } else {
+ return anythingElse(t, tb);
+ }
+ } else {
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ return tb.process(t, InTable);
+ }
+
+ private boolean handleMissingTr(Token t, TreeBuilder tb) {
+ boolean processed = tb.process(new Token.EndTag("tr"));
+ if (processed)
+ return tb.process(t);
+ else
+ return false;
+ }
+ },
+ InCell {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isEndTag()) {
+ Token.EndTag endTag = t.asEndTag();
+ String name = endTag.name();
+
+ if (StringUtil.in(name, "td", "th")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ tb.transition(InRow); // might not be in scope if empty: <td /> and processing fake end tag
+ return false;
+ }
+ tb.generateImpliedEndTags();
+ if (!tb.currentElement().nodeName().equals(name))
+ tb.error(this);
+ tb.popStackToClose(name);
+ tb.clearFormattingElementsToLastMarker();
+ tb.transition(InRow);
+ } else if (StringUtil.in(name, "body", "caption", "col", "colgroup", "html")) {
+ tb.error(this);
+ return false;
+ } else if (StringUtil.in(name, "table", "tbody", "tfoot", "thead", "tr")) {
+ if (!tb.inTableScope(name)) {
+ tb.error(this);
+ return false;
+ }
+ closeCell(tb);
+ return tb.process(t);
+ } else {
+ return anythingElse(t, tb);
+ }
+ } else if (t.isStartTag() &&
+ StringUtil.in(t.asStartTag().name(),
+ "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr")) {
+ if (!(tb.inTableScope("td") || tb.inTableScope("th"))) {
+ tb.error(this);
+ return false;
+ }
+ closeCell(tb);
+ return tb.process(t);
+ } else {
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ return tb.process(t, InBody);
+ }
+
+ private void closeCell(HtmlTreeBuilder tb) {
+ if (tb.inTableScope("td"))
+ tb.process(new Token.EndTag("td"));
+ else
+ tb.process(new Token.EndTag("th")); // only here if th or td in scope
+ }
+ },
+ InSelect {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ switch (t.type) {
+ case Character:
+ Token.Character c = t.asCharacter();
+ if (c.getData().equals(nullString)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.insert(c);
+ }
+ break;
+ case Comment:
+ tb.insert(t.asComment());
+ break;
+ case Doctype:
+ tb.error(this);
+ return false;
+ case StartTag:
+ Token.StartTag start = t.asStartTag();
+ String name = start.name();
+ if (name.equals("html"))
+ return tb.process(start, InBody);
+ else if (name.equals("option")) {
+ tb.process(new Token.EndTag("option"));
+ tb.insert(start);
+ } else if (name.equals("optgroup")) {
+ if (tb.currentElement().nodeName().equals("option"))
+ tb.process(new Token.EndTag("option"));
+ else if (tb.currentElement().nodeName().equals("optgroup"))
+ tb.process(new Token.EndTag("optgroup"));
+ tb.insert(start);
+ } else if (name.equals("select")) {
+ tb.error(this);
+ return tb.process(new Token.EndTag("select"));
+ } else if (StringUtil.in(name, "input", "keygen", "textarea")) {
+ tb.error(this);
+ if (!tb.inSelectScope("select"))
+ return false; // frag
+ tb.process(new Token.EndTag("select"));
+ return tb.process(start);
+ } else if (name.equals("script")) {
+ return tb.process(t, InHead);
+ } else {
+ return anythingElse(t, tb);
+ }
+ break;
+ case EndTag:
+ Token.EndTag end = t.asEndTag();
+ name = end.name();
+ if (name.equals("optgroup")) {
+ if (tb.currentElement().nodeName().equals("option") && tb.aboveOnStack(tb.currentElement()) != null && tb.aboveOnStack(tb.currentElement()).nodeName().equals("optgroup"))
+ tb.process(new Token.EndTag("option"));
+ if (tb.currentElement().nodeName().equals("optgroup"))
+ tb.pop();
+ else
+ tb.error(this);
+ } else if (name.equals("option")) {
+ if (tb.currentElement().nodeName().equals("option"))
+ tb.pop();
+ else
+ tb.error(this);
+ } else if (name.equals("select")) {
+ if (!tb.inSelectScope(name)) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.popStackToClose(name);
+ tb.resetInsertionMode();
+ }
+ } else
+ return anythingElse(t, tb);
+ break;
+ case EOF:
+ if (!tb.currentElement().nodeName().equals("html"))
+ tb.error(this);
+ break;
+ default:
+ return anythingElse(t, tb);
+ }
+ return true;
+ }
+
+ private boolean anythingElse(Token t, HtmlTreeBuilder tb) {
+ tb.error(this);
+ return false;
+ }
+ },
+ InSelectInTable {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isStartTag() && StringUtil.in(t.asStartTag().name(), "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th")) {
+ tb.error(this);
+ tb.process(new Token.EndTag("select"));
+ return tb.process(t);
+ } else if (t.isEndTag() && StringUtil.in(t.asEndTag().name(), "caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th")) {
+ tb.error(this);
+ if (tb.inTableScope(t.asEndTag().name())) {
+ tb.process(new Token.EndTag("select"));
+ return (tb.process(t));
+ } else
+ return false;
+ } else {
+ return tb.process(t, InSelect);
+ }
+ }
+ },
+ AfterBody {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ return tb.process(t, InBody);
+ } else if (t.isComment()) {
+ tb.insert(t.asComment()); // into html node
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isStartTag() && t.asStartTag().name().equals("html")) {
+ return tb.process(t, InBody);
+ } else if (t.isEndTag() && t.asEndTag().name().equals("html")) {
+ if (tb.isFragmentParsing()) {
+ tb.error(this);
+ return false;
+ } else {
+ tb.transition(AfterAfterBody);
+ }
+ } else if (t.isEOF()) {
+ // chillax! we're done
+ } else {
+ tb.error(this);
+ tb.transition(InBody);
+ return tb.process(t);
+ }
+ return true;
+ }
+ },
+ InFrameset {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ tb.insert(t.asCharacter());
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isStartTag()) {
+ Token.StartTag start = t.asStartTag();
+ String name = start.name();
+ if (name.equals("html")) {
+ return tb.process(start, InBody);
+ } else if (name.equals("frameset")) {
+ tb.insert(start);
+ } else if (name.equals("frame")) {
+ tb.insertEmpty(start);
+ } else if (name.equals("noframes")) {
+ return tb.process(start, InHead);
+ } else {
+ tb.error(this);
+ return false;
+ }
+ } else if (t.isEndTag() && t.asEndTag().name().equals("frameset")) {
+ if (tb.currentElement().nodeName().equals("html")) { // frag
+ tb.error(this);
+ return false;
+ } else {
+ tb.pop();
+ if (!tb.isFragmentParsing() && !tb.currentElement().nodeName().equals("frameset")) {
+ tb.transition(AfterFrameset);
+ }
+ }
+ } else if (t.isEOF()) {
+ if (!tb.currentElement().nodeName().equals("html")) {
+ tb.error(this);
+ return true;
+ }
+ } else {
+ tb.error(this);
+ return false;
+ }
+ return true;
+ }
+ },
+ AfterFrameset {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (isWhitespace(t)) {
+ tb.insert(t.asCharacter());
+ } else if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype()) {
+ tb.error(this);
+ return false;
+ } else if (t.isStartTag() && t.asStartTag().name().equals("html")) {
+ return tb.process(t, InBody);
+ } else if (t.isEndTag() && t.asEndTag().name().equals("html")) {
+ tb.transition(AfterAfterFrameset);
+ } else if (t.isStartTag() && t.asStartTag().name().equals("noframes")) {
+ return tb.process(t, InHead);
+ } else if (t.isEOF()) {
+ // cool your heels, we're complete
+ } else {
+ tb.error(this);
+ return false;
+ }
+ return true;
+ }
+ },
+ AfterAfterBody {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype() || isWhitespace(t) || (t.isStartTag() && t.asStartTag().name().equals("html"))) {
+ return tb.process(t, InBody);
+ } else if (t.isEOF()) {
+ // nice work chuck
+ } else {
+ tb.error(this);
+ tb.transition(InBody);
+ return tb.process(t);
+ }
+ return true;
+ }
+ },
+ AfterAfterFrameset {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ if (t.isComment()) {
+ tb.insert(t.asComment());
+ } else if (t.isDoctype() || isWhitespace(t) || (t.isStartTag() && t.asStartTag().name().equals("html"))) {
+ return tb.process(t, InBody);
+ } else if (t.isEOF()) {
+ // nice work chuck
+ } else if (t.isStartTag() && t.asStartTag().name().equals("noframes")) {
+ return tb.process(t, InHead);
+ } else {
+ tb.error(this);
+ return false;
+ }
+ return true;
+ }
+ },
+ ForeignContent {
+ boolean process(Token t, HtmlTreeBuilder tb) {
+ return true;
+ // todo: implement. Also; how do we get here?
+ }
+ };
+
+ private static String nullString = String.valueOf('\u0000');
+
+ abstract boolean process(Token t, HtmlTreeBuilder tb);
+
+ private static boolean isWhitespace(Token t) {
+ if (t.isCharacter()) {
+ String data = t.asCharacter().getData();
+ // todo: this checks more than spec - "\t", "\n", "\f", "\r", " "
+ for (int i = 0; i < data.length(); i++) {
+ char c = data.charAt(i);
+ if (!StringUtil.isWhitespace(c))
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static void handleRcData(Token.StartTag startTag, HtmlTreeBuilder tb) {
+ tb.insert(startTag);
+ tb.tokeniser.transition(TokeniserState.Rcdata);
+ tb.markInsertionMode();
+ tb.transition(Text);
+ }
+
+ private static void handleRawtext(Token.StartTag startTag, HtmlTreeBuilder tb) {
+ tb.insert(startTag);
+ tb.tokeniser.transition(TokeniserState.Rawtext);
+ tb.markInsertionMode();
+ tb.transition(Text);
+ }
+}
diff --git a/server/src/org/jsoup/parser/ParseError.java b/server/src/org/jsoup/parser/ParseError.java
new file mode 100644
index 0000000000..dfa090051b
--- /dev/null
+++ b/server/src/org/jsoup/parser/ParseError.java
@@ -0,0 +1,40 @@
+package org.jsoup.parser;
+
+/**
+ * A Parse Error records an error in the input HTML that occurs in either the tokenisation or the tree building phase.
+ */
+public class ParseError {
+ private int pos;
+ private String errorMsg;
+
+ ParseError(int pos, String errorMsg) {
+ this.pos = pos;
+ this.errorMsg = errorMsg;
+ }
+
+ ParseError(int pos, String errorFormat, Object... args) {
+ this.errorMsg = String.format(errorFormat, args);
+ this.pos = pos;
+ }
+
+ /**
+ * Retrieve the error message.
+ * @return the error message.
+ */
+ public String getErrorMessage() {
+ return errorMsg;
+ }
+
+ /**
+ * Retrieves the offset of the error.
+ * @return error offset within input
+ */
+ public int getPosition() {
+ return pos;
+ }
+
+ @Override
+ public String toString() {
+ return pos + ": " + errorMsg;
+ }
+}
diff --git a/server/src/org/jsoup/parser/ParseErrorList.java b/server/src/org/jsoup/parser/ParseErrorList.java
new file mode 100644
index 0000000000..3824ffbc4e
--- /dev/null
+++ b/server/src/org/jsoup/parser/ParseErrorList.java
@@ -0,0 +1,34 @@
+package org.jsoup.parser;
+
+import java.util.ArrayList;
+
+/**
+ * A container for ParseErrors.
+ *
+ * @author Jonathan Hedley
+ */
+class ParseErrorList extends ArrayList<ParseError>{
+ private static final int INITIAL_CAPACITY = 16;
+ private final int maxSize;
+
+ ParseErrorList(int initialCapacity, int maxSize) {
+ super(initialCapacity);
+ this.maxSize = maxSize;
+ }
+
+ boolean canAddError() {
+ return size() < maxSize;
+ }
+
+ int getMaxSize() {
+ return maxSize;
+ }
+
+ static ParseErrorList noTracking() {
+ return new ParseErrorList(0, 0);
+ }
+
+ static ParseErrorList tracking(int maxSize) {
+ return new ParseErrorList(INITIAL_CAPACITY, maxSize);
+ }
+}
diff --git a/server/src/org/jsoup/parser/Parser.java b/server/src/org/jsoup/parser/Parser.java
new file mode 100644
index 0000000000..2236219c06
--- /dev/null
+++ b/server/src/org/jsoup/parser/Parser.java
@@ -0,0 +1,157 @@
+package org.jsoup.parser;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+import java.util.List;
+
+/**
+ * Parses HTML into a {@link org.jsoup.nodes.Document}. Generally best to use one of the more convenient parse methods
+ * in {@link org.jsoup.Jsoup}.
+ */
+public class Parser {
+ private static final int DEFAULT_MAX_ERRORS = 0; // by default, error tracking is disabled.
+
+ private TreeBuilder treeBuilder;
+ private int maxErrors = DEFAULT_MAX_ERRORS;
+ private ParseErrorList errors;
+
+ /**
+ * Create a new Parser, using the specified TreeBuilder
+ * @param treeBuilder TreeBuilder to use to parse input into Documents.
+ */
+ public Parser(TreeBuilder treeBuilder) {
+ this.treeBuilder = treeBuilder;
+ }
+
+ public Document parseInput(String html, String baseUri) {
+ errors = isTrackErrors() ? ParseErrorList.tracking(maxErrors) : ParseErrorList.noTracking();
+ Document doc = treeBuilder.parse(html, baseUri, errors);
+ return doc;
+ }
+
+ // gets & sets
+ /**
+ * Get the TreeBuilder currently in use.
+ * @return current TreeBuilder.
+ */
+ public TreeBuilder getTreeBuilder() {
+ return treeBuilder;
+ }
+
+ /**
+ * Update the TreeBuilder used when parsing content.
+ * @param treeBuilder current TreeBuilder
+ * @return this, for chaining
+ */
+ public Parser setTreeBuilder(TreeBuilder treeBuilder) {
+ this.treeBuilder = treeBuilder;
+ return this;
+ }
+
+ /**
+ * Check if parse error tracking is enabled.
+ * @return current track error state.
+ */
+ public boolean isTrackErrors() {
+ return maxErrors > 0;
+ }
+
+ /**
+ * Enable or disable parse error tracking for the next parse.
+ * @param maxErrors the maximum number of errors to track. Set to 0 to disable.
+ * @return this, for chaining
+ */
+ public Parser setTrackErrors(int maxErrors) {
+ this.maxErrors = maxErrors;
+ return this;
+ }
+
+ /**
+ * Retrieve the parse errors, if any, from the last parse.
+ * @return list of parse errors, up to the size of the maximum errors tracked.
+ */
+ public List<ParseError> getErrors() {
+ return errors;
+ }
+
+ // static parse functions below
+ /**
+ * Parse HTML into a Document.
+ *
+ * @param html HTML to parse
+ * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
+ *
+ * @return parsed Document
+ */
+ public static Document parse(String html, String baseUri) {
+ TreeBuilder treeBuilder = new HtmlTreeBuilder();
+ return treeBuilder.parse(html, baseUri, ParseErrorList.noTracking());
+ }
+
+ /**
+ * Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context.
+ *
+ * @param fragmentHtml the fragment of HTML to parse
+ * @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). This
+ * provides stack context (for implicit element creation).
+ * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
+ *
+ * @return list of nodes parsed from the input HTML. Note that the context element, if supplied, is not modified.
+ */
+ public static List<Node> parseFragment(String fragmentHtml, Element context, String baseUri) {
+ HtmlTreeBuilder treeBuilder = new HtmlTreeBuilder();
+ return treeBuilder.parseFragment(fragmentHtml, context, baseUri, ParseErrorList.noTracking());
+ }
+
+ /**
+ * Parse a fragment of HTML into the {@code body} of a Document.
+ *
+ * @param bodyHtml fragment of HTML
+ * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
+ *
+ * @return Document, with empty head, and HTML parsed into body
+ */
+ public static Document parseBodyFragment(String bodyHtml, String baseUri) {
+ Document doc = Document.createShell(baseUri);
+ Element body = doc.body();
+ List<Node> nodeList = parseFragment(bodyHtml, body, baseUri);
+ Node[] nodes = nodeList.toArray(new Node[nodeList.size()]); // the node list gets modified when re-parented
+ for (Node node : nodes) {
+ body.appendChild(node);
+ }
+ return doc;
+ }
+
+ /**
+ * @param bodyHtml HTML to parse
+ * @param baseUri baseUri base URI of document (i.e. original fetch location), for resolving relative URLs.
+ *
+ * @return parsed Document
+ * @deprecated Use {@link #parseBodyFragment} or {@link #parseFragment} instead.
+ */
+ public static Document parseBodyFragmentRelaxed(String bodyHtml, String baseUri) {
+ return parse(bodyHtml, baseUri);
+ }
+
+ // builders
+
+ /**
+ * Create a new HTML parser. This parser treats input as HTML5, and enforces the creation of a normalised document,
+ * based on a knowledge of the semantics of the incoming tags.
+ * @return a new HTML parser.
+ */
+ public static Parser htmlParser() {
+ return new Parser(new HtmlTreeBuilder());
+ }
+
+ /**
+ * Create a new XML parser. This parser assumes no knowledge of the incoming tags and does not treat it as HTML,
+ * rather creates a simple tree directly from the input.
+ * @return a new simple XML parser.
+ */
+ public static Parser xmlParser() {
+ return new Parser(new XmlTreeBuilder());
+ }
+}
diff --git a/server/src/org/jsoup/parser/Tag.java b/server/src/org/jsoup/parser/Tag.java
new file mode 100644
index 0000000000..40b7557b39
--- /dev/null
+++ b/server/src/org/jsoup/parser/Tag.java
@@ -0,0 +1,262 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.Validate;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * HTML Tag capabilities.
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ */
+public class Tag {
+ private static final Map<String, Tag> tags = new HashMap<String, Tag>(); // map of known tags
+
+ private String tagName;
+ private boolean isBlock = true; // block or inline
+ private boolean formatAsBlock = true; // should be formatted as a block
+ private boolean canContainBlock = true; // Can this tag hold block level tags?
+ private boolean canContainInline = true; // only pcdata if not
+ private boolean empty = false; // can hold nothing; e.g. img
+ private boolean selfClosing = false; // can self close (<foo />). used for unknown tags that self close, without forcing them as empty.
+ private boolean preserveWhitespace = false; // for pre, textarea, script etc
+
+ private Tag(String tagName) {
+ this.tagName = tagName.toLowerCase();
+ }
+
+ /**
+ * Get this tag's name.
+ *
+ * @return the tag's name
+ */
+ public String getName() {
+ return tagName;
+ }
+
+ /**
+ * Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
+ * <p/>
+ * Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
+ *
+ * @param tagName Name of tag, e.g. "p". Case insensitive.
+ * @return The tag, either defined or new generic.
+ */
+ public static Tag valueOf(String tagName) {
+ Validate.notNull(tagName);
+ tagName = tagName.trim().toLowerCase();
+ Validate.notEmpty(tagName);
+
+ synchronized (tags) {
+ Tag tag = tags.get(tagName);
+ if (tag == null) {
+ // not defined: create default; go anywhere, do anything! (incl be inside a <p>)
+ tag = new Tag(tagName);
+ tag.isBlock = false;
+ tag.canContainBlock = true;
+ }
+ return tag;
+ }
+ }
+
+ /**
+ * Gets if this is a block tag.
+ *
+ * @return if block tag
+ */
+ public boolean isBlock() {
+ return isBlock;
+ }
+
+ /**
+ * Gets if this tag should be formatted as a block (or as inline)
+ *
+ * @return if should be formatted as block or inline
+ */
+ public boolean formatAsBlock() {
+ return formatAsBlock;
+ }
+
+ /**
+ * Gets if this tag can contain block tags.
+ *
+ * @return if tag can contain block tags
+ */
+ public boolean canContainBlock() {
+ return canContainBlock;
+ }
+
+ /**
+ * Gets if this tag is an inline tag.
+ *
+ * @return if this tag is an inline tag.
+ */
+ public boolean isInline() {
+ return !isBlock;
+ }
+
+ /**
+ * Gets if this tag is a data only tag.
+ *
+ * @return if this tag is a data only tag
+ */
+ public boolean isData() {
+ return !canContainInline && !isEmpty();
+ }
+
+ /**
+ * Get if this is an empty tag
+ *
+ * @return if this is an empty tag
+ */
+ public boolean isEmpty() {
+ return empty;
+ }
+
+ /**
+ * Get if this tag is self closing.
+ *
+ * @return if this tag should be output as self closing.
+ */
+ public boolean isSelfClosing() {
+ return empty || selfClosing;
+ }
+
+ /**
+ * Get if this is a pre-defined tag, or was auto created on parsing.
+ *
+ * @return if a known tag
+ */
+ public boolean isKnownTag() {
+ return tags.containsKey(tagName);
+ }
+
+ /**
+ * Check if this tagname is a known tag.
+ *
+ * @param tagName name of tag
+ * @return if known HTML tag
+ */
+ public static boolean isKnownTag(String tagName) {
+ return tags.containsKey(tagName);
+ }
+
+ /**
+ * Get if this tag should preserve whitespace within child text nodes.
+ *
+ * @return if preserve whitepace
+ */
+ public boolean preserveWhitespace() {
+ return preserveWhitespace;
+ }
+
+ Tag setSelfClosing() {
+ selfClosing = true;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Tag)) return false;
+
+ Tag tag = (Tag) o;
+
+ if (canContainBlock != tag.canContainBlock) return false;
+ if (canContainInline != tag.canContainInline) return false;
+ if (empty != tag.empty) return false;
+ if (formatAsBlock != tag.formatAsBlock) return false;
+ if (isBlock != tag.isBlock) return false;
+ if (preserveWhitespace != tag.preserveWhitespace) return false;
+ if (selfClosing != tag.selfClosing) return false;
+ if (!tagName.equals(tag.tagName)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tagName.hashCode();
+ result = 31 * result + (isBlock ? 1 : 0);
+ result = 31 * result + (formatAsBlock ? 1 : 0);
+ result = 31 * result + (canContainBlock ? 1 : 0);
+ result = 31 * result + (canContainInline ? 1 : 0);
+ result = 31 * result + (empty ? 1 : 0);
+ result = 31 * result + (selfClosing ? 1 : 0);
+ result = 31 * result + (preserveWhitespace ? 1 : 0);
+ return result;
+ }
+
+ public String toString() {
+ return tagName;
+ }
+
+ // internal static initialisers:
+ // prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources
+ private static final String[] blockTags = {
+ "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame",
+ "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6",
+ "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins",
+ "del", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th",
+ "td", "video", "audio", "canvas", "details", "menu", "plaintext"
+ };
+ private static final String[] inlineTags = {
+ "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd",
+ "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q",
+ "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup",
+ "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
+ "summary", "command", "device"
+ };
+ private static final String[] emptyTags = {
+ "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command",
+ "device"
+ };
+ private static final String[] formatAsInlineTags = {
+ "title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style"
+ };
+ private static final String[] preserveWhitespaceTags = {"pre", "plaintext", "title"};
+
+ static {
+ // creates
+ for (String tagName : blockTags) {
+ Tag tag = new Tag(tagName);
+ register(tag);
+ }
+ for (String tagName : inlineTags) {
+ Tag tag = new Tag(tagName);
+ tag.isBlock = false;
+ tag.canContainBlock = false;
+ tag.formatAsBlock = false;
+ register(tag);
+ }
+
+ // mods:
+ for (String tagName : emptyTags) {
+ Tag tag = tags.get(tagName);
+ Validate.notNull(tag);
+ tag.canContainBlock = false;
+ tag.canContainInline = false;
+ tag.empty = true;
+ }
+
+ for (String tagName : formatAsInlineTags) {
+ Tag tag = tags.get(tagName);
+ Validate.notNull(tag);
+ tag.formatAsBlock = false;
+ }
+
+ for (String tagName : preserveWhitespaceTags) {
+ Tag tag = tags.get(tagName);
+ Validate.notNull(tag);
+ tag.preserveWhitespace = true;
+ }
+ }
+
+ private static Tag register(Tag tag) {
+ synchronized (tags) {
+ tags.put(tag.tagName, tag);
+ }
+ return tag;
+ }
+}
diff --git a/server/src/org/jsoup/parser/Token.java b/server/src/org/jsoup/parser/Token.java
new file mode 100644
index 0000000000..9f4f9e250d
--- /dev/null
+++ b/server/src/org/jsoup/parser/Token.java
@@ -0,0 +1,252 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+
+/**
+ * Parse tokens for the Tokeniser.
+ */
+abstract class Token {
+ TokenType type;
+
+ private Token() {
+ }
+
+ String tokenType() {
+ return this.getClass().getSimpleName();
+ }
+
+ static class Doctype extends Token {
+ final StringBuilder name = new StringBuilder();
+ final StringBuilder publicIdentifier = new StringBuilder();
+ final StringBuilder systemIdentifier = new StringBuilder();
+ boolean forceQuirks = false;
+
+ Doctype() {
+ type = TokenType.Doctype;
+ }
+
+ String getName() {
+ return name.toString();
+ }
+
+ String getPublicIdentifier() {
+ return publicIdentifier.toString();
+ }
+
+ public String getSystemIdentifier() {
+ return systemIdentifier.toString();
+ }
+
+ public boolean isForceQuirks() {
+ return forceQuirks;
+ }
+ }
+
+ static abstract class Tag extends Token {
+ protected String tagName;
+ private String pendingAttributeName;
+ private String pendingAttributeValue;
+
+ boolean selfClosing = false;
+ Attributes attributes = new Attributes(); // todo: allow nodes to not have attributes
+
+ void newAttribute() {
+ if (pendingAttributeName != null) {
+ if (pendingAttributeValue == null)
+ pendingAttributeValue = "";
+ Attribute attribute = new Attribute(pendingAttributeName, pendingAttributeValue);
+ attributes.put(attribute);
+ }
+ pendingAttributeName = null;
+ pendingAttributeValue = null;
+ }
+
+ void finaliseTag() {
+ // finalises for emit
+ if (pendingAttributeName != null) {
+ // todo: check if attribute name exists; if so, drop and error
+ newAttribute();
+ }
+ }
+
+ String name() {
+ Validate.isFalse(tagName.length() == 0);
+ return tagName;
+ }
+
+ Tag name(String name) {
+ tagName = name;
+ return this;
+ }
+
+ boolean isSelfClosing() {
+ return selfClosing;
+ }
+
+ @SuppressWarnings({"TypeMayBeWeakened"})
+ Attributes getAttributes() {
+ return attributes;
+ }
+
+ // these appenders are rarely hit in not null state-- caused by null chars.
+ void appendTagName(String append) {
+ tagName = tagName == null ? append : tagName.concat(append);
+ }
+
+ void appendTagName(char append) {
+ appendTagName(String.valueOf(append));
+ }
+
+ void appendAttributeName(String append) {
+ pendingAttributeName = pendingAttributeName == null ? append : pendingAttributeName.concat(append);
+ }
+
+ void appendAttributeName(char append) {
+ appendAttributeName(String.valueOf(append));
+ }
+
+ void appendAttributeValue(String append) {
+ pendingAttributeValue = pendingAttributeValue == null ? append : pendingAttributeValue.concat(append);
+ }
+
+ void appendAttributeValue(char append) {
+ appendAttributeValue(String.valueOf(append));
+ }
+ }
+
+ static class StartTag extends Tag {
+ StartTag() {
+ super();
+ type = TokenType.StartTag;
+ }
+
+ StartTag(String name) {
+ this();
+ this.tagName = name;
+ }
+
+ StartTag(String name, Attributes attributes) {
+ this();
+ this.tagName = name;
+ this.attributes = attributes;
+ }
+
+ @Override
+ public String toString() {
+ return "<" + name() + " " + attributes.toString() + ">";
+ }
+ }
+
+ static class EndTag extends Tag{
+ EndTag() {
+ super();
+ type = TokenType.EndTag;
+ }
+
+ EndTag(String name) {
+ this();
+ this.tagName = name;
+ }
+
+ @Override
+ public String toString() {
+ return "</" + name() + " " + attributes.toString() + ">";
+ }
+ }
+
+ static class Comment extends Token {
+ final StringBuilder data = new StringBuilder();
+
+ Comment() {
+ type = TokenType.Comment;
+ }
+
+ String getData() {
+ return data.toString();
+ }
+
+ @Override
+ public String toString() {
+ return "<!--" + getData() + "-->";
+ }
+ }
+
+ static class Character extends Token {
+ private final String data;
+
+ Character(String data) {
+ type = TokenType.Character;
+ this.data = data;
+ }
+
+ String getData() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return getData();
+ }
+ }
+
+ static class EOF extends Token {
+ EOF() {
+ type = Token.TokenType.EOF;
+ }
+ }
+
+ boolean isDoctype() {
+ return type == TokenType.Doctype;
+ }
+
+ Doctype asDoctype() {
+ return (Doctype) this;
+ }
+
+ boolean isStartTag() {
+ return type == TokenType.StartTag;
+ }
+
+ StartTag asStartTag() {
+ return (StartTag) this;
+ }
+
+ boolean isEndTag() {
+ return type == TokenType.EndTag;
+ }
+
+ EndTag asEndTag() {
+ return (EndTag) this;
+ }
+
+ boolean isComment() {
+ return type == TokenType.Comment;
+ }
+
+ Comment asComment() {
+ return (Comment) this;
+ }
+
+ boolean isCharacter() {
+ return type == TokenType.Character;
+ }
+
+ Character asCharacter() {
+ return (Character) this;
+ }
+
+ boolean isEOF() {
+ return type == TokenType.EOF;
+ }
+
+ enum TokenType {
+ Doctype,
+ StartTag,
+ EndTag,
+ Comment,
+ Character,
+ EOF
+ }
+}
diff --git a/server/src/org/jsoup/parser/TokenQueue.java b/server/src/org/jsoup/parser/TokenQueue.java
new file mode 100644
index 0000000000..a2fdfe621a
--- /dev/null
+++ b/server/src/org/jsoup/parser/TokenQueue.java
@@ -0,0 +1,393 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+
+/**
+ * A character queue with parsing helpers.
+ *
+ * @author Jonathan Hedley
+ */
+public class TokenQueue {
+ private String queue;
+ private int pos = 0;
+
+ private static final char ESC = '\\'; // escape char for chomp balanced.
+
+ /**
+ Create a new TokenQueue.
+ @param data string of data to back queue.
+ */
+ public TokenQueue(String data) {
+ Validate.notNull(data);
+ queue = data;
+ }
+
+ /**
+ * Is the queue empty?
+ * @return true if no data left in queue.
+ */
+ public boolean isEmpty() {
+ return remainingLength() == 0;
+ }
+
+ private int remainingLength() {
+ return queue.length() - pos;
+ }
+
+ /**
+ * Retrieves but does not remove the first character from the queue.
+ * @return First character, or 0 if empty.
+ */
+ public char peek() {
+ return isEmpty() ? 0 : queue.charAt(pos);
+ }
+
+ /**
+ Add a character to the start of the queue (will be the next character retrieved).
+ @param c character to add
+ */
+ public void addFirst(Character c) {
+ addFirst(c.toString());
+ }
+
+ /**
+ Add a string to the start of the queue.
+ @param seq string to add.
+ */
+ public void addFirst(String seq) {
+ // not very performant, but an edge case
+ queue = seq + queue.substring(pos);
+ pos = 0;
+ }
+
+ /**
+ * Tests if the next characters on the queue match the sequence. Case insensitive.
+ * @param seq String to check queue for.
+ * @return true if the next characters match.
+ */
+ public boolean matches(String seq) {
+ return queue.regionMatches(true, pos, seq, 0, seq.length());
+ }
+
+ /**
+ * Case sensitive match test.
+ * @param seq string to case sensitively check for
+ * @return true if matched, false if not
+ */
+ public boolean matchesCS(String seq) {
+ return queue.startsWith(seq, pos);
+ }
+
+
+ /**
+ Tests if the next characters match any of the sequences. Case insensitive.
+ @param seq list of strings to case insensitively check for
+ @return true of any matched, false if none did
+ */
+ public boolean matchesAny(String... seq) {
+ for (String s : seq) {
+ if (matches(s))
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matchesAny(char... seq) {
+ if (isEmpty())
+ return false;
+
+ for (char c: seq) {
+ if (queue.charAt(pos) == c)
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matchesStartTag() {
+ // micro opt for matching "<x"
+ return (remainingLength() >= 2 && queue.charAt(pos) == '<' && Character.isLetter(queue.charAt(pos+1)));
+ }
+
+ /**
+ * Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the
+ * queue.
+ * @param seq String to search for, and if found, remove from queue.
+ * @return true if found and removed, false if not found.
+ */
+ public boolean matchChomp(String seq) {
+ if (matches(seq)) {
+ pos += seq.length();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ Tests if queue starts with a whitespace character.
+ @return if starts with whitespace
+ */
+ public boolean matchesWhitespace() {
+ return !isEmpty() && StringUtil.isWhitespace(queue.charAt(pos));
+ }
+
+ /**
+ Test if the queue matches a word character (letter or digit).
+ @return if matches a word character
+ */
+ public boolean matchesWord() {
+ return !isEmpty() && Character.isLetterOrDigit(queue.charAt(pos));
+ }
+
+ /**
+ * Drops the next character off the queue.
+ */
+ public void advance() {
+ if (!isEmpty()) pos++;
+ }
+
+ /**
+ * Consume one character off queue.
+ * @return first character on queue.
+ */
+ public char consume() {
+ return queue.charAt(pos++);
+ }
+
+ /**
+ * Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will
+ * throw an illegal state exception -- but you should be running match() against that condition.
+ <p>
+ Case insensitive.
+ * @param seq sequence to remove from head of queue.
+ */
+ public void consume(String seq) {
+ if (!matches(seq))
+ throw new IllegalStateException("Queue did not match expected sequence");
+ int len = seq.length();
+ if (len > remainingLength())
+ throw new IllegalStateException("Queue not long enough to consume sequence");
+
+ pos += len;
+ }
+
+ /**
+ * Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out.
+ * @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b>
+ * @return The matched data consumed from queue.
+ */
+ public String consumeTo(String seq) {
+ int offset = queue.indexOf(seq, pos);
+ if (offset != -1) {
+ String consumed = queue.substring(pos, offset);
+ pos += consumed.length();
+ return consumed;
+ } else {
+ return remainder();
+ }
+ }
+
+ public String consumeToIgnoreCase(String seq) {
+ int start = pos;
+ String first = seq.substring(0, 1);
+ boolean canScan = first.toLowerCase().equals(first.toUpperCase()); // if first is not cased, use index of
+ while (!isEmpty()) {
+ if (matches(seq))
+ break;
+
+ if (canScan) {
+ int skip = queue.indexOf(first, pos) - pos;
+ if (skip == 0) // this char is the skip char, but not match, so force advance of pos
+ pos++;
+ else if (skip < 0) // no chance of finding, grab to end
+ pos = queue.length();
+ else
+ pos += skip;
+ }
+ else
+ pos++;
+ }
+
+ String data = queue.substring(start, pos);
+ return data;
+ }
+
+ /**
+ Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue.
+ @param seq any number of terminators to consume to. <b>Case insensitive.</b>
+ @return consumed string
+ */
+ // todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this
+ // is is a case sensitive time...
+ public String consumeToAny(String... seq) {
+ int start = pos;
+ while (!isEmpty() && !matchesAny(seq)) {
+ pos++;
+ }
+
+ String data = queue.substring(start, pos);
+ return data;
+ }
+
+ /**
+ * Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it).
+ * <p>
+ * If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go
+ * isEmpty() == true).
+ * @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b>
+ * @return Data matched from queue.
+ */
+ public String chompTo(String seq) {
+ String data = consumeTo(seq);
+ matchChomp(seq);
+ return data;
+ }
+
+ public String chompToIgnoreCase(String seq) {
+ String data = consumeToIgnoreCase(seq); // case insensitive scan
+ matchChomp(seq);
+ return data;
+ }
+
+ /**
+ * Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three",
+ * and leave " four" on the queue. Unbalanced openers and closers can be escaped (with \). Those escapes will be left
+ * in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for
+ * contains text strings; use unescape for that.
+ * @param open opener
+ * @param close closer
+ * @return data matched from the queue
+ */
+ public String chompBalanced(char open, char close) {
+ StringBuilder accum = new StringBuilder();
+ int depth = 0;
+ char last = 0;
+
+ do {
+ if (isEmpty()) break;
+ Character c = consume();
+ if (last == 0 || last != ESC) {
+ if (c.equals(open))
+ depth++;
+ else if (c.equals(close))
+ depth--;
+ }
+
+ if (depth > 0 && last != 0)
+ accum.append(c); // don't include the outer match pair in the return
+ last = c;
+ } while (depth > 0);
+ return accum.toString();
+ }
+
+ /**
+ * Unescaped a \ escaped string.
+ * @param in backslash escaped string
+ * @return unescaped string
+ */
+ public static String unescape(String in) {
+ StringBuilder out = new StringBuilder();
+ char last = 0;
+ for (char c : in.toCharArray()) {
+ if (c == ESC) {
+ if (last != 0 && last == ESC)
+ out.append(c);
+ }
+ else
+ out.append(c);
+ last = c;
+ }
+ return out.toString();
+ }
+
+ /**
+ * Pulls the next run of whitespace characters of the queue.
+ */
+ public boolean consumeWhitespace() {
+ boolean seen = false;
+ while (matchesWhitespace()) {
+ pos++;
+ seen = true;
+ }
+ return seen;
+ }
+
+ /**
+ * Retrieves the next run of word type (letter or digit) off the queue.
+ * @return String of word characters from queue, or empty string if none.
+ */
+ public String consumeWord() {
+ int start = pos;
+ while (matchesWord())
+ pos++;
+ return queue.substring(start, pos);
+ }
+
+ /**
+ * Consume an tag name off the queue (word or :, _, -)
+ *
+ * @return tag name
+ */
+ public String consumeTagName() {
+ int start = pos;
+ while (!isEmpty() && (matchesWord() || matchesAny(':', '_', '-')))
+ pos++;
+
+ return queue.substring(start, pos);
+ }
+
+ /**
+ * Consume a CSS element selector (tag name, but | instead of : for namespaces, to not conflict with :pseudo selects).
+ *
+ * @return tag name
+ */
+ public String consumeElementSelector() {
+ int start = pos;
+ while (!isEmpty() && (matchesWord() || matchesAny('|', '_', '-')))
+ pos++;
+
+ return queue.substring(start, pos);
+ }
+
+ /**
+ Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _)
+ http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
+ @return identifier
+ */
+ public String consumeCssIdentifier() {
+ int start = pos;
+ while (!isEmpty() && (matchesWord() || matchesAny('-', '_')))
+ pos++;
+
+ return queue.substring(start, pos);
+ }
+
+ /**
+ Consume an attribute key off the queue (letter, digit, -, _, :")
+ @return attribute key
+ */
+ public String consumeAttributeKey() {
+ int start = pos;
+ while (!isEmpty() && (matchesWord() || matchesAny('-', '_', ':')))
+ pos++;
+
+ return queue.substring(start, pos);
+ }
+
+ /**
+ Consume and return whatever is left on the queue.
+ @return remained of queue.
+ */
+ public String remainder() {
+ StringBuilder accum = new StringBuilder();
+ while (!isEmpty()) {
+ accum.append(consume());
+ }
+ return accum.toString();
+ }
+
+ public String toString() {
+ return queue.substring(pos);
+ }
+}
diff --git a/server/src/org/jsoup/parser/Tokeniser.java b/server/src/org/jsoup/parser/Tokeniser.java
new file mode 100644
index 0000000000..ce6ee690d6
--- /dev/null
+++ b/server/src/org/jsoup/parser/Tokeniser.java
@@ -0,0 +1,230 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Entities;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Readers the input stream into tokens.
+ */
+class Tokeniser {
+ static final char replacementChar = '\uFFFD'; // replaces null character
+
+ private CharacterReader reader; // html input
+ private ParseErrorList errors; // errors found while tokenising
+
+ private TokeniserState state = TokeniserState.Data; // current tokenisation state
+ private Token emitPending; // the token we are about to emit on next read
+ private boolean isEmitPending = false;
+ private StringBuilder charBuffer = new StringBuilder(); // buffers characters to output as one token
+ StringBuilder dataBuffer; // buffers data looking for </script>
+
+ Token.Tag tagPending; // tag we are building up
+ Token.Doctype doctypePending; // doctype building up
+ Token.Comment commentPending; // comment building up
+ private Token.StartTag lastStartTag; // the last start tag emitted, to test appropriate end tag
+ private boolean selfClosingFlagAcknowledged = true;
+
+ Tokeniser(CharacterReader reader, ParseErrorList errors) {
+ this.reader = reader;
+ this.errors = errors;
+ }
+
+ Token read() {
+ if (!selfClosingFlagAcknowledged) {
+ error("Self closing flag not acknowledged");
+ selfClosingFlagAcknowledged = true;
+ }
+
+ while (!isEmitPending)
+ state.read(this, reader);
+
+ // if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read:
+ if (charBuffer.length() > 0) {
+ String str = charBuffer.toString();
+ charBuffer.delete(0, charBuffer.length());
+ return new Token.Character(str);
+ } else {
+ isEmitPending = false;
+ return emitPending;
+ }
+ }
+
+ void emit(Token token) {
+ Validate.isFalse(isEmitPending, "There is an unread token pending!");
+
+ emitPending = token;
+ isEmitPending = true;
+
+ if (token.type == Token.TokenType.StartTag) {
+ Token.StartTag startTag = (Token.StartTag) token;
+ lastStartTag = startTag;
+ if (startTag.selfClosing)
+ selfClosingFlagAcknowledged = false;
+ } else if (token.type == Token.TokenType.EndTag) {
+ Token.EndTag endTag = (Token.EndTag) token;
+ if (endTag.attributes.size() > 0)
+ error("Attributes incorrectly present on end tag");
+ }
+ }
+
+ void emit(String str) {
+ // buffer strings up until last string token found, to emit only one token for a run of character refs etc.
+ // does not set isEmitPending; read checks that
+ charBuffer.append(str);
+ }
+
+ void emit(char c) {
+ charBuffer.append(c);
+ }
+
+ TokeniserState getState() {
+ return state;
+ }
+
+ void transition(TokeniserState state) {
+ this.state = state;
+ }
+
+ void advanceTransition(TokeniserState state) {
+ reader.advance();
+ this.state = state;
+ }
+
+ void acknowledgeSelfClosingFlag() {
+ selfClosingFlagAcknowledged = true;
+ }
+
+ Character consumeCharacterReference(Character additionalAllowedCharacter, boolean inAttribute) {
+ if (reader.isEmpty())
+ return null;
+ if (additionalAllowedCharacter != null && additionalAllowedCharacter == reader.current())
+ return null;
+ if (reader.matchesAny('\t', '\n', '\f', ' ', '<', '&'))
+ return null;
+
+ reader.mark();
+ if (reader.matchConsume("#")) { // numbered
+ boolean isHexMode = reader.matchConsumeIgnoreCase("X");
+ String numRef = isHexMode ? reader.consumeHexSequence() : reader.consumeDigitSequence();
+ if (numRef.length() == 0) { // didn't match anything
+ characterReferenceError("numeric reference with no numerals");
+ reader.rewindToMark();
+ return null;
+ }
+ if (!reader.matchConsume(";"))
+ characterReferenceError("missing semicolon"); // missing semi
+ int charval = -1;
+ try {
+ int base = isHexMode ? 16 : 10;
+ charval = Integer.valueOf(numRef, base);
+ } catch (NumberFormatException e) {
+ } // skip
+ if (charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF) {
+ characterReferenceError("character outside of valid range");
+ return replacementChar;
+ } else {
+ // todo: implement number replacement table
+ // todo: check for extra illegal unicode points as parse errors
+ return (char) charval;
+ }
+ } else { // named
+ // get as many letters as possible, and look for matching entities. unconsume backwards till a match is found
+ String nameRef = reader.consumeLetterThenDigitSequence();
+ String origNameRef = new String(nameRef); // for error reporting. nameRef gets chomped looking for matches
+ boolean looksLegit = reader.matches(';');
+ boolean found = false;
+ while (nameRef.length() > 0 && !found) {
+ if (Entities.isNamedEntity(nameRef))
+ found = true;
+ else {
+ nameRef = nameRef.substring(0, nameRef.length()-1);
+ reader.unconsume();
+ }
+ }
+ if (!found) {
+ if (looksLegit) // named with semicolon
+ characterReferenceError(String.format("invalid named referenece '%s'", origNameRef));
+ reader.rewindToMark();
+ return null;
+ }
+ if (inAttribute && (reader.matchesLetter() || reader.matchesDigit() || reader.matchesAny('=', '-', '_'))) {
+ // don't want that to match
+ reader.rewindToMark();
+ return null;
+ }
+ if (!reader.matchConsume(";"))
+ characterReferenceError("missing semicolon"); // missing semi
+ return Entities.getCharacterByName(nameRef);
+ }
+ }
+
+ Token.Tag createTagPending(boolean start) {
+ tagPending = start ? new Token.StartTag() : new Token.EndTag();
+ return tagPending;
+ }
+
+ void emitTagPending() {
+ tagPending.finaliseTag();
+ emit(tagPending);
+ }
+
+ void createCommentPending() {
+ commentPending = new Token.Comment();
+ }
+
+ void emitCommentPending() {
+ emit(commentPending);
+ }
+
+ void createDoctypePending() {
+ doctypePending = new Token.Doctype();
+ }
+
+ void emitDoctypePending() {
+ emit(doctypePending);
+ }
+
+ void createTempBuffer() {
+ dataBuffer = new StringBuilder();
+ }
+
+ boolean isAppropriateEndTagToken() {
+ if (lastStartTag == null)
+ return false;
+ return tagPending.tagName.equals(lastStartTag.tagName);
+ }
+
+ String appropriateEndTagName() {
+ return lastStartTag.tagName;
+ }
+
+ void error(TokeniserState state) {
+ if (errors.canAddError())
+ errors.add(new ParseError(reader.pos(), "Unexpected character '%s' in input state [%s]", reader.current(), state));
+ }
+
+ void eofError(TokeniserState state) {
+ if (errors.canAddError())
+ errors.add(new ParseError(reader.pos(), "Unexpectedly reached end of file (EOF) in input state [%s]", state));
+ }
+
+ private void characterReferenceError(String message) {
+ if (errors.canAddError())
+ errors.add(new ParseError(reader.pos(), "Invalid character reference: %s", message));
+ }
+
+ private void error(String errorMsg) {
+ if (errors.canAddError())
+ errors.add(new ParseError(reader.pos(), errorMsg));
+ }
+
+ boolean currentNodeInHtmlNS() {
+ // todo: implement namespaces correctly
+ return true;
+ // Element currentNode = currentNode();
+ // return currentNode != null && currentNode.namespace().equals("HTML");
+ }
+}
diff --git a/server/src/org/jsoup/parser/TokeniserState.java b/server/src/org/jsoup/parser/TokeniserState.java
new file mode 100644
index 0000000000..e3013c73e9
--- /dev/null
+++ b/server/src/org/jsoup/parser/TokeniserState.java
@@ -0,0 +1,1778 @@
+package org.jsoup.parser;
+
+/**
+ * States and transition activations for the Tokeniser.
+ */
+enum TokeniserState {
+ Data {
+ // in data state, gather characters until a character reference or tag is found
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case '&':
+ t.advanceTransition(CharacterReferenceInData);
+ break;
+ case '<':
+ t.advanceTransition(TagOpen);
+ break;
+ case nullChar:
+ t.error(this); // NOT replacement character (oddly?)
+ t.emit(r.consume());
+ break;
+ case eof:
+ t.emit(new Token.EOF());
+ break;
+ default:
+ String data = r.consumeToAny('&', '<', nullChar);
+ t.emit(data);
+ break;
+ }
+ }
+ },
+ CharacterReferenceInData {
+ // from & in data
+ void read(Tokeniser t, CharacterReader r) {
+ Character c = t.consumeCharacterReference(null, false);
+ if (c == null)
+ t.emit('&');
+ else
+ t.emit(c);
+ t.transition(Data);
+ }
+ },
+ Rcdata {
+ /// handles data in title, textarea etc
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case '&':
+ t.advanceTransition(CharacterReferenceInRcdata);
+ break;
+ case '<':
+ t.advanceTransition(RcdataLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ case eof:
+ t.emit(new Token.EOF());
+ break;
+ default:
+ String data = r.consumeToAny('&', '<', nullChar);
+ t.emit(data);
+ break;
+ }
+ }
+ },
+ CharacterReferenceInRcdata {
+ void read(Tokeniser t, CharacterReader r) {
+ Character c = t.consumeCharacterReference(null, false);
+ if (c == null)
+ t.emit('&');
+ else
+ t.emit(c);
+ t.transition(Rcdata);
+ }
+ },
+ Rawtext {
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case '<':
+ t.advanceTransition(RawtextLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ case eof:
+ t.emit(new Token.EOF());
+ break;
+ default:
+ String data = r.consumeToAny('<', nullChar);
+ t.emit(data);
+ break;
+ }
+ }
+ },
+ ScriptData {
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case '<':
+ t.advanceTransition(ScriptDataLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ case eof:
+ t.emit(new Token.EOF());
+ break;
+ default:
+ String data = r.consumeToAny('<', nullChar);
+ t.emit(data);
+ break;
+ }
+ }
+ },
+ PLAINTEXT {
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ case eof:
+ t.emit(new Token.EOF());
+ break;
+ default:
+ String data = r.consumeTo(nullChar);
+ t.emit(data);
+ break;
+ }
+ }
+ },
+ TagOpen {
+ // from < in data
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.current()) {
+ case '!':
+ t.advanceTransition(MarkupDeclarationOpen);
+ break;
+ case '/':
+ t.advanceTransition(EndTagOpen);
+ break;
+ case '?':
+ t.advanceTransition(BogusComment);
+ break;
+ default:
+ if (r.matchesLetter()) {
+ t.createTagPending(true);
+ t.transition(TagName);
+ } else {
+ t.error(this);
+ t.emit('<'); // char that got us here
+ t.transition(Data);
+ }
+ break;
+ }
+ }
+ },
+ EndTagOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.isEmpty()) {
+ t.eofError(this);
+ t.emit("</");
+ t.transition(Data);
+ } else if (r.matchesLetter()) {
+ t.createTagPending(false);
+ t.transition(TagName);
+ } else if (r.matches('>')) {
+ t.error(this);
+ t.advanceTransition(Data);
+ } else {
+ t.error(this);
+ t.advanceTransition(BogusComment);
+ }
+ }
+ },
+ TagName {
+ // from < or </ in data, will have start or end tag pending
+ void read(Tokeniser t, CharacterReader r) {
+ // previous TagOpen state did NOT consume, will have a letter char in current
+ String tagName = r.consumeToAny('\t', '\n', '\f', ' ', '/', '>', nullChar).toLowerCase();
+ t.tagPending.appendTagName(tagName);
+
+ switch (r.consume()) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case nullChar: // replacement
+ t.tagPending.appendTagName(replacementStr);
+ break;
+ case eof: // should emit pending tag?
+ t.eofError(this);
+ t.transition(Data);
+ // no default, as covered with above consumeToAny
+ }
+ }
+ },
+ RcdataLessthanSign {
+ // from < in rcdata
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matches('/')) {
+ t.createTempBuffer();
+ t.advanceTransition(RCDATAEndTagOpen);
+ } else if (r.matchesLetter() && !r.containsIgnoreCase("</" + t.appropriateEndTagName())) {
+ // diverge from spec: got a start tag, but there's no appropriate end tag (</title>), so rather than
+ // consuming to EOF; break out here
+ t.tagPending = new Token.EndTag(t.appropriateEndTagName());
+ t.emitTagPending();
+ r.unconsume(); // undo "<"
+ t.transition(Data);
+ } else {
+ t.emit("<");
+ t.transition(Rcdata);
+ }
+ }
+ },
+ RCDATAEndTagOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createTagPending(false);
+ t.tagPending.appendTagName(Character.toLowerCase(r.current()));
+ t.dataBuffer.append(Character.toLowerCase(r.current()));
+ t.advanceTransition(RCDATAEndTagName);
+ } else {
+ t.emit("</");
+ t.transition(Rcdata);
+ }
+ }
+ },
+ RCDATAEndTagName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.tagPending.appendTagName(name.toLowerCase());
+ t.dataBuffer.append(name);
+ return;
+ }
+
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ if (t.isAppropriateEndTagToken())
+ t.transition(BeforeAttributeName);
+ else
+ anythingElse(t, r);
+ break;
+ case '/':
+ if (t.isAppropriateEndTagToken())
+ t.transition(SelfClosingStartTag);
+ else
+ anythingElse(t, r);
+ break;
+ case '>':
+ if (t.isAppropriateEndTagToken()) {
+ t.emitTagPending();
+ t.transition(Data);
+ }
+ else
+ anythingElse(t, r);
+ break;
+ default:
+ anythingElse(t, r);
+ }
+ }
+
+ private void anythingElse(Tokeniser t, CharacterReader r) {
+ t.emit("</" + t.dataBuffer.toString());
+ t.transition(Rcdata);
+ }
+ },
+ RawtextLessthanSign {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matches('/')) {
+ t.createTempBuffer();
+ t.advanceTransition(RawtextEndTagOpen);
+ } else {
+ t.emit('<');
+ t.transition(Rawtext);
+ }
+ }
+ },
+ RawtextEndTagOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createTagPending(false);
+ t.transition(RawtextEndTagName);
+ } else {
+ t.emit("</");
+ t.transition(Rawtext);
+ }
+ }
+ },
+ RawtextEndTagName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.tagPending.appendTagName(name.toLowerCase());
+ t.dataBuffer.append(name);
+ return;
+ }
+
+ if (t.isAppropriateEndTagToken() && !r.isEmpty()) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ default:
+ t.dataBuffer.append(c);
+ anythingElse(t, r);
+ }
+ } else
+ anythingElse(t, r);
+ }
+
+ private void anythingElse(Tokeniser t, CharacterReader r) {
+ t.emit("</" + t.dataBuffer.toString());
+ t.transition(Rawtext);
+ }
+ },
+ ScriptDataLessthanSign {
+ void read(Tokeniser t, CharacterReader r) {
+ switch (r.consume()) {
+ case '/':
+ t.createTempBuffer();
+ t.transition(ScriptDataEndTagOpen);
+ break;
+ case '!':
+ t.emit("<!");
+ t.transition(ScriptDataEscapeStart);
+ break;
+ default:
+ t.emit("<");
+ r.unconsume();
+ t.transition(ScriptData);
+ }
+ }
+ },
+ ScriptDataEndTagOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createTagPending(false);
+ t.transition(ScriptDataEndTagName);
+ } else {
+ t.emit("</");
+ t.transition(ScriptData);
+ }
+
+ }
+ },
+ ScriptDataEndTagName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.tagPending.appendTagName(name.toLowerCase());
+ t.dataBuffer.append(name);
+ return;
+ }
+
+ if (t.isAppropriateEndTagToken() && !r.isEmpty()) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ default:
+ t.dataBuffer.append(c);
+ anythingElse(t, r);
+ }
+ } else {
+ anythingElse(t, r);
+ }
+ }
+
+ private void anythingElse(Tokeniser t, CharacterReader r) {
+ t.emit("</" + t.dataBuffer.toString());
+ t.transition(ScriptData);
+ }
+ },
+ ScriptDataEscapeStart {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matches('-')) {
+ t.emit('-');
+ t.advanceTransition(ScriptDataEscapeStartDash);
+ } else {
+ t.transition(ScriptData);
+ }
+ }
+ },
+ ScriptDataEscapeStartDash {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matches('-')) {
+ t.emit('-');
+ t.advanceTransition(ScriptDataEscapedDashDash);
+ } else {
+ t.transition(ScriptData);
+ }
+ }
+ },
+ ScriptDataEscaped {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.isEmpty()) {
+ t.eofError(this);
+ t.transition(Data);
+ return;
+ }
+
+ switch (r.current()) {
+ case '-':
+ t.emit('-');
+ t.advanceTransition(ScriptDataEscapedDash);
+ break;
+ case '<':
+ t.advanceTransition(ScriptDataEscapedLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ default:
+ String data = r.consumeToAny('-', '<', nullChar);
+ t.emit(data);
+ }
+ }
+ },
+ ScriptDataEscapedDash {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.isEmpty()) {
+ t.eofError(this);
+ t.transition(Data);
+ return;
+ }
+
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.emit(c);
+ t.transition(ScriptDataEscapedDashDash);
+ break;
+ case '<':
+ t.transition(ScriptDataEscapedLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ t.emit(replacementChar);
+ t.transition(ScriptDataEscaped);
+ break;
+ default:
+ t.emit(c);
+ t.transition(ScriptDataEscaped);
+ }
+ }
+ },
+ ScriptDataEscapedDashDash {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.isEmpty()) {
+ t.eofError(this);
+ t.transition(Data);
+ return;
+ }
+
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.emit(c);
+ break;
+ case '<':
+ t.transition(ScriptDataEscapedLessthanSign);
+ break;
+ case '>':
+ t.emit(c);
+ t.transition(ScriptData);
+ break;
+ case nullChar:
+ t.error(this);
+ t.emit(replacementChar);
+ t.transition(ScriptDataEscaped);
+ break;
+ default:
+ t.emit(c);
+ t.transition(ScriptDataEscaped);
+ }
+ }
+ },
+ ScriptDataEscapedLessthanSign {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createTempBuffer();
+ t.dataBuffer.append(Character.toLowerCase(r.current()));
+ t.emit("<" + r.current());
+ t.advanceTransition(ScriptDataDoubleEscapeStart);
+ } else if (r.matches('/')) {
+ t.createTempBuffer();
+ t.advanceTransition(ScriptDataEscapedEndTagOpen);
+ } else {
+ t.emit('<');
+ t.transition(ScriptDataEscaped);
+ }
+ }
+ },
+ ScriptDataEscapedEndTagOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createTagPending(false);
+ t.tagPending.appendTagName(Character.toLowerCase(r.current()));
+ t.dataBuffer.append(r.current());
+ t.advanceTransition(ScriptDataEscapedEndTagName);
+ } else {
+ t.emit("</");
+ t.transition(ScriptDataEscaped);
+ }
+ }
+ },
+ ScriptDataEscapedEndTagName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.tagPending.appendTagName(name.toLowerCase());
+ t.dataBuffer.append(name);
+ return;
+ }
+
+ if (t.isAppropriateEndTagToken() && !r.isEmpty()) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ default:
+ t.dataBuffer.append(c);
+ anythingElse(t, r);
+ break;
+ }
+ } else {
+ anythingElse(t, r);
+ }
+ }
+
+ private void anythingElse(Tokeniser t, CharacterReader r) {
+ t.emit("</" + t.dataBuffer.toString());
+ t.transition(ScriptDataEscaped);
+ }
+ },
+ ScriptDataDoubleEscapeStart {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.dataBuffer.append(name.toLowerCase());
+ t.emit(name);
+ return;
+ }
+
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ case '/':
+ case '>':
+ if (t.dataBuffer.toString().equals("script"))
+ t.transition(ScriptDataDoubleEscaped);
+ else
+ t.transition(ScriptDataEscaped);
+ t.emit(c);
+ break;
+ default:
+ r.unconsume();
+ t.transition(ScriptDataEscaped);
+ }
+ }
+ },
+ ScriptDataDoubleEscaped {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.current();
+ switch (c) {
+ case '-':
+ t.emit(c);
+ t.advanceTransition(ScriptDataDoubleEscapedDash);
+ break;
+ case '<':
+ t.emit(c);
+ t.advanceTransition(ScriptDataDoubleEscapedLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.emit(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ default:
+ String data = r.consumeToAny('-', '<', nullChar);
+ t.emit(data);
+ }
+ }
+ },
+ ScriptDataDoubleEscapedDash {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.emit(c);
+ t.transition(ScriptDataDoubleEscapedDashDash);
+ break;
+ case '<':
+ t.emit(c);
+ t.transition(ScriptDataDoubleEscapedLessthanSign);
+ break;
+ case nullChar:
+ t.error(this);
+ t.emit(replacementChar);
+ t.transition(ScriptDataDoubleEscaped);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ default:
+ t.emit(c);
+ t.transition(ScriptDataDoubleEscaped);
+ }
+ }
+ },
+ ScriptDataDoubleEscapedDashDash {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.emit(c);
+ break;
+ case '<':
+ t.emit(c);
+ t.transition(ScriptDataDoubleEscapedLessthanSign);
+ break;
+ case '>':
+ t.emit(c);
+ t.transition(ScriptData);
+ break;
+ case nullChar:
+ t.error(this);
+ t.emit(replacementChar);
+ t.transition(ScriptDataDoubleEscaped);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ default:
+ t.emit(c);
+ t.transition(ScriptDataDoubleEscaped);
+ }
+ }
+ },
+ ScriptDataDoubleEscapedLessthanSign {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matches('/')) {
+ t.emit('/');
+ t.createTempBuffer();
+ t.advanceTransition(ScriptDataDoubleEscapeEnd);
+ } else {
+ t.transition(ScriptDataDoubleEscaped);
+ }
+ }
+ },
+ ScriptDataDoubleEscapeEnd {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.dataBuffer.append(name.toLowerCase());
+ t.emit(name);
+ return;
+ }
+
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ case '/':
+ case '>':
+ if (t.dataBuffer.toString().equals("script"))
+ t.transition(ScriptDataEscaped);
+ else
+ t.transition(ScriptDataDoubleEscaped);
+ t.emit(c);
+ break;
+ default:
+ r.unconsume();
+ t.transition(ScriptDataDoubleEscaped);
+ }
+ }
+ },
+ BeforeAttributeName {
+ // from tagname <xxx
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break; // ignore whitespace
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.newAttribute();
+ r.unconsume();
+ t.transition(AttributeName);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ case '"':
+ case '\'':
+ case '<':
+ case '=':
+ t.error(this);
+ t.tagPending.newAttribute();
+ t.tagPending.appendAttributeName(c);
+ t.transition(AttributeName);
+ break;
+ default: // A-Z, anything else
+ t.tagPending.newAttribute();
+ r.unconsume();
+ t.transition(AttributeName);
+ }
+ }
+ },
+ AttributeName {
+ // from before attribute name
+ void read(Tokeniser t, CharacterReader r) {
+ String name = r.consumeToAny('\t', '\n', '\f', ' ', '/', '=', '>', nullChar, '"', '\'', '<');
+ t.tagPending.appendAttributeName(name.toLowerCase());
+
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(AfterAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '=':
+ t.transition(BeforeAttributeValue);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeName(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ case '"':
+ case '\'':
+ case '<':
+ t.error(this);
+ t.tagPending.appendAttributeName(c);
+ // no default, as covered in consumeToAny
+ }
+ }
+ },
+ AfterAttributeName {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ // ignore
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '=':
+ t.transition(BeforeAttributeValue);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeName(replacementChar);
+ t.transition(AttributeName);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ case '"':
+ case '\'':
+ case '<':
+ t.error(this);
+ t.tagPending.newAttribute();
+ t.tagPending.appendAttributeName(c);
+ t.transition(AttributeName);
+ break;
+ default: // A-Z, anything else
+ t.tagPending.newAttribute();
+ r.unconsume();
+ t.transition(AttributeName);
+ }
+ }
+ },
+ BeforeAttributeValue {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ // ignore
+ break;
+ case '"':
+ t.transition(AttributeValue_doubleQuoted);
+ break;
+ case '&':
+ r.unconsume();
+ t.transition(AttributeValue_unquoted);
+ break;
+ case '\'':
+ t.transition(AttributeValue_singleQuoted);
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeValue(replacementChar);
+ t.transition(AttributeValue_unquoted);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ case '>':
+ t.error(this);
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case '<':
+ case '=':
+ case '`':
+ t.error(this);
+ t.tagPending.appendAttributeValue(c);
+ t.transition(AttributeValue_unquoted);
+ break;
+ default:
+ r.unconsume();
+ t.transition(AttributeValue_unquoted);
+ }
+ }
+ },
+ AttributeValue_doubleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ String value = r.consumeToAny('"', '&', nullChar);
+ if (value.length() > 0)
+ t.tagPending.appendAttributeValue(value);
+
+ char c = r.consume();
+ switch (c) {
+ case '"':
+ t.transition(AfterAttributeValue_quoted);
+ break;
+ case '&':
+ Character ref = t.consumeCharacterReference('"', true);
+ if (ref != null)
+ t.tagPending.appendAttributeValue(ref);
+ else
+ t.tagPending.appendAttributeValue('&');
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeValue(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ // no default, handled in consume to any above
+ }
+ }
+ },
+ AttributeValue_singleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ String value = r.consumeToAny('\'', '&', nullChar);
+ if (value.length() > 0)
+ t.tagPending.appendAttributeValue(value);
+
+ char c = r.consume();
+ switch (c) {
+ case '\'':
+ t.transition(AfterAttributeValue_quoted);
+ break;
+ case '&':
+ Character ref = t.consumeCharacterReference('\'', true);
+ if (ref != null)
+ t.tagPending.appendAttributeValue(ref);
+ else
+ t.tagPending.appendAttributeValue('&');
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeValue(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ // no default, handled in consume to any above
+ }
+ }
+ },
+ AttributeValue_unquoted {
+ void read(Tokeniser t, CharacterReader r) {
+ String value = r.consumeToAny('\t', '\n', '\f', ' ', '&', '>', nullChar, '"', '\'', '<', '=', '`');
+ if (value.length() > 0)
+ t.tagPending.appendAttributeValue(value);
+
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '&':
+ Character ref = t.consumeCharacterReference('>', true);
+ if (ref != null)
+ t.tagPending.appendAttributeValue(ref);
+ else
+ t.tagPending.appendAttributeValue('&');
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.tagPending.appendAttributeValue(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ case '"':
+ case '\'':
+ case '<':
+ case '=':
+ case '`':
+ t.error(this);
+ t.tagPending.appendAttributeValue(c);
+ break;
+ // no default, handled in consume to any above
+ }
+
+ }
+ },
+ // CharacterReferenceInAttributeValue state handled inline
+ AfterAttributeValue_quoted {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeAttributeName);
+ break;
+ case '/':
+ t.transition(SelfClosingStartTag);
+ break;
+ case '>':
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ r.unconsume();
+ t.transition(BeforeAttributeName);
+ }
+
+ }
+ },
+ SelfClosingStartTag {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '>':
+ t.tagPending.selfClosing = true;
+ t.emitTagPending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.transition(BeforeAttributeName);
+ }
+ }
+ },
+ BogusComment {
+ void read(Tokeniser t, CharacterReader r) {
+ // todo: handle bogus comment starting from eof. when does that trigger?
+ // rewind to capture character that lead us here
+ r.unconsume();
+ Token.Comment comment = new Token.Comment();
+ comment.data.append(r.consumeTo('>'));
+ // todo: replace nullChar with replaceChar
+ t.emit(comment);
+ t.advanceTransition(Data);
+ }
+ },
+ MarkupDeclarationOpen {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchConsume("--")) {
+ t.createCommentPending();
+ t.transition(CommentStart);
+ } else if (r.matchConsumeIgnoreCase("DOCTYPE")) {
+ t.transition(Doctype);
+ } else if (r.matchConsume("[CDATA[")) {
+ // todo: should actually check current namepspace, and only non-html allows cdata. until namespace
+ // is implemented properly, keep handling as cdata
+ //} else if (!t.currentNodeInHtmlNS() && r.matchConsume("[CDATA[")) {
+ t.transition(CdataSection);
+ } else {
+ t.error(this);
+ t.advanceTransition(BogusComment); // advance so this character gets in bogus comment data's rewind
+ }
+ }
+ },
+ CommentStart {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.transition(CommentStartDash);
+ break;
+ case nullChar:
+ t.error(this);
+ t.commentPending.data.append(replacementChar);
+ t.transition(Comment);
+ break;
+ case '>':
+ t.error(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.commentPending.data.append(c);
+ t.transition(Comment);
+ }
+ }
+ },
+ CommentStartDash {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.transition(CommentStartDash);
+ break;
+ case nullChar:
+ t.error(this);
+ t.commentPending.data.append(replacementChar);
+ t.transition(Comment);
+ break;
+ case '>':
+ t.error(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.commentPending.data.append(c);
+ t.transition(Comment);
+ }
+ }
+ },
+ Comment {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.current();
+ switch (c) {
+ case '-':
+ t.advanceTransition(CommentEndDash);
+ break;
+ case nullChar:
+ t.error(this);
+ r.advance();
+ t.commentPending.data.append(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.commentPending.data.append(r.consumeToAny('-', nullChar));
+ }
+ }
+ },
+ CommentEndDash {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.transition(CommentEnd);
+ break;
+ case nullChar:
+ t.error(this);
+ t.commentPending.data.append('-').append(replacementChar);
+ t.transition(Comment);
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.commentPending.data.append('-').append(c);
+ t.transition(Comment);
+ }
+ }
+ },
+ CommentEnd {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '>':
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.commentPending.data.append("--").append(replacementChar);
+ t.transition(Comment);
+ break;
+ case '!':
+ t.error(this);
+ t.transition(CommentEndBang);
+ break;
+ case '-':
+ t.error(this);
+ t.commentPending.data.append('-');
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.commentPending.data.append("--").append(c);
+ t.transition(Comment);
+ }
+ }
+ },
+ CommentEndBang {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '-':
+ t.commentPending.data.append("--!");
+ t.transition(CommentEndDash);
+ break;
+ case '>':
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ case nullChar:
+ t.error(this);
+ t.commentPending.data.append("--!").append(replacementChar);
+ t.transition(Comment);
+ break;
+ case eof:
+ t.eofError(this);
+ t.emitCommentPending();
+ t.transition(Data);
+ break;
+ default:
+ t.commentPending.data.append("--!").append(c);
+ t.transition(Comment);
+ }
+ }
+ },
+ Doctype {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeDoctypeName);
+ break;
+ case eof:
+ t.eofError(this);
+ t.createDoctypePending();
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.transition(BeforeDoctypeName);
+ }
+ }
+ },
+ BeforeDoctypeName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ t.createDoctypePending();
+ t.transition(DoctypeName);
+ return;
+ }
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break; // ignore whitespace
+ case nullChar:
+ t.error(this);
+ t.doctypePending.name.append(replacementChar);
+ t.transition(DoctypeName);
+ break;
+ case eof:
+ t.eofError(this);
+ t.createDoctypePending();
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.createDoctypePending();
+ t.doctypePending.name.append(c);
+ t.transition(DoctypeName);
+ }
+ }
+ },
+ DoctypeName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.matchesLetter()) {
+ String name = r.consumeLetterSequence();
+ t.doctypePending.name.append(name.toLowerCase());
+ return;
+ }
+ char c = r.consume();
+ switch (c) {
+ case '>':
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(AfterDoctypeName);
+ break;
+ case nullChar:
+ t.error(this);
+ t.doctypePending.name.append(replacementChar);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.doctypePending.name.append(c);
+ }
+ }
+ },
+ AfterDoctypeName {
+ void read(Tokeniser t, CharacterReader r) {
+ if (r.isEmpty()) {
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ return;
+ }
+ if (r.matchesAny('\t', '\n', '\f', ' '))
+ r.advance(); // ignore whitespace
+ else if (r.matches('>')) {
+ t.emitDoctypePending();
+ t.advanceTransition(Data);
+ } else if (r.matchConsumeIgnoreCase("PUBLIC")) {
+ t.transition(AfterDoctypePublicKeyword);
+ } else if (r.matchConsumeIgnoreCase("SYSTEM")) {
+ t.transition(AfterDoctypeSystemKeyword);
+ } else {
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.advanceTransition(BogusDoctype);
+ }
+
+ }
+ },
+ AfterDoctypePublicKeyword {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeDoctypePublicIdentifier);
+ break;
+ case '"':
+ t.error(this);
+ // set public id to empty string
+ t.transition(DoctypePublicIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ t.error(this);
+ // set public id to empty string
+ t.transition(DoctypePublicIdentifier_singleQuoted);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.transition(BogusDoctype);
+ }
+ }
+ },
+ BeforeDoctypePublicIdentifier {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break;
+ case '"':
+ // set public id to empty string
+ t.transition(DoctypePublicIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ // set public id to empty string
+ t.transition(DoctypePublicIdentifier_singleQuoted);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.transition(BogusDoctype);
+ }
+ }
+ },
+ DoctypePublicIdentifier_doubleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '"':
+ t.transition(AfterDoctypePublicIdentifier);
+ break;
+ case nullChar:
+ t.error(this);
+ t.doctypePending.publicIdentifier.append(replacementChar);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.doctypePending.publicIdentifier.append(c);
+ }
+ }
+ },
+ DoctypePublicIdentifier_singleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\'':
+ t.transition(AfterDoctypePublicIdentifier);
+ break;
+ case nullChar:
+ t.error(this);
+ t.doctypePending.publicIdentifier.append(replacementChar);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.doctypePending.publicIdentifier.append(c);
+ }
+ }
+ },
+ AfterDoctypePublicIdentifier {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BetweenDoctypePublicAndSystemIdentifiers);
+ break;
+ case '>':
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case '"':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_singleQuoted);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.transition(BogusDoctype);
+ }
+ }
+ },
+ BetweenDoctypePublicAndSystemIdentifiers {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break;
+ case '>':
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case '"':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_singleQuoted);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.transition(BogusDoctype);
+ }
+ }
+ },
+ AfterDoctypeSystemKeyword {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ t.transition(BeforeDoctypeSystemIdentifier);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case '"':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ t.error(this);
+ // system id empty
+ t.transition(DoctypeSystemIdentifier_singleQuoted);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ }
+ }
+ },
+ BeforeDoctypeSystemIdentifier {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break;
+ case '"':
+ // set system id to empty string
+ t.transition(DoctypeSystemIdentifier_doubleQuoted);
+ break;
+ case '\'':
+ // set public id to empty string
+ t.transition(DoctypeSystemIdentifier_singleQuoted);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.transition(BogusDoctype);
+ }
+ }
+ },
+ DoctypeSystemIdentifier_doubleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '"':
+ t.transition(AfterDoctypeSystemIdentifier);
+ break;
+ case nullChar:
+ t.error(this);
+ t.doctypePending.systemIdentifier.append(replacementChar);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.doctypePending.systemIdentifier.append(c);
+ }
+ }
+ },
+ DoctypeSystemIdentifier_singleQuoted {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\'':
+ t.transition(AfterDoctypeSystemIdentifier);
+ break;
+ case nullChar:
+ t.error(this);
+ t.doctypePending.systemIdentifier.append(replacementChar);
+ break;
+ case '>':
+ t.error(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.doctypePending.systemIdentifier.append(c);
+ }
+ }
+ },
+ AfterDoctypeSystemIdentifier {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '\t':
+ case '\n':
+ case '\f':
+ case ' ':
+ break;
+ case '>':
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.eofError(this);
+ t.doctypePending.forceQuirks = true;
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ t.error(this);
+ t.transition(BogusDoctype);
+ // NOT force quirks
+ }
+ }
+ },
+ BogusDoctype {
+ void read(Tokeniser t, CharacterReader r) {
+ char c = r.consume();
+ switch (c) {
+ case '>':
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ case eof:
+ t.emitDoctypePending();
+ t.transition(Data);
+ break;
+ default:
+ // ignore char
+ break;
+ }
+ }
+ },
+ CdataSection {
+ void read(Tokeniser t, CharacterReader r) {
+ String data = r.consumeTo("]]>");
+ t.emit(data);
+ r.matchConsume("]]>");
+ t.transition(Data);
+ }
+ };
+
+
+ abstract void read(Tokeniser t, CharacterReader r);
+
+ private static final char nullChar = '\u0000';
+ private static final char replacementChar = Tokeniser.replacementChar;
+ private static final String replacementStr = String.valueOf(Tokeniser.replacementChar);
+ private static final char eof = CharacterReader.EOF;
+}
diff --git a/server/src/org/jsoup/parser/TreeBuilder.java b/server/src/org/jsoup/parser/TreeBuilder.java
new file mode 100644
index 0000000000..e06caad501
--- /dev/null
+++ b/server/src/org/jsoup/parser/TreeBuilder.java
@@ -0,0 +1,60 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.DescendableLinkedList;
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Jonathan Hedley
+ */
+abstract class TreeBuilder {
+ CharacterReader reader;
+ Tokeniser tokeniser;
+ protected Document doc; // current doc we are building into
+ protected DescendableLinkedList<Element> stack; // the stack of open elements
+ protected String baseUri; // current base uri, for creating new elements
+ protected Token currentToken; // currentToken is used only for error tracking.
+ protected ParseErrorList errors; // null when not tracking errors
+
+ protected void initialiseParse(String input, String baseUri, ParseErrorList errors) {
+ Validate.notNull(input, "String input must not be null");
+ Validate.notNull(baseUri, "BaseURI must not be null");
+
+ doc = new Document(baseUri);
+ reader = new CharacterReader(input);
+ this.errors = errors;
+ tokeniser = new Tokeniser(reader, errors);
+ stack = new DescendableLinkedList<Element>();
+ this.baseUri = baseUri;
+ }
+
+ Document parse(String input, String baseUri) {
+ return parse(input, baseUri, ParseErrorList.noTracking());
+ }
+
+ Document parse(String input, String baseUri, ParseErrorList errors) {
+ initialiseParse(input, baseUri, errors);
+ runParser();
+ return doc;
+ }
+
+ protected void runParser() {
+ while (true) {
+ Token token = tokeniser.read();
+ process(token);
+
+ if (token.type == Token.TokenType.EOF)
+ break;
+ }
+ }
+
+ protected abstract boolean process(Token token);
+
+ protected Element currentElement() {
+ return stack.getLast();
+ }
+}
diff --git a/server/src/org/jsoup/parser/XmlTreeBuilder.java b/server/src/org/jsoup/parser/XmlTreeBuilder.java
new file mode 100644
index 0000000000..3f03ad26ac
--- /dev/null
+++ b/server/src/org/jsoup/parser/XmlTreeBuilder.java
@@ -0,0 +1,111 @@
+package org.jsoup.parser;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.*;
+
+import java.util.Iterator;
+
+/**
+ * @author Jonathan Hedley
+ */
+public class XmlTreeBuilder extends TreeBuilder {
+ @Override
+ protected void initialiseParse(String input, String baseUri, ParseErrorList errors) {
+ super.initialiseParse(input, baseUri, errors);
+ stack.add(doc); // place the document onto the stack. differs from HtmlTreeBuilder (not on stack)
+ }
+
+ @Override
+ protected boolean process(Token token) {
+ // start tag, end tag, doctype, comment, character, eof
+ switch (token.type) {
+ case StartTag:
+ insert(token.asStartTag());
+ break;
+ case EndTag:
+ popStackToClose(token.asEndTag());
+ break;
+ case Comment:
+ insert(token.asComment());
+ break;
+ case Character:
+ insert(token.asCharacter());
+ break;
+ case Doctype:
+ insert(token.asDoctype());
+ break;
+ case EOF: // could put some normalisation here if desired
+ break;
+ default:
+ Validate.fail("Unexpected token type: " + token.type);
+ }
+ return true;
+ }
+
+ private void insertNode(Node node) {
+ currentElement().appendChild(node);
+ }
+
+ Element insert(Token.StartTag startTag) {
+ Tag tag = Tag.valueOf(startTag.name());
+ // todo: wonder if for xml parsing, should treat all tags as unknown? because it's not html.
+ Element el = new Element(tag, baseUri, startTag.attributes);
+ insertNode(el);
+ if (startTag.isSelfClosing()) {
+ tokeniser.acknowledgeSelfClosingFlag();
+ if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above.
+ tag.setSelfClosing();
+ } else {
+ stack.add(el);
+ }
+ return el;
+ }
+
+ void insert(Token.Comment commentToken) {
+ Comment comment = new Comment(commentToken.getData(), baseUri);
+ insertNode(comment);
+ }
+
+ void insert(Token.Character characterToken) {
+ Node node = new TextNode(characterToken.getData(), baseUri);
+ insertNode(node);
+ }
+
+ void insert(Token.Doctype d) {
+ DocumentType doctypeNode = new DocumentType(d.getName(), d.getPublicIdentifier(), d.getSystemIdentifier(), baseUri);
+ insertNode(doctypeNode);
+ }
+
+ /**
+ * If the stack contains an element with this tag's name, pop up the stack to remove the first occurrence. If not
+ * found, skips.
+ *
+ * @param endTag
+ */
+ private void popStackToClose(Token.EndTag endTag) {
+ String elName = endTag.name();
+ Element firstFound = null;
+
+ Iterator<Element> it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next.nodeName().equals(elName)) {
+ firstFound = next;
+ break;
+ }
+ }
+ if (firstFound == null)
+ return; // not found, skip
+
+ it = stack.descendingIterator();
+ while (it.hasNext()) {
+ Element next = it.next();
+ if (next == firstFound) {
+ it.remove();
+ break;
+ } else {
+ it.remove();
+ }
+ }
+ }
+}
diff --git a/server/src/org/jsoup/parser/package-info.java b/server/src/org/jsoup/parser/package-info.java
new file mode 100644
index 0000000000..168fdf4086
--- /dev/null
+++ b/server/src/org/jsoup/parser/package-info.java
@@ -0,0 +1,4 @@
+/**
+ Contains the HTML parser, tag specifications, and HTML tokeniser.
+ */
+package org.jsoup.parser;
diff --git a/server/src/org/jsoup/safety/Cleaner.java b/server/src/org/jsoup/safety/Cleaner.java
new file mode 100644
index 0000000000..eda67df86b
--- /dev/null
+++ b/server/src/org/jsoup/safety/Cleaner.java
@@ -0,0 +1,129 @@
+package org.jsoup.safety;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.*;
+import org.jsoup.parser.Tag;
+
+import java.util.List;
+
+/**
+ The whitelist based HTML cleaner. Use to ensure that end-user provided HTML contains only the elements and attributes
+ that you are expecting; no junk, and no cross-site scripting attacks!
+ <p/>
+ The HTML cleaner parses the input as HTML and then runs it through a white-list, so the output HTML can only contain
+ HTML that is allowed by the whitelist.
+ <p/>
+ It is assumed that the input HTML is a body fragment; the clean methods only pull from the source's body, and the
+ canned white-lists only allow body contained tags.
+ <p/>
+ Rather than interacting directly with a Cleaner object, generally see the {@code clean} methods in {@link org.jsoup.Jsoup}.
+ */
+public class Cleaner {
+ private Whitelist whitelist;
+
+ /**
+ Create a new cleaner, that sanitizes documents using the supplied whitelist.
+ @param whitelist white-list to clean with
+ */
+ public Cleaner(Whitelist whitelist) {
+ Validate.notNull(whitelist);
+ this.whitelist = whitelist;
+ }
+
+ /**
+ Creates a new, clean document, from the original dirty document, containing only elements allowed by the whitelist.
+ The original document is not modified. Only elements from the dirt document's <code>body</code> are used.
+ @param dirtyDocument Untrusted base document to clean.
+ @return cleaned document.
+ */
+ public Document clean(Document dirtyDocument) {
+ Validate.notNull(dirtyDocument);
+
+ Document clean = Document.createShell(dirtyDocument.baseUri());
+ copySafeNodes(dirtyDocument.body(), clean.body());
+
+ return clean;
+ }
+
+ /**
+ Determines if the input document is valid, against the whitelist. It is considered valid if all the tags and attributes
+ in the input HTML are allowed by the whitelist.
+ <p/>
+ This method can be used as a validator for user input forms. An invalid document will still be cleaned successfully
+ using the {@link #clean(Document)} document. If using as a validator, it is recommended to still clean the document
+ to ensure enforced attributes are set correctly, and that the output is tidied.
+ @param dirtyDocument document to test
+ @return true if no tags or attributes need to be removed; false if they do
+ */
+ public boolean isValid(Document dirtyDocument) {
+ Validate.notNull(dirtyDocument);
+
+ Document clean = Document.createShell(dirtyDocument.baseUri());
+ int numDiscarded = copySafeNodes(dirtyDocument.body(), clean.body());
+ return numDiscarded == 0;
+ }
+
+ /**
+ Iterates the input and copies trusted nodes (tags, attributes, text) into the destination.
+ @param source source of HTML
+ @param dest destination element to copy into
+ @return number of discarded elements (that were considered unsafe)
+ */
+ private int copySafeNodes(Element source, Element dest) {
+ List<Node> sourceChildren = source.childNodes();
+ int numDiscarded = 0;
+
+ for (Node sourceChild : sourceChildren) {
+ if (sourceChild instanceof Element) {
+ Element sourceEl = (Element) sourceChild;
+
+ if (whitelist.isSafeTag(sourceEl.tagName())) { // safe, clone and copy safe attrs
+ ElementMeta meta = createSafeElement(sourceEl);
+ Element destChild = meta.el;
+ dest.appendChild(destChild);
+
+ numDiscarded += meta.numAttribsDiscarded;
+ numDiscarded += copySafeNodes(sourceEl, destChild); // recurs
+ } else { // not a safe tag, but it may have children (els or text) that are, so recurse
+ numDiscarded++;
+ numDiscarded += copySafeNodes(sourceEl, dest);
+ }
+ } else if (sourceChild instanceof TextNode) {
+ TextNode sourceText = (TextNode) sourceChild;
+ TextNode destText = new TextNode(sourceText.getWholeText(), sourceChild.baseUri());
+ dest.appendChild(destText);
+ } // else, we don't care about comments, xml proc instructions, etc
+ }
+ return numDiscarded;
+ }
+
+ private ElementMeta createSafeElement(Element sourceEl) {
+ String sourceTag = sourceEl.tagName();
+ Attributes destAttrs = new Attributes();
+ Element dest = new Element(Tag.valueOf(sourceTag), sourceEl.baseUri(), destAttrs);
+ int numDiscarded = 0;
+
+ Attributes sourceAttrs = sourceEl.attributes();
+ for (Attribute sourceAttr : sourceAttrs) {
+ if (whitelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr))
+ destAttrs.put(sourceAttr);
+ else
+ numDiscarded++;
+ }
+ Attributes enforcedAttrs = whitelist.getEnforcedAttributes(sourceTag);
+ destAttrs.addAll(enforcedAttrs);
+
+ return new ElementMeta(dest, numDiscarded);
+ }
+
+ private static class ElementMeta {
+ Element el;
+ int numAttribsDiscarded;
+
+ ElementMeta(Element el, int numAttribsDiscarded) {
+ this.el = el;
+ this.numAttribsDiscarded = numAttribsDiscarded;
+ }
+ }
+
+}
diff --git a/server/src/org/jsoup/safety/Whitelist.java b/server/src/org/jsoup/safety/Whitelist.java
new file mode 100644
index 0000000000..2c1150ce9e
--- /dev/null
+++ b/server/src/org/jsoup/safety/Whitelist.java
@@ -0,0 +1,451 @@
+package org.jsoup.safety;
+
+/*
+ Thank you to Ryan Grove (wonko.com) for the Ruby HTML cleaner http://github.com/rgrove/sanitize/, which inspired
+ this whitelist configuration, and the initial defaults.
+ */
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Attribute;
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ Whitelists define what HTML (elements and attributes) to allow through the cleaner. Everything else is removed.
+ <p/>
+ Start with one of the defaults:
+ <ul>
+ <li>{@link #none}
+ <li>{@link #simpleText}
+ <li>{@link #basic}
+ <li>{@link #basicWithImages}
+ <li>{@link #relaxed}
+ </ul>
+ <p/>
+ If you need to allow more through (please be careful!), tweak a base whitelist with:
+ <ul>
+ <li>{@link #addTags}
+ <li>{@link #addAttributes}
+ <li>{@link #addEnforcedAttribute}
+ <li>{@link #addProtocols}
+ </ul>
+ <p/>
+ The cleaner and these whitelists assume that you want to clean a <code>body</code> fragment of HTML (to add user
+ supplied HTML into a templated page), and not to clean a full HTML document. If the latter is the case, either wrap the
+ document HTML around the cleaned body HTML, or create a whitelist that allows <code>html</code> and <code>head</code>
+ elements as appropriate.
+ <p/>
+ If you are going to extend a whitelist, please be very careful. Make sure you understand what attributes may lead to
+ XSS attack vectors. URL attributes are particularly vulnerable and require careful validation. See
+ http://ha.ckers.org/xss.html for some XSS attack examples.
+
+ @author Jonathan Hedley
+ */
+public class Whitelist {
+ private Set<TagName> tagNames; // tags allowed, lower case. e.g. [p, br, span]
+ private Map<TagName, Set<AttributeKey>> attributes; // tag -> attribute[]. allowed attributes [href] for a tag.
+ private Map<TagName, Map<AttributeKey, AttributeValue>> enforcedAttributes; // always set these attribute values
+ private Map<TagName, Map<AttributeKey, Set<Protocol>>> protocols; // allowed URL protocols for attributes
+ private boolean preserveRelativeLinks; // option to preserve relative links
+
+ /**
+ This whitelist allows only text nodes: all HTML will be stripped.
+
+ @return whitelist
+ */
+ public static Whitelist none() {
+ return new Whitelist();
+ }
+
+ /**
+ This whitelist allows only simple text formatting: <code>b, em, i, strong, u</code>. All other HTML (tags and
+ attributes) will be removed.
+
+ @return whitelist
+ */
+ public static Whitelist simpleText() {
+ return new Whitelist()
+ .addTags("b", "em", "i", "strong", "u")
+ ;
+ }
+
+ /**
+ This whitelist allows a fuller range of text nodes: <code>a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li,
+ ol, p, pre, q, small, strike, strong, sub, sup, u, ul</code>, and appropriate attributes.
+ <p/>
+ Links (<code>a</code> elements) can point to <code>http, https, ftp, mailto</code>, and have an enforced
+ <code>rel=nofollow</code> attribute.
+ <p/>
+ Does not allow images.
+
+ @return whitelist
+ */
+ public static Whitelist basic() {
+ return new Whitelist()
+ .addTags(
+ "a", "b", "blockquote", "br", "cite", "code", "dd", "dl", "dt", "em",
+ "i", "li", "ol", "p", "pre", "q", "small", "strike", "strong", "sub",
+ "sup", "u", "ul")
+
+ .addAttributes("a", "href")
+ .addAttributes("blockquote", "cite")
+ .addAttributes("q", "cite")
+
+ .addProtocols("a", "href", "ftp", "http", "https", "mailto")
+ .addProtocols("blockquote", "cite", "http", "https")
+ .addProtocols("cite", "cite", "http", "https")
+
+ .addEnforcedAttribute("a", "rel", "nofollow")
+ ;
+
+ }
+
+ /**
+ This whitelist allows the same text tags as {@link #basic}, and also allows <code>img</code> tags, with appropriate
+ attributes, with <code>src</code> pointing to <code>http</code> or <code>https</code>.
+
+ @return whitelist
+ */
+ public static Whitelist basicWithImages() {
+ return basic()
+ .addTags("img")
+ .addAttributes("img", "align", "alt", "height", "src", "title", "width")
+ .addProtocols("img", "src", "http", "https")
+ ;
+ }
+
+ /**
+ This whitelist allows a full range of text and structural body HTML: <code>a, b, blockquote, br, caption, cite,
+ code, col, colgroup, dd, dl, dt, em, h1, h2, h3, h4, h5, h6, i, img, li, ol, p, pre, q, small, strike, strong, sub,
+ sup, table, tbody, td, tfoot, th, thead, tr, u, ul</code>
+ <p/>
+ Links do not have an enforced <code>rel=nofollow</code> attribute, but you can add that if desired.
+
+ @return whitelist
+ */
+ public static Whitelist relaxed() {
+ return new Whitelist()
+ .addTags(
+ "a", "b", "blockquote", "br", "caption", "cite", "code", "col",
+ "colgroup", "dd", "div", "dl", "dt", "em", "h1", "h2", "h3", "h4", "h5", "h6",
+ "i", "img", "li", "ol", "p", "pre", "q", "small", "strike", "strong",
+ "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u",
+ "ul")
+
+ .addAttributes("a", "href", "title")
+ .addAttributes("blockquote", "cite")
+ .addAttributes("col", "span", "width")
+ .addAttributes("colgroup", "span", "width")
+ .addAttributes("img", "align", "alt", "height", "src", "title", "width")
+ .addAttributes("ol", "start", "type")
+ .addAttributes("q", "cite")
+ .addAttributes("table", "summary", "width")
+ .addAttributes("td", "abbr", "axis", "colspan", "rowspan", "width")
+ .addAttributes(
+ "th", "abbr", "axis", "colspan", "rowspan", "scope",
+ "width")
+ .addAttributes("ul", "type")
+
+ .addProtocols("a", "href", "ftp", "http", "https", "mailto")
+ .addProtocols("blockquote", "cite", "http", "https")
+ .addProtocols("img", "src", "http", "https")
+ .addProtocols("q", "cite", "http", "https")
+ ;
+ }
+
+ /**
+ Create a new, empty whitelist. Generally it will be better to start with a default prepared whitelist instead.
+
+ @see #basic()
+ @see #basicWithImages()
+ @see #simpleText()
+ @see #relaxed()
+ */
+ public Whitelist() {
+ tagNames = new HashSet<TagName>();
+ attributes = new HashMap<TagName, Set<AttributeKey>>();
+ enforcedAttributes = new HashMap<TagName, Map<AttributeKey, AttributeValue>>();
+ protocols = new HashMap<TagName, Map<AttributeKey, Set<Protocol>>>();
+ preserveRelativeLinks = false;
+ }
+
+ /**
+ Add a list of allowed elements to a whitelist. (If a tag is not allowed, it will be removed from the HTML.)
+
+ @param tags tag names to allow
+ @return this (for chaining)
+ */
+ public Whitelist addTags(String... tags) {
+ Validate.notNull(tags);
+
+ for (String tagName : tags) {
+ Validate.notEmpty(tagName);
+ tagNames.add(TagName.valueOf(tagName));
+ }
+ return this;
+ }
+
+ /**
+ Add a list of allowed attributes to a tag. (If an attribute is not allowed on an element, it will be removed.)
+ <p/>
+ E.g.: <code>addAttributes("a", "href", "class")</code> allows <code>href</code> and <code>class</code> attributes
+ on <code>a</code> tags.
+ <p/>
+ To make an attribute valid for <b>all tags</b>, use the pseudo tag <code>:all</code>, e.g.
+ <code>addAttributes(":all", "class")</code>.
+
+ @param tag The tag the attributes are for. The tag will be added to the allowed tag list if necessary.
+ @param keys List of valid attributes for the tag
+ @return this (for chaining)
+ */
+ public Whitelist addAttributes(String tag, String... keys) {
+ Validate.notEmpty(tag);
+ Validate.notNull(keys);
+ Validate.isTrue(keys.length > 0, "No attributes supplied.");
+
+ TagName tagName = TagName.valueOf(tag);
+ if (!tagNames.contains(tagName))
+ tagNames.add(tagName);
+ Set<AttributeKey> attributeSet = new HashSet<AttributeKey>();
+ for (String key : keys) {
+ Validate.notEmpty(key);
+ attributeSet.add(AttributeKey.valueOf(key));
+ }
+ if (attributes.containsKey(tagName)) {
+ Set<AttributeKey> currentSet = attributes.get(tagName);
+ currentSet.addAll(attributeSet);
+ } else {
+ attributes.put(tagName, attributeSet);
+ }
+ return this;
+ }
+
+ /**
+ Add an enforced attribute to a tag. An enforced attribute will always be added to the element. If the element
+ already has the attribute set, it will be overridden.
+ <p/>
+ E.g.: <code>addEnforcedAttribute("a", "rel", "nofollow")</code> will make all <code>a</code> tags output as
+ <code>&lt;a href="..." rel="nofollow"></code>
+
+ @param tag The tag the enforced attribute is for. The tag will be added to the allowed tag list if necessary.
+ @param key The attribute key
+ @param value The enforced attribute value
+ @return this (for chaining)
+ */
+ public Whitelist addEnforcedAttribute(String tag, String key, String value) {
+ Validate.notEmpty(tag);
+ Validate.notEmpty(key);
+ Validate.notEmpty(value);
+
+ TagName tagName = TagName.valueOf(tag);
+ if (!tagNames.contains(tagName))
+ tagNames.add(tagName);
+ AttributeKey attrKey = AttributeKey.valueOf(key);
+ AttributeValue attrVal = AttributeValue.valueOf(value);
+
+ if (enforcedAttributes.containsKey(tagName)) {
+ enforcedAttributes.get(tagName).put(attrKey, attrVal);
+ } else {
+ Map<AttributeKey, AttributeValue> attrMap = new HashMap<AttributeKey, AttributeValue>();
+ attrMap.put(attrKey, attrVal);
+ enforcedAttributes.put(tagName, attrMap);
+ }
+ return this;
+ }
+
+ /**
+ * Configure this Whitelist to preserve relative links in an element's URL attribute, or convert them to absolute
+ * links. By default, this is <b>false</b>: URLs will be made absolute (e.g. start with an allowed protocol, like
+ * e.g. {@code http://}.
+ * <p />
+ * Note that when handling relative links, the input document must have an appropriate {@code base URI} set when
+ * parsing, so that the link's protocol can be confirmed. Regardless of the setting of the {@code preserve relative
+ * links} option, the link must be resolvable against the base URI to an allowed protocol; otherwise the attribute
+ * will be removed.
+ *
+ * @param preserve {@code true} to allow relative links, {@code false} (default) to deny
+ * @return this Whitelist, for chaining.
+ * @see #addProtocols
+ */
+ public Whitelist preserveRelativeLinks(boolean preserve) {
+ preserveRelativeLinks = preserve;
+ return this;
+ }
+
+ /**
+ Add allowed URL protocols for an element's URL attribute. This restricts the possible values of the attribute to
+ URLs with the defined protocol.
+ <p/>
+ E.g.: <code>addProtocols("a", "href", "ftp", "http", "https")</code>
+
+ @param tag Tag the URL protocol is for
+ @param key Attribute key
+ @param protocols List of valid protocols
+ @return this, for chaining
+ */
+ public Whitelist addProtocols(String tag, String key, String... protocols) {
+ Validate.notEmpty(tag);
+ Validate.notEmpty(key);
+ Validate.notNull(protocols);
+
+ TagName tagName = TagName.valueOf(tag);
+ AttributeKey attrKey = AttributeKey.valueOf(key);
+ Map<AttributeKey, Set<Protocol>> attrMap;
+ Set<Protocol> protSet;
+
+ if (this.protocols.containsKey(tagName)) {
+ attrMap = this.protocols.get(tagName);
+ } else {
+ attrMap = new HashMap<AttributeKey, Set<Protocol>>();
+ this.protocols.put(tagName, attrMap);
+ }
+ if (attrMap.containsKey(attrKey)) {
+ protSet = attrMap.get(attrKey);
+ } else {
+ protSet = new HashSet<Protocol>();
+ attrMap.put(attrKey, protSet);
+ }
+ for (String protocol : protocols) {
+ Validate.notEmpty(protocol);
+ Protocol prot = Protocol.valueOf(protocol);
+ protSet.add(prot);
+ }
+ return this;
+ }
+
+ boolean isSafeTag(String tag) {
+ return tagNames.contains(TagName.valueOf(tag));
+ }
+
+ boolean isSafeAttribute(String tagName, Element el, Attribute attr) {
+ TagName tag = TagName.valueOf(tagName);
+ AttributeKey key = AttributeKey.valueOf(attr.getKey());
+
+ if (attributes.containsKey(tag)) {
+ if (attributes.get(tag).contains(key)) {
+ if (protocols.containsKey(tag)) {
+ Map<AttributeKey, Set<Protocol>> attrProts = protocols.get(tag);
+ // ok if not defined protocol; otherwise test
+ return !attrProts.containsKey(key) || testValidProtocol(el, attr, attrProts.get(key));
+ } else { // attribute found, no protocols defined, so OK
+ return true;
+ }
+ }
+ }
+ // no attributes defined for tag, try :all tag
+ return !tagName.equals(":all") && isSafeAttribute(":all", el, attr);
+ }
+
+ private boolean testValidProtocol(Element el, Attribute attr, Set<Protocol> protocols) {
+ // try to resolve relative urls to abs, and optionally update the attribute so output html has abs.
+ // rels without a baseuri get removed
+ String value = el.absUrl(attr.getKey());
+ if (value.length() == 0)
+ value = attr.getValue(); // if it could not be made abs, run as-is to allow custom unknown protocols
+ if (!preserveRelativeLinks)
+ attr.setValue(value);
+
+ for (Protocol protocol : protocols) {
+ String prot = protocol.toString() + ":";
+ if (value.toLowerCase().startsWith(prot)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Attributes getEnforcedAttributes(String tagName) {
+ Attributes attrs = new Attributes();
+ TagName tag = TagName.valueOf(tagName);
+ if (enforcedAttributes.containsKey(tag)) {
+ Map<AttributeKey, AttributeValue> keyVals = enforcedAttributes.get(tag);
+ for (Map.Entry<AttributeKey, AttributeValue> entry : keyVals.entrySet()) {
+ attrs.put(entry.getKey().toString(), entry.getValue().toString());
+ }
+ }
+ return attrs;
+ }
+
+ // named types for config. All just hold strings, but here for my sanity.
+
+ static class TagName extends TypedValue {
+ TagName(String value) {
+ super(value);
+ }
+
+ static TagName valueOf(String value) {
+ return new TagName(value);
+ }
+ }
+
+ static class AttributeKey extends TypedValue {
+ AttributeKey(String value) {
+ super(value);
+ }
+
+ static AttributeKey valueOf(String value) {
+ return new AttributeKey(value);
+ }
+ }
+
+ static class AttributeValue extends TypedValue {
+ AttributeValue(String value) {
+ super(value);
+ }
+
+ static AttributeValue valueOf(String value) {
+ return new AttributeValue(value);
+ }
+ }
+
+ static class Protocol extends TypedValue {
+ Protocol(String value) {
+ super(value);
+ }
+
+ static Protocol valueOf(String value) {
+ return new Protocol(value);
+ }
+ }
+
+ abstract static class TypedValue {
+ private String value;
+
+ TypedValue(String value) {
+ Validate.notNull(value);
+ this.value = value;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((value == null) ? 0 : value.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ TypedValue other = (TypedValue) obj;
+ if (value == null) {
+ if (other.value != null) return false;
+ } else if (!value.equals(other.value)) return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+ }
+}
+
diff --git a/server/src/org/jsoup/safety/package-info.java b/server/src/org/jsoup/safety/package-info.java
new file mode 100644
index 0000000000..ac890f0607
--- /dev/null
+++ b/server/src/org/jsoup/safety/package-info.java
@@ -0,0 +1,4 @@
+/**
+ Contains the jsoup HTML cleaner, and whitelist definitions.
+ */
+package org.jsoup.safety;
diff --git a/server/src/org/jsoup/select/Collector.java b/server/src/org/jsoup/select/Collector.java
new file mode 100644
index 0000000000..8f01045768
--- /dev/null
+++ b/server/src/org/jsoup/select/Collector.java
@@ -0,0 +1,51 @@
+package org.jsoup.select;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+/**
+ * Collects a list of elements that match the supplied criteria.
+ *
+ * @author Jonathan Hedley
+ */
+public class Collector {
+
+ private Collector() {
+ }
+
+ /**
+ Build a list of elements, by visiting root and every descendant of root, and testing it against the evaluator.
+ @param eval Evaluator to test elements against
+ @param root root of tree to descend
+ @return list of matches; empty if none
+ */
+ public static Elements collect (Evaluator eval, Element root) {
+ Elements elements = new Elements();
+ new NodeTraversor(new Accumulator(root, elements, eval)).traverse(root);
+ return elements;
+ }
+
+ private static class Accumulator implements NodeVisitor {
+ private final Element root;
+ private final Elements elements;
+ private final Evaluator eval;
+
+ Accumulator(Element root, Elements elements, Evaluator eval) {
+ this.root = root;
+ this.elements = elements;
+ this.eval = eval;
+ }
+
+ public void head(Node node, int depth) {
+ if (node instanceof Element) {
+ Element el = (Element) node;
+ if (eval.matches(root, el))
+ elements.add(el);
+ }
+ }
+
+ public void tail(Node node, int depth) {
+ // void
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/CombiningEvaluator.java b/server/src/org/jsoup/select/CombiningEvaluator.java
new file mode 100644
index 0000000000..a31ed2636f
--- /dev/null
+++ b/server/src/org/jsoup/select/CombiningEvaluator.java
@@ -0,0 +1,94 @@
+package org.jsoup.select;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.nodes.Element;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Base combining (and, or) evaluator.
+ */
+abstract class CombiningEvaluator extends Evaluator {
+ final List<Evaluator> evaluators;
+
+ CombiningEvaluator() {
+ super();
+ evaluators = new ArrayList<Evaluator>();
+ }
+
+ CombiningEvaluator(Collection<Evaluator> evaluators) {
+ this();
+ this.evaluators.addAll(evaluators);
+ }
+
+ Evaluator rightMostEvaluator() {
+ return evaluators.size() > 0 ? evaluators.get(evaluators.size() - 1) : null;
+ }
+
+ void replaceRightMostEvaluator(Evaluator replacement) {
+ evaluators.set(evaluators.size() - 1, replacement);
+ }
+
+ static final class And extends CombiningEvaluator {
+ And(Collection<Evaluator> evaluators) {
+ super(evaluators);
+ }
+
+ And(Evaluator... evaluators) {
+ this(Arrays.asList(evaluators));
+ }
+
+ @Override
+ public boolean matches(Element root, Element node) {
+ for (Evaluator s : evaluators) {
+ if (!s.matches(root, node))
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtil.join(evaluators, " ");
+ }
+ }
+
+ static final class Or extends CombiningEvaluator {
+ /**
+ * Create a new Or evaluator. The initial evaluators are ANDed together and used as the first clause of the OR.
+ * @param evaluators initial OR clause (these are wrapped into an AND evaluator).
+ */
+ Or(Collection<Evaluator> evaluators) {
+ super();
+ if (evaluators.size() > 1)
+ this.evaluators.add(new And(evaluators));
+ else // 0 or 1
+ this.evaluators.addAll(evaluators);
+ }
+
+ Or() {
+ super();
+ }
+
+ public void add(Evaluator e) {
+ evaluators.add(e);
+ }
+
+ @Override
+ public boolean matches(Element root, Element node) {
+ for (Evaluator s : evaluators) {
+ if (s.matches(root, node))
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":or%s", evaluators);
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/Elements.java b/server/src/org/jsoup/select/Elements.java
new file mode 100644
index 0000000000..8302da1e53
--- /dev/null
+++ b/server/src/org/jsoup/select/Elements.java
@@ -0,0 +1,536 @@
+package org.jsoup.select;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+
+import java.util.*;
+
+/**
+ A list of {@link Element Elements}, with methods that act on every element in the list.
+ <p/>
+ To get an Elements object, use the {@link Element#select(String)} method.
+
+ @author Jonathan Hedley, jonathan@hedley.net */
+public class Elements implements List<Element>, Cloneable {
+ private List<Element> contents;
+
+ public Elements() {
+ contents = new ArrayList<Element>();
+ }
+
+ public Elements(int initialCapacity) {
+ contents = new ArrayList<Element>(initialCapacity);
+ }
+
+ public Elements(Collection<Element> elements) {
+ contents = new ArrayList<Element>(elements);
+ }
+
+ public Elements(List<Element> elements) {
+ contents = elements;
+ }
+
+ public Elements(Element... elements) {
+ this(Arrays.asList(elements));
+ }
+
+ @Override
+ public Elements clone() {
+ List<Element> elements = new ArrayList<Element>();
+
+ for(Element e : contents)
+ elements.add(e.clone());
+
+
+ return new Elements(elements);
+ }
+
+ // attribute methods
+ /**
+ Get an attribute value from the first matched element that has the attribute.
+ @param attributeKey The attribute key.
+ @return The attribute value from the first matched element that has the attribute.. If no elements were matched (isEmpty() == true),
+ or if the no elements have the attribute, returns empty string.
+ @see #hasAttr(String)
+ */
+ public String attr(String attributeKey) {
+ for (Element element : contents) {
+ if (element.hasAttr(attributeKey))
+ return element.attr(attributeKey);
+ }
+ return "";
+ }
+
+ /**
+ Checks if any of the matched elements have this attribute set.
+ @param attributeKey attribute key
+ @return true if any of the elements have the attribute; false if none do.
+ */
+ public boolean hasAttr(String attributeKey) {
+ for (Element element : contents) {
+ if (element.hasAttr(attributeKey))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Set an attribute on all matched elements.
+ * @param attributeKey attribute key
+ * @param attributeValue attribute value
+ * @return this
+ */
+ public Elements attr(String attributeKey, String attributeValue) {
+ for (Element element : contents) {
+ element.attr(attributeKey, attributeValue);
+ }
+ return this;
+ }
+
+ /**
+ * Remove an attribute from every matched element.
+ * @param attributeKey The attribute to remove.
+ * @return this (for chaining)
+ */
+ public Elements removeAttr(String attributeKey) {
+ for (Element element : contents) {
+ element.removeAttr(attributeKey);
+ }
+ return this;
+ }
+
+ /**
+ Add the class name to every matched element's {@code class} attribute.
+ @param className class name to add
+ @return this
+ */
+ public Elements addClass(String className) {
+ for (Element element : contents) {
+ element.addClass(className);
+ }
+ return this;
+ }
+
+ /**
+ Remove the class name from every matched element's {@code class} attribute, if present.
+ @param className class name to remove
+ @return this
+ */
+ public Elements removeClass(String className) {
+ for (Element element : contents) {
+ element.removeClass(className);
+ }
+ return this;
+ }
+
+ /**
+ Toggle the class name on every matched element's {@code class} attribute.
+ @param className class name to add if missing, or remove if present, from every element.
+ @return this
+ */
+ public Elements toggleClass(String className) {
+ for (Element element : contents) {
+ element.toggleClass(className);
+ }
+ return this;
+ }
+
+ /**
+ Determine if any of the matched elements have this class name set in their {@code class} attribute.
+ @param className class name to check for
+ @return true if any do, false if none do
+ */
+ public boolean hasClass(String className) {
+ for (Element element : contents) {
+ if (element.hasClass(className))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the form element's value of the first matched element.
+ * @return The form element's value, or empty if not set.
+ * @see Element#val()
+ */
+ public String val() {
+ if (size() > 0)
+ return first().val();
+ else
+ return "";
+ }
+
+ /**
+ * Set the form element's value in each of the matched elements.
+ * @param value The value to set into each matched element
+ * @return this (for chaining)
+ */
+ public Elements val(String value) {
+ for (Element element : contents)
+ element.val(value);
+ return this;
+ }
+
+ /**
+ * Get the combined text of all the matched elements.
+ * <p>
+ * Note that it is possible to get repeats if the matched elements contain both parent elements and their own
+ * children, as the Element.text() method returns the combined text of a parent and all its children.
+ * @return string of all text: unescaped and no HTML.
+ * @see Element#text()
+ */
+ public String text() {
+ StringBuilder sb = new StringBuilder();
+ for (Element element : contents) {
+ if (sb.length() != 0)
+ sb.append(" ");
+ sb.append(element.text());
+ }
+ return sb.toString();
+ }
+
+ public boolean hasText() {
+ for (Element element: contents) {
+ if (element.hasText())
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get the combined inner HTML of all matched elements.
+ * @return string of all element's inner HTML.
+ * @see #text()
+ * @see #outerHtml()
+ */
+ public String html() {
+ StringBuilder sb = new StringBuilder();
+ for (Element element : contents) {
+ if (sb.length() != 0)
+ sb.append("\n");
+ sb.append(element.html());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the combined outer HTML of all matched elements.
+ * @return string of all element's outer HTML.
+ * @see #text()
+ * @see #html()
+ */
+ public String outerHtml() {
+ StringBuilder sb = new StringBuilder();
+ for (Element element : contents) {
+ if (sb.length() != 0)
+ sb.append("\n");
+ sb.append(element.outerHtml());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the combined outer HTML of all matched elements. Alias of {@link #outerHtml()}.
+ * @return string of all element's outer HTML.
+ * @see #text()
+ * @see #html()
+ */
+ public String toString() {
+ return outerHtml();
+ }
+
+ /**
+ * Update the tag name of each matched element. For example, to change each {@code <i>} to a {@code <em>}, do
+ * {@code doc.select("i").tagName("em");}
+ * @param tagName the new tag name
+ * @return this, for chaining
+ * @see Element#tagName(String)
+ */
+ public Elements tagName(String tagName) {
+ for (Element element : contents) {
+ element.tagName(tagName);
+ }
+ return this;
+ }
+
+ /**
+ * Set the inner HTML of each matched element.
+ * @param html HTML to parse and set into each matched element.
+ * @return this, for chaining
+ * @see Element#html(String)
+ */
+ public Elements html(String html) {
+ for (Element element : contents) {
+ element.html(html);
+ }
+ return this;
+ }
+
+ /**
+ * Add the supplied HTML to the start of each matched element's inner HTML.
+ * @param html HTML to add inside each element, before the existing HTML
+ * @return this, for chaining
+ * @see Element#prepend(String)
+ */
+ public Elements prepend(String html) {
+ for (Element element : contents) {
+ element.prepend(html);
+ }
+ return this;
+ }
+
+ /**
+ * Add the supplied HTML to the end of each matched element's inner HTML.
+ * @param html HTML to add inside each element, after the existing HTML
+ * @return this, for chaining
+ * @see Element#append(String)
+ */
+ public Elements append(String html) {
+ for (Element element : contents) {
+ element.append(html);
+ }
+ return this;
+ }
+
+ /**
+ * Insert the supplied HTML before each matched element's outer HTML.
+ * @param html HTML to insert before each element
+ * @return this, for chaining
+ * @see Element#before(String)
+ */
+ public Elements before(String html) {
+ for (Element element : contents) {
+ element.before(html);
+ }
+ return this;
+ }
+
+ /**
+ * Insert the supplied HTML after each matched element's outer HTML.
+ * @param html HTML to insert after each element
+ * @return this, for chaining
+ * @see Element#after(String)
+ */
+ public Elements after(String html) {
+ for (Element element : contents) {
+ element.after(html);
+ }
+ return this;
+ }
+
+ /**
+ Wrap the supplied HTML around each matched elements. For example, with HTML
+ {@code <p><b>This</b> is <b>Jsoup</b></p>},
+ <code>doc.select("b").wrap("&lt;i&gt;&lt;/i&gt;");</code>
+ becomes {@code <p><i><b>This</b></i> is <i><b>jsoup</b></i></p>}
+ @param html HTML to wrap around each element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
+ @return this (for chaining)
+ @see Element#wrap
+ */
+ public Elements wrap(String html) {
+ Validate.notEmpty(html);
+ for (Element element : contents) {
+ element.wrap(html);
+ }
+ return this;
+ }
+
+ /**
+ * Removes the matched elements from the DOM, and moves their children up into their parents. This has the effect of
+ * dropping the elements but keeping their children.
+ * <p/>
+ * This is useful for e.g removing unwanted formatting elements but keeping their contents.
+ * <p/>
+ * E.g. with HTML: {@code <div><font>One</font> <font><a href="/">Two</a></font></div>}<br/>
+ * {@code doc.select("font").unwrap();}<br/>
+ * HTML = {@code <div>One <a href="/">Two</a></div>}
+ *
+ * @return this (for chaining)
+ * @see Node#unwrap
+ */
+ public Elements unwrap() {
+ for (Element element : contents) {
+ element.unwrap();
+ }
+ return this;
+ }
+
+ /**
+ * Empty (remove all child nodes from) each matched element. This is similar to setting the inner HTML of each
+ * element to nothing.
+ * <p>
+ * E.g. HTML: {@code <div><p>Hello <b>there</b></p> <p>now</p></div>}<br>
+ * <code>doc.select("p").empty();</code><br>
+ * HTML = {@code <div><p></p> <p></p></div>}
+ * @return this, for chaining
+ * @see Element#empty()
+ * @see #remove()
+ */
+ public Elements empty() {
+ for (Element element : contents) {
+ element.empty();
+ }
+ return this;
+ }
+
+ /**
+ * Remove each matched element from the DOM. This is similar to setting the outer HTML of each element to nothing.
+ * <p>
+ * E.g. HTML: {@code <div><p>Hello</p> <p>there</p> <img /></div>}<br>
+ * <code>doc.select("p").remove();</code><br>
+ * HTML = {@code <div> <img /></div>}
+ * <p>
+ * Note that this method should not be used to clean user-submitted HTML; rather, use {@link org.jsoup.safety.Cleaner} to clean HTML.
+ * @return this, for chaining
+ * @see Element#empty()
+ * @see #empty()
+ */
+ public Elements remove() {
+ for (Element element : contents) {
+ element.remove();
+ }
+ return this;
+ }
+
+ // filters
+
+ /**
+ * Find matching elements within this element list.
+ * @param query A {@link Selector} query
+ * @return the filtered list of elements, or an empty list if none match.
+ */
+ public Elements select(String query) {
+ return Selector.select(query, this);
+ }
+
+ /**
+ * Remove elements from this list that match the {@link Selector} query.
+ * <p>
+ * E.g. HTML: {@code <div class=logo>One</div> <div>Two</div>}<br>
+ * <code>Elements divs = doc.select("div").not("#logo");</code><br>
+ * Result: {@code divs: [<div>Two</div>]}
+ * <p>
+ * @param query the selector query whose results should be removed from these elements
+ * @return a new elements list that contains only the filtered results
+ */
+ public Elements not(String query) {
+ Elements out = Selector.select(query, this);
+ return Selector.filterOut(this, out);
+ }
+
+ /**
+ * Get the <i>nth</i> matched element as an Elements object.
+ * <p>
+ * See also {@link #get(int)} to retrieve an Element.
+ * @param index the (zero-based) index of the element in the list to retain
+ * @return Elements containing only the specified element, or, if that element did not exist, an empty list.
+ */
+ public Elements eq(int index) {
+ return contents.size() > index ? new Elements(get(index)) : new Elements();
+ }
+
+ /**
+ * Test if any of the matched elements match the supplied query.
+ * @param query A selector
+ * @return true if at least one element in the list matches the query.
+ */
+ public boolean is(String query) {
+ Elements children = select(query);
+ return !children.isEmpty();
+ }
+
+ /**
+ * Get all of the parents and ancestor elements of the matched elements.
+ * @return all of the parents and ancestor elements of the matched elements
+ */
+ public Elements parents() {
+ HashSet<Element> combo = new LinkedHashSet<Element>();
+ for (Element e: contents) {
+ combo.addAll(e.parents());
+ }
+ return new Elements(combo);
+ }
+
+ // list-like methods
+ /**
+ Get the first matched element.
+ @return The first matched element, or <code>null</code> if contents is empty;
+ */
+ public Element first() {
+ return contents.isEmpty() ? null : contents.get(0);
+ }
+
+ /**
+ Get the last matched element.
+ @return The last matched element, or <code>null</code> if contents is empty.
+ */
+ public Element last() {
+ return contents.isEmpty() ? null : contents.get(contents.size() - 1);
+ }
+
+ /**
+ * Perform a depth-first traversal on each of the selected elements.
+ * @param nodeVisitor the visitor callbacks to perform on each node
+ * @return this, for chaining
+ */
+ public Elements traverse(NodeVisitor nodeVisitor) {
+ Validate.notNull(nodeVisitor);
+ NodeTraversor traversor = new NodeTraversor(nodeVisitor);
+ for (Element el: contents) {
+ traversor.traverse(el);
+ }
+ return this;
+ }
+
+ // implements List<Element> delegates:
+ public int size() {return contents.size();}
+
+ public boolean isEmpty() {return contents.isEmpty();}
+
+ public boolean contains(Object o) {return contents.contains(o);}
+
+ public Iterator<Element> iterator() {return contents.iterator();}
+
+ public Object[] toArray() {return contents.toArray();}
+
+ public <T> T[] toArray(T[] a) {return contents.toArray(a);}
+
+ public boolean add(Element element) {return contents.add(element);}
+
+ public boolean remove(Object o) {return contents.remove(o);}
+
+ public boolean containsAll(Collection<?> c) {return contents.containsAll(c);}
+
+ public boolean addAll(Collection<? extends Element> c) {return contents.addAll(c);}
+
+ public boolean addAll(int index, Collection<? extends Element> c) {return contents.addAll(index, c);}
+
+ public boolean removeAll(Collection<?> c) {return contents.removeAll(c);}
+
+ public boolean retainAll(Collection<?> c) {return contents.retainAll(c);}
+
+ public void clear() {contents.clear();}
+
+ public boolean equals(Object o) {return contents.equals(o);}
+
+ public int hashCode() {return contents.hashCode();}
+
+ public Element get(int index) {return contents.get(index);}
+
+ public Element set(int index, Element element) {return contents.set(index, element);}
+
+ public void add(int index, Element element) {contents.add(index, element);}
+
+ public Element remove(int index) {return contents.remove(index);}
+
+ public int indexOf(Object o) {return contents.indexOf(o);}
+
+ public int lastIndexOf(Object o) {return contents.lastIndexOf(o);}
+
+ public ListIterator<Element> listIterator() {return contents.listIterator();}
+
+ public ListIterator<Element> listIterator(int index) {return contents.listIterator(index);}
+
+ public List<Element> subList(int fromIndex, int toIndex) {return contents.subList(fromIndex, toIndex);}
+}
diff --git a/server/src/org/jsoup/select/Evaluator.java b/server/src/org/jsoup/select/Evaluator.java
new file mode 100644
index 0000000000..16a083bd77
--- /dev/null
+++ b/server/src/org/jsoup/select/Evaluator.java
@@ -0,0 +1,454 @@
+package org.jsoup.select;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Element;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Evaluates that an element matches the selector.
+ */
+public abstract class Evaluator {
+ protected Evaluator() {
+ }
+
+ /**
+ * Test if the element meets the evaluator's requirements.
+ *
+ * @param root Root of the matching subtree
+ * @param element tested element
+ */
+ public abstract boolean matches(Element root, Element element);
+
+ /**
+ * Evaluator for tag name
+ */
+ public static final class Tag extends Evaluator {
+ private String tagName;
+
+ public Tag(String tagName) {
+ this.tagName = tagName;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return (element.tagName().equals(tagName));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s", tagName);
+ }
+ }
+
+ /**
+ * Evaluator for element id
+ */
+ public static final class Id extends Evaluator {
+ private String id;
+
+ public Id(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return (id.equals(element.id()));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("#%s", id);
+ }
+
+ }
+
+ /**
+ * Evaluator for element class
+ */
+ public static final class Class extends Evaluator {
+ private String className;
+
+ public Class(String className) {
+ this.className = className;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return (element.hasClass(className));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(".%s", className);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name matching
+ */
+ public static final class Attribute extends Evaluator {
+ private String key;
+
+ public Attribute(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s]", key);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name prefix matching
+ */
+ public static final class AttributeStarting extends Evaluator {
+ private String keyPrefix;
+
+ public AttributeStarting(String keyPrefix) {
+ this.keyPrefix = keyPrefix;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ List<org.jsoup.nodes.Attribute> values = element.attributes().asList();
+ for (org.jsoup.nodes.Attribute attribute : values) {
+ if (attribute.getKey().startsWith(keyPrefix))
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[^%s]", keyPrefix);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name/value matching
+ */
+ public static final class AttributeWithValue extends AttributeKeyPair {
+ public AttributeWithValue(String key, String value) {
+ super(key, value);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key) && value.equalsIgnoreCase(element.attr(key));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s=%s]", key, value);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name != value matching
+ */
+ public static final class AttributeWithValueNot extends AttributeKeyPair {
+ public AttributeWithValueNot(String key, String value) {
+ super(key, value);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return !value.equalsIgnoreCase(element.attr(key));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s!=%s]", key, value);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name/value matching (value prefix)
+ */
+ public static final class AttributeWithValueStarting extends AttributeKeyPair {
+ public AttributeWithValueStarting(String key, String value) {
+ super(key, value);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key) && element.attr(key).toLowerCase().startsWith(value); // value is lower case already
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s^=%s]", key, value);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name/value matching (value ending)
+ */
+ public static final class AttributeWithValueEnding extends AttributeKeyPair {
+ public AttributeWithValueEnding(String key, String value) {
+ super(key, value);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key) && element.attr(key).toLowerCase().endsWith(value); // value is lower case
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s$=%s]", key, value);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name/value matching (value containing)
+ */
+ public static final class AttributeWithValueContaining extends AttributeKeyPair {
+ public AttributeWithValueContaining(String key, String value) {
+ super(key, value);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key) && element.attr(key).toLowerCase().contains(value); // value is lower case
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s*=%s]", key, value);
+ }
+
+ }
+
+ /**
+ * Evaluator for attribute name/value matching (value regex matching)
+ */
+ public static final class AttributeWithValueMatching extends Evaluator {
+ String key;
+ Pattern pattern;
+
+ public AttributeWithValueMatching(String key, Pattern pattern) {
+ this.key = key.trim().toLowerCase();
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.hasAttr(key) && pattern.matcher(element.attr(key)).find();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[%s~=%s]", key, pattern.toString());
+ }
+
+ }
+
+ /**
+ * Abstract evaluator for attribute name/value matching
+ */
+ public abstract static class AttributeKeyPair extends Evaluator {
+ String key;
+ String value;
+
+ public AttributeKeyPair(String key, String value) {
+ Validate.notEmpty(key);
+ Validate.notEmpty(value);
+
+ this.key = key.trim().toLowerCase();
+ this.value = value.trim().toLowerCase();
+ }
+ }
+
+ /**
+ * Evaluator for any / all element matching
+ */
+ public static final class AllElements extends Evaluator {
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "*";
+ }
+ }
+
+ /**
+ * Evaluator for matching by sibling index number (e < idx)
+ */
+ public static final class IndexLessThan extends IndexEvaluator {
+ public IndexLessThan(int index) {
+ super(index);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.elementSiblingIndex() < index;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":lt(%d)", index);
+ }
+
+ }
+
+ /**
+ * Evaluator for matching by sibling index number (e > idx)
+ */
+ public static final class IndexGreaterThan extends IndexEvaluator {
+ public IndexGreaterThan(int index) {
+ super(index);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.elementSiblingIndex() > index;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":gt(%d)", index);
+ }
+
+ }
+
+ /**
+ * Evaluator for matching by sibling index number (e = idx)
+ */
+ public static final class IndexEquals extends IndexEvaluator {
+ public IndexEquals(int index) {
+ super(index);
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return element.elementSiblingIndex() == index;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":eq(%d)", index);
+ }
+
+ }
+
+ /**
+ * Abstract evaluator for sibling index matching
+ *
+ * @author ant
+ */
+ public abstract static class IndexEvaluator extends Evaluator {
+ int index;
+
+ public IndexEvaluator(int index) {
+ this.index = index;
+ }
+ }
+
+ /**
+ * Evaluator for matching Element (and its descendants) text
+ */
+ public static final class ContainsText extends Evaluator {
+ private String searchText;
+
+ public ContainsText(String searchText) {
+ this.searchText = searchText.toLowerCase();
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return (element.text().toLowerCase().contains(searchText));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":contains(%s", searchText);
+ }
+ }
+
+ /**
+ * Evaluator for matching Element's own text
+ */
+ public static final class ContainsOwnText extends Evaluator {
+ private String searchText;
+
+ public ContainsOwnText(String searchText) {
+ this.searchText = searchText.toLowerCase();
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ return (element.ownText().toLowerCase().contains(searchText));
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":containsOwn(%s", searchText);
+ }
+ }
+
+ /**
+ * Evaluator for matching Element (and its descendants) text with regex
+ */
+ public static final class Matches extends Evaluator {
+ private Pattern pattern;
+
+ public Matches(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ Matcher m = pattern.matcher(element.text());
+ return m.find();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":matches(%s", pattern);
+ }
+ }
+
+ /**
+ * Evaluator for matching Element's own text with regex
+ */
+ public static final class MatchesOwn extends Evaluator {
+ private Pattern pattern;
+
+ public MatchesOwn(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ @Override
+ public boolean matches(Element root, Element element) {
+ Matcher m = pattern.matcher(element.ownText());
+ return m.find();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(":matchesOwn(%s", pattern);
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/NodeTraversor.java b/server/src/org/jsoup/select/NodeTraversor.java
new file mode 100644
index 0000000000..9bb081e56c
--- /dev/null
+++ b/server/src/org/jsoup/select/NodeTraversor.java
@@ -0,0 +1,47 @@
+package org.jsoup.select;
+
+import org.jsoup.nodes.Node;
+
+/**
+ * Depth-first node traversor. Use to iterate through all nodes under and including the specified root node.
+ * <p/>
+ * This implementation does not use recursion, so a deep DOM does not risk blowing the stack.
+ */
+public class NodeTraversor {
+ private NodeVisitor visitor;
+
+ /**
+ * Create a new traversor.
+ * @param visitor a class implementing the {@link NodeVisitor} interface, to be called when visiting each node.
+ */
+ public NodeTraversor(NodeVisitor visitor) {
+ this.visitor = visitor;
+ }
+
+ /**
+ * Start a depth-first traverse of the root and all of its descendants.
+ * @param root the root node point to traverse.
+ */
+ public void traverse(Node root) {
+ Node node = root;
+ int depth = 0;
+
+ while (node != null) {
+ visitor.head(node, depth);
+ if (node.childNodes().size() > 0) {
+ node = node.childNode(0);
+ depth++;
+ } else {
+ while (node.nextSibling() == null && depth > 0) {
+ visitor.tail(node, depth);
+ node = node.parent();
+ depth--;
+ }
+ visitor.tail(node, depth);
+ if (node == root)
+ break;
+ node = node.nextSibling();
+ }
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/NodeVisitor.java b/server/src/org/jsoup/select/NodeVisitor.java
new file mode 100644
index 0000000000..20112e8d29
--- /dev/null
+++ b/server/src/org/jsoup/select/NodeVisitor.java
@@ -0,0 +1,30 @@
+package org.jsoup.select;
+
+import org.jsoup.nodes.Node;
+
+/**
+ * Node visitor interface. Provide an implementing class to {@link NodeTraversor} to iterate through nodes.
+ * <p/>
+ * This interface provides two methods, {@code head} and {@code tail}. The head method is called when the node is first
+ * seen, and the tail method when all of the node's children have been visited. As an example, head can be used to
+ * create a start tag for a node, and tail to create the end tag.
+ */
+public interface NodeVisitor {
+ /**
+ * Callback for when a node is first visited.
+ *
+ * @param node the node being visited.
+ * @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node
+ * of that will have depth 1.
+ */
+ public void head(Node node, int depth);
+
+ /**
+ * Callback for when a node is last visited, after all of its descendants have been visited.
+ *
+ * @param node the node being visited.
+ * @param depth the depth of the node, relative to the root node. E.g., the root node has depth 0, and a child node
+ * of that will have depth 1.
+ */
+ public void tail(Node node, int depth);
+}
diff --git a/server/src/org/jsoup/select/QueryParser.java b/server/src/org/jsoup/select/QueryParser.java
new file mode 100644
index 0000000000..d3cc36f91c
--- /dev/null
+++ b/server/src/org/jsoup/select/QueryParser.java
@@ -0,0 +1,293 @@
+package org.jsoup.select;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.jsoup.helper.StringUtil;
+import org.jsoup.helper.Validate;
+import org.jsoup.parser.TokenQueue;
+
+/**
+ * Parses a CSS selector into an Evaluator tree.
+ */
+class QueryParser {
+ private final static String[] combinators = {",", ">", "+", "~", " "};
+
+ private TokenQueue tq;
+ private String query;
+ private List<Evaluator> evals = new ArrayList<Evaluator>();
+
+ /**
+ * Create a new QueryParser.
+ * @param query CSS query
+ */
+ private QueryParser(String query) {
+ this.query = query;
+ this.tq = new TokenQueue(query);
+ }
+
+ /**
+ * Parse a CSS query into an Evaluator.
+ * @param query CSS query
+ * @return Evaluator
+ */
+ public static Evaluator parse(String query) {
+ QueryParser p = new QueryParser(query);
+ return p.parse();
+ }
+
+ /**
+ * Parse the query
+ * @return Evaluator
+ */
+ Evaluator parse() {
+ tq.consumeWhitespace();
+
+ if (tq.matchesAny(combinators)) { // if starts with a combinator, use root as elements
+ evals.add(new StructuralEvaluator.Root());
+ combinator(tq.consume());
+ } else {
+ findElements();
+ }
+
+ while (!tq.isEmpty()) {
+ // hierarchy and extras
+ boolean seenWhite = tq.consumeWhitespace();
+
+ if (tq.matchesAny(combinators)) {
+ combinator(tq.consume());
+ } else if (seenWhite) {
+ combinator(' ');
+ } else { // E.class, E#id, E[attr] etc. AND
+ findElements(); // take next el, #. etc off queue
+ }
+ }
+
+ if (evals.size() == 1)
+ return evals.get(0);
+
+ return new CombiningEvaluator.And(evals);
+ }
+
+ private void combinator(char combinator) {
+ tq.consumeWhitespace();
+ String subQuery = consumeSubQuery(); // support multi > childs
+
+ Evaluator rootEval; // the new topmost evaluator
+ Evaluator currentEval; // the evaluator the new eval will be combined to. could be root, or rightmost or.
+ Evaluator newEval = parse(subQuery); // the evaluator to add into target evaluator
+ boolean replaceRightMost = false;
+
+ if (evals.size() == 1) {
+ rootEval = currentEval = evals.get(0);
+ // make sure OR (,) has precedence:
+ if (rootEval instanceof CombiningEvaluator.Or && combinator != ',') {
+ currentEval = ((CombiningEvaluator.Or) currentEval).rightMostEvaluator();
+ replaceRightMost = true;
+ }
+ }
+ else {
+ rootEval = currentEval = new CombiningEvaluator.And(evals);
+ }
+ evals.clear();
+
+ // for most combinators: change the current eval into an AND of the current eval and the new eval
+ if (combinator == '>')
+ currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.ImmediateParent(currentEval));
+ else if (combinator == ' ')
+ currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.Parent(currentEval));
+ else if (combinator == '+')
+ currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.ImmediatePreviousSibling(currentEval));
+ else if (combinator == '~')
+ currentEval = new CombiningEvaluator.And(newEval, new StructuralEvaluator.PreviousSibling(currentEval));
+ else if (combinator == ',') { // group or.
+ CombiningEvaluator.Or or;
+ if (currentEval instanceof CombiningEvaluator.Or) {
+ or = (CombiningEvaluator.Or) currentEval;
+ or.add(newEval);
+ } else {
+ or = new CombiningEvaluator.Or();
+ or.add(currentEval);
+ or.add(newEval);
+ }
+ currentEval = or;
+ }
+ else
+ throw new Selector.SelectorParseException("Unknown combinator: " + combinator);
+
+ if (replaceRightMost)
+ ((CombiningEvaluator.Or) rootEval).replaceRightMostEvaluator(currentEval);
+ else rootEval = currentEval;
+ evals.add(rootEval);
+ }
+
+ private String consumeSubQuery() {
+ StringBuilder sq = new StringBuilder();
+ while (!tq.isEmpty()) {
+ if (tq.matches("("))
+ sq.append("(").append(tq.chompBalanced('(', ')')).append(")");
+ else if (tq.matches("["))
+ sq.append("[").append(tq.chompBalanced('[', ']')).append("]");
+ else if (tq.matchesAny(combinators))
+ break;
+ else
+ sq.append(tq.consume());
+ }
+ return sq.toString();
+ }
+
+ private void findElements() {
+ if (tq.matchChomp("#"))
+ byId();
+ else if (tq.matchChomp("."))
+ byClass();
+ else if (tq.matchesWord())
+ byTag();
+ else if (tq.matches("["))
+ byAttribute();
+ else if (tq.matchChomp("*"))
+ allElements();
+ else if (tq.matchChomp(":lt("))
+ indexLessThan();
+ else if (tq.matchChomp(":gt("))
+ indexGreaterThan();
+ else if (tq.matchChomp(":eq("))
+ indexEquals();
+ else if (tq.matches(":has("))
+ has();
+ else if (tq.matches(":contains("))
+ contains(false);
+ else if (tq.matches(":containsOwn("))
+ contains(true);
+ else if (tq.matches(":matches("))
+ matches(false);
+ else if (tq.matches(":matchesOwn("))
+ matches(true);
+ else if (tq.matches(":not("))
+ not();
+ else // unhandled
+ throw new Selector.SelectorParseException("Could not parse query '%s': unexpected token at '%s'", query, tq.remainder());
+
+ }
+
+ private void byId() {
+ String id = tq.consumeCssIdentifier();
+ Validate.notEmpty(id);
+ evals.add(new Evaluator.Id(id));
+ }
+
+ private void byClass() {
+ String className = tq.consumeCssIdentifier();
+ Validate.notEmpty(className);
+ evals.add(new Evaluator.Class(className.trim().toLowerCase()));
+ }
+
+ private void byTag() {
+ String tagName = tq.consumeElementSelector();
+ Validate.notEmpty(tagName);
+
+ // namespaces: if element name is "abc:def", selector must be "abc|def", so flip:
+ if (tagName.contains("|"))
+ tagName = tagName.replace("|", ":");
+
+ evals.add(new Evaluator.Tag(tagName.trim().toLowerCase()));
+ }
+
+ private void byAttribute() {
+ TokenQueue cq = new TokenQueue(tq.chompBalanced('[', ']')); // content queue
+ String key = cq.consumeToAny("=", "!=", "^=", "$=", "*=", "~="); // eq, not, start, end, contain, match, (no val)
+ Validate.notEmpty(key);
+ cq.consumeWhitespace();
+
+ if (cq.isEmpty()) {
+ if (key.startsWith("^"))
+ evals.add(new Evaluator.AttributeStarting(key.substring(1)));
+ else
+ evals.add(new Evaluator.Attribute(key));
+ } else {
+ if (cq.matchChomp("="))
+ evals.add(new Evaluator.AttributeWithValue(key, cq.remainder()));
+
+ else if (cq.matchChomp("!="))
+ evals.add(new Evaluator.AttributeWithValueNot(key, cq.remainder()));
+
+ else if (cq.matchChomp("^="))
+ evals.add(new Evaluator.AttributeWithValueStarting(key, cq.remainder()));
+
+ else if (cq.matchChomp("$="))
+ evals.add(new Evaluator.AttributeWithValueEnding(key, cq.remainder()));
+
+ else if (cq.matchChomp("*="))
+ evals.add(new Evaluator.AttributeWithValueContaining(key, cq.remainder()));
+
+ else if (cq.matchChomp("~="))
+ evals.add(new Evaluator.AttributeWithValueMatching(key, Pattern.compile(cq.remainder())));
+ else
+ throw new Selector.SelectorParseException("Could not parse attribute query '%s': unexpected token at '%s'", query, cq.remainder());
+ }
+ }
+
+ private void allElements() {
+ evals.add(new Evaluator.AllElements());
+ }
+
+ // pseudo selectors :lt, :gt, :eq
+ private void indexLessThan() {
+ evals.add(new Evaluator.IndexLessThan(consumeIndex()));
+ }
+
+ private void indexGreaterThan() {
+ evals.add(new Evaluator.IndexGreaterThan(consumeIndex()));
+ }
+
+ private void indexEquals() {
+ evals.add(new Evaluator.IndexEquals(consumeIndex()));
+ }
+
+ private int consumeIndex() {
+ String indexS = tq.chompTo(")").trim();
+ Validate.isTrue(StringUtil.isNumeric(indexS), "Index must be numeric");
+ return Integer.parseInt(indexS);
+ }
+
+ // pseudo selector :has(el)
+ private void has() {
+ tq.consume(":has");
+ String subQuery = tq.chompBalanced('(', ')');
+ Validate.notEmpty(subQuery, ":has(el) subselect must not be empty");
+ evals.add(new StructuralEvaluator.Has(parse(subQuery)));
+ }
+
+ // pseudo selector :contains(text), containsOwn(text)
+ private void contains(boolean own) {
+ tq.consume(own ? ":containsOwn" : ":contains");
+ String searchText = TokenQueue.unescape(tq.chompBalanced('(', ')'));
+ Validate.notEmpty(searchText, ":contains(text) query must not be empty");
+ if (own)
+ evals.add(new Evaluator.ContainsOwnText(searchText));
+ else
+ evals.add(new Evaluator.ContainsText(searchText));
+ }
+
+ // :matches(regex), matchesOwn(regex)
+ private void matches(boolean own) {
+ tq.consume(own ? ":matchesOwn" : ":matches");
+ String regex = tq.chompBalanced('(', ')'); // don't unescape, as regex bits will be escaped
+ Validate.notEmpty(regex, ":matches(regex) query must not be empty");
+
+ if (own)
+ evals.add(new Evaluator.MatchesOwn(Pattern.compile(regex)));
+ else
+ evals.add(new Evaluator.Matches(Pattern.compile(regex)));
+ }
+
+ // :not(selector)
+ private void not() {
+ tq.consume(":not");
+ String subQuery = tq.chompBalanced('(', ')');
+ Validate.notEmpty(subQuery, ":not(selector) subselect must not be empty");
+
+ evals.add(new StructuralEvaluator.Not(parse(subQuery)));
+ }
+}
diff --git a/server/src/org/jsoup/select/Selector.java b/server/src/org/jsoup/select/Selector.java
new file mode 100644
index 0000000000..8fc6286798
--- /dev/null
+++ b/server/src/org/jsoup/select/Selector.java
@@ -0,0 +1,126 @@
+package org.jsoup.select;
+
+import org.jsoup.helper.Validate;
+import org.jsoup.nodes.Element;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+/**
+ * CSS-like element selector, that finds elements matching a query.
+ * <p/>
+ * <h2>Selector syntax</h2>
+ * A selector is a chain of simple selectors, separated by combinators. Selectors are case insensitive (including against
+ * elements, attributes, and attribute values).
+ * <p/>
+ * The universal selector (*) is implicit when no element selector is supplied (i.e. {@code *.header} and {@code .header}
+ * is equivalent).
+ * <p/>
+ * <table>
+ * <tr><th>Pattern</th><th>Matches</th><th>Example</th></tr>
+ * <tr><td><code>*</code></td><td>any element</td><td><code>*</code></td></tr>
+ * <tr><td><code>tag</code></td><td>elements with the given tag name</td><td><code>div</code></td></tr>
+ * <tr><td><code>ns|E</code></td><td>elements of type E in the namespace <i>ns</i></td><td><code>fb|name</code> finds <code>&lt;fb:name></code> elements</td></tr>
+ * <tr><td><code>#id</code></td><td>elements with attribute ID of "id"</td><td><code>div#wrap</code>, <code>#logo</code></td></tr>
+ * <tr><td><code>.class</code></td><td>elements with a class name of "class"</td><td><code>div.left</code>, <code>.result</code></td></tr>
+ * <tr><td><code>[attr]</code></td><td>elements with an attribute named "attr" (with any value)</td><td><code>a[href]</code>, <code>[title]</code></td></tr>
+ * <tr><td><code>[^attrPrefix]</code></td><td>elements with an attribute name starting with "attrPrefix". Use to find elements with HTML5 datasets</td><td><code>[^data-]</code>, <code>div[^data-]</code></td></tr>
+ * <tr><td><code>[attr=val]</code></td><td>elements with an attribute named "attr", and value equal to "val"</td><td><code>img[width=500]</code>, <code>a[rel=nofollow]</code></td></tr>
+ * <tr><td><code>[attr^=valPrefix]</code></td><td>elements with an attribute named "attr", and value starting with "valPrefix"</td><td><code>a[href^=http:]</code></code></td></tr>
+ * <tr><td><code>[attr$=valSuffix]</code></td><td>elements with an attribute named "attr", and value ending with "valSuffix"</td><td><code>img[src$=.png]</code></td></tr>
+ * <tr><td><code>[attr*=valContaining]</code></td><td>elements with an attribute named "attr", and value containing "valContaining"</td><td><code>a[href*=/search/]</code></td></tr>
+ * <tr><td><code>[attr~=<em>regex</em>]</code></td><td>elements with an attribute named "attr", and value matching the regular expression</td><td><code>img[src~=(?i)\\.(png|jpe?g)]</code></td></tr>
+ * <tr><td></td><td>The above may be combined in any order</td><td><code>div.header[title]</code></td></tr>
+ * <tr><td><td colspan="3"><h3>Combinators</h3></td></tr>
+ * <tr><td><code>E F</code></td><td>an F element descended from an E element</td><td><code>div a</code>, <code>.logo h1</code></td></tr>
+ * <tr><td><code>E > F</code></td><td>an F direct child of E</td><td><code>ol > li</code></td></tr>
+ * <tr><td><code>E + F</code></td><td>an F element immediately preceded by sibling E</td><td><code>li + li</code>, <code>div.head + div</code></td></tr>
+ * <tr><td><code>E ~ F</code></td><td>an F element preceded by sibling E</td><td><code>h1 ~ p</code></td></tr>
+ * <tr><td><code>E, F, G</code></td><td>all matching elements E, F, or G</td><td><code>a[href], div, h3</code></td></tr>
+ * <tr><td><td colspan="3"><h3>Pseudo selectors</h3></td></tr>
+ * <tr><td><code>:lt(<em>n</em>)</code></td><td>elements whose sibling index is less than <em>n</em></td><td><code>td:lt(3)</code> finds the first 2 cells of each row</td></tr>
+ * <tr><td><code>:gt(<em>n</em>)</code></td><td>elements whose sibling index is greater than <em>n</em></td><td><code>td:gt(1)</code> finds cells after skipping the first two</td></tr>
+ * <tr><td><code>:eq(<em>n</em>)</code></td><td>elements whose sibling index is equal to <em>n</em></td><td><code>td:eq(0)</code> finds the first cell of each row</td></tr>
+ * <tr><td><code>:has(<em>selector</em>)</code></td><td>elements that contains at least one element matching the <em>selector</em></td><td><code>div:has(p)</code> finds divs that contain p elements </td></tr>
+ * <tr><td><code>:not(<em>selector</em>)</code></td><td>elements that do not match the <em>selector</em>. See also {@link Elements#not(String)}</td><td><code>div:not(.logo)</code> finds all divs that do not have the "logo" class.<br /><code>div:not(:has(div))</code> finds divs that do not contain divs.</code></td></tr>
+ * <tr><td><code>:contains(<em>text</em>)</code></td><td>elements that contains the specified text. The search is case insensitive. The text may appear in the found element, or any of its descendants.</td><td><code>p:contains(jsoup)</code> finds p elements containing the text "jsoup".</td></tr>
+ * <tr><td><code>:matches(<em>regex</em>)</code></td><td>elements whose text matches the specified regular expression. The text may appear in the found element, or any of its descendants.</td><td><code>td:matches(\\d+)</code> finds table cells containing digits. <code>div:matches((?i)login)</code> finds divs containing the text, case insensitively.</td></tr>
+ * <tr><td><code>:containsOwn(<em>text</em>)</code></td><td>elements that directly contains the specified text. The search is case insensitive. The text must appear in the found element, not any of its descendants.</td><td><code>p:containsOwn(jsoup)</code> finds p elements with own text "jsoup".</td></tr>
+ * <tr><td><code>:matchesOwn(<em>regex</em>)</code></td><td>elements whose own text matches the specified regular expression. The text must appear in the found element, not any of its descendants.</td><td><code>td:matchesOwn(\\d+)</code> finds table cells directly containing digits. <code>div:matchesOwn((?i)login)</code> finds divs containing the text, case insensitively.</td></tr>
+ * <tr><td></td><td>The above may be combined in any order and with other selectors</td><td><code>.light:contains(name):eq(0)</code></td></tr>
+ * </table>
+ *
+ * @author Jonathan Hedley, jonathan@hedley.net
+ * @see Element#select(String)
+ */
+public class Selector {
+ private final Evaluator evaluator;
+ private final Element root;
+
+ private Selector(String query, Element root) {
+ Validate.notNull(query);
+ query = query.trim();
+ Validate.notEmpty(query);
+ Validate.notNull(root);
+
+ this.evaluator = QueryParser.parse(query);
+
+ this.root = root;
+ }
+
+ /**
+ * Find elements matching selector.
+ *
+ * @param query CSS selector
+ * @param root root element to descend into
+ * @return matching elements, empty if not
+ */
+ public static Elements select(String query, Element root) {
+ return new Selector(query, root).select();
+ }
+
+ /**
+ * Find elements matching selector.
+ *
+ * @param query CSS selector
+ * @param roots root elements to descend into
+ * @return matching elements, empty if not
+ */
+ public static Elements select(String query, Iterable<Element> roots) {
+ Validate.notEmpty(query);
+ Validate.notNull(roots);
+ LinkedHashSet<Element> elements = new LinkedHashSet<Element>();
+
+ for (Element root : roots) {
+ elements.addAll(select(query, root));
+ }
+ return new Elements(elements);
+ }
+
+ private Elements select() {
+ return Collector.collect(evaluator, root);
+ }
+
+ // exclude set. package open so that Elements can implement .not() selector.
+ static Elements filterOut(Collection<Element> elements, Collection<Element> outs) {
+ Elements output = new Elements();
+ for (Element el : elements) {
+ boolean found = false;
+ for (Element out : outs) {
+ if (el.equals(out)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ output.add(el);
+ }
+ return output;
+ }
+
+ public static class SelectorParseException extends IllegalStateException {
+ public SelectorParseException(String msg, Object... params) {
+ super(String.format(msg, params));
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/StructuralEvaluator.java b/server/src/org/jsoup/select/StructuralEvaluator.java
new file mode 100644
index 0000000000..69e8a62e58
--- /dev/null
+++ b/server/src/org/jsoup/select/StructuralEvaluator.java
@@ -0,0 +1,132 @@
+package org.jsoup.select;
+
+import org.jsoup.nodes.Element;
+
+/**
+ * Base structural evaluator.
+ */
+abstract class StructuralEvaluator extends Evaluator {
+ Evaluator evaluator;
+
+ static class Root extends Evaluator {
+ public boolean matches(Element root, Element element) {
+ return root == element;
+ }
+ }
+
+ static class Has extends StructuralEvaluator {
+ public Has(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element element) {
+ for (Element e : element.getAllElements()) {
+ if (e != element && evaluator.matches(root, e))
+ return true;
+ }
+ return false;
+ }
+
+ public String toString() {
+ return String.format(":has(%s)", evaluator);
+ }
+ }
+
+ static class Not extends StructuralEvaluator {
+ public Not(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element node) {
+ return !evaluator.matches(root, node);
+ }
+
+ public String toString() {
+ return String.format(":not%s", evaluator);
+ }
+ }
+
+ static class Parent extends StructuralEvaluator {
+ public Parent(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element element) {
+ if (root == element)
+ return false;
+
+ Element parent = element.parent();
+ while (parent != root) {
+ if (evaluator.matches(root, parent))
+ return true;
+ parent = parent.parent();
+ }
+ return false;
+ }
+
+ public String toString() {
+ return String.format(":parent%s", evaluator);
+ }
+ }
+
+ static class ImmediateParent extends StructuralEvaluator {
+ public ImmediateParent(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element element) {
+ if (root == element)
+ return false;
+
+ Element parent = element.parent();
+ return parent != null && evaluator.matches(root, parent);
+ }
+
+ public String toString() {
+ return String.format(":ImmediateParent%s", evaluator);
+ }
+ }
+
+ static class PreviousSibling extends StructuralEvaluator {
+ public PreviousSibling(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element element) {
+ if (root == element)
+ return false;
+
+ Element prev = element.previousElementSibling();
+
+ while (prev != null) {
+ if (evaluator.matches(root, prev))
+ return true;
+
+ prev = prev.previousElementSibling();
+ }
+ return false;
+ }
+
+ public String toString() {
+ return String.format(":prev*%s", evaluator);
+ }
+ }
+
+ static class ImmediatePreviousSibling extends StructuralEvaluator {
+ public ImmediatePreviousSibling(Evaluator evaluator) {
+ this.evaluator = evaluator;
+ }
+
+ public boolean matches(Element root, Element element) {
+ if (root == element)
+ return false;
+
+ Element prev = element.previousElementSibling();
+ return prev != null && evaluator.matches(root, prev);
+ }
+
+ public String toString() {
+ return String.format(":prev%s", evaluator);
+ }
+ }
+}
diff --git a/server/src/org/jsoup/select/package-info.java b/server/src/org/jsoup/select/package-info.java
new file mode 100644
index 0000000000..a6e6a2fa0f
--- /dev/null
+++ b/server/src/org/jsoup/select/package-info.java
@@ -0,0 +1,4 @@
+/**
+ Packages to support the CSS-style element selector.
+ */
+package org.jsoup.select; \ No newline at end of file