From e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Aug 2012 18:34:33 +0300 Subject: Moved server files to a server src folder (#9299) --- server/src/com/vaadin/Application.java | 2426 +++++++++ .../RootRequiresMoreInformationException.java | 25 + server/src/com/vaadin/Vaadin.gwt.xml | 85 + server/src/com/vaadin/Version.java | 74 + .../src/com/vaadin/annotations/AutoGenerated.java | 18 + server/src/com/vaadin/annotations/EagerInit.java | 30 + server/src/com/vaadin/annotations/JavaScript.java | 41 + server/src/com/vaadin/annotations/StyleSheet.java | 38 + server/src/com/vaadin/annotations/Theme.java | 24 + server/src/com/vaadin/annotations/Widgetset.java | 25 + server/src/com/vaadin/annotations/package.html | 12 + server/src/com/vaadin/data/Buffered.java | 280 + .../src/com/vaadin/data/BufferedValidatable.java | 35 + server/src/com/vaadin/data/Collapsible.java | 68 + server/src/com/vaadin/data/Container.java | 1105 ++++ server/src/com/vaadin/data/Item.java | 180 + server/src/com/vaadin/data/Property.java | 402 ++ server/src/com/vaadin/data/Validatable.java | 110 + server/src/com/vaadin/data/Validator.java | 175 + .../com/vaadin/data/fieldgroup/BeanFieldGroup.java | 157 + server/src/com/vaadin/data/fieldgroup/Caption.java | 15 + .../fieldgroup/DefaultFieldGroupFieldFactory.java | 157 + .../src/com/vaadin/data/fieldgroup/FieldGroup.java | 978 ++++ .../data/fieldgroup/FieldGroupFieldFactory.java | 31 + .../src/com/vaadin/data/fieldgroup/PropertyId.java | 15 + server/src/com/vaadin/data/package.html | 49 + .../vaadin/data/util/AbstractBeanContainer.java | 856 +++ .../com/vaadin/data/util/AbstractContainer.java | 251 + .../data/util/AbstractInMemoryContainer.java | 941 ++++ .../src/com/vaadin/data/util/AbstractProperty.java | 226 + server/src/com/vaadin/data/util/BeanContainer.java | 168 + server/src/com/vaadin/data/util/BeanItem.java | 269 + .../com/vaadin/data/util/BeanItemContainer.java | 241 + .../data/util/ContainerHierarchicalWrapper.java | 792 +++ .../vaadin/data/util/ContainerOrderedWrapper.java | 644 +++ .../com/vaadin/data/util/DefaultItemSorter.java | 210 + .../com/vaadin/data/util/FilesystemContainer.java | 918 ++++ .../vaadin/data/util/HierarchicalContainer.java | 814 +++ .../util/HierarchicalContainerOrderedWrapper.java | 70 + .../src/com/vaadin/data/util/IndexedContainer.java | 1109 ++++ server/src/com/vaadin/data/util/ItemSorter.java | 57 + server/src/com/vaadin/data/util/ListSet.java | 264 + .../src/com/vaadin/data/util/MethodProperty.java | 784 +++ .../vaadin/data/util/MethodPropertyDescriptor.java | 134 + .../com/vaadin/data/util/NestedMethodProperty.java | 257 + .../vaadin/data/util/NestedPropertyDescriptor.java | 60 + .../src/com/vaadin/data/util/ObjectProperty.java | 141 + .../com/vaadin/data/util/PropertyFormatter.java | 245 + .../src/com/vaadin/data/util/PropertysetItem.java | 340 ++ .../src/com/vaadin/data/util/QueryContainer.java | 675 +++ .../src/com/vaadin/data/util/TextFileProperty.java | 144 + .../data/util/TransactionalPropertyWrapper.java | 114 + .../vaadin/data/util/VaadinPropertyDescriptor.java | 43 + .../com/vaadin/data/util/converter/Converter.java | 159 + .../data/util/converter/ConverterFactory.java | 23 + .../vaadin/data/util/converter/ConverterUtil.java | 168 + .../data/util/converter/DateToLongConverter.java | 72 + .../util/converter/DefaultConverterFactory.java | 101 + .../data/util/converter/ReverseConverter.java | 84 + .../util/converter/StringToBooleanConverter.java | 108 + .../data/util/converter/StringToDateConverter.java | 112 + .../util/converter/StringToDoubleConverter.java | 107 + .../util/converter/StringToIntegerConverter.java | 88 + .../util/converter/StringToNumberConverter.java | 111 + .../data/util/filter/AbstractJunctionFilter.java | 76 + server/src/com/vaadin/data/util/filter/And.java | 44 + .../src/com/vaadin/data/util/filter/Between.java | 74 + .../src/com/vaadin/data/util/filter/Compare.java | 327 ++ server/src/com/vaadin/data/util/filter/IsNull.java | 79 + server/src/com/vaadin/data/util/filter/Like.java | 83 + server/src/com/vaadin/data/util/filter/Not.java | 70 + server/src/com/vaadin/data/util/filter/Or.java | 63 + .../data/util/filter/SimpleStringFilter.java | 152 + .../util/filter/UnsupportedFilterException.java | 35 + server/src/com/vaadin/data/util/package.html | 18 + .../data/util/sqlcontainer/CacheFlushNotifier.java | 92 + .../vaadin/data/util/sqlcontainer/CacheMap.java | 31 + .../data/util/sqlcontainer/ColumnProperty.java | 248 + .../util/sqlcontainer/OptimisticLockException.java | 38 + .../data/util/sqlcontainer/ReadOnlyRowId.java | 31 + .../vaadin/data/util/sqlcontainer/Reference.java | 56 + .../com/vaadin/data/util/sqlcontainer/RowId.java | 81 + .../com/vaadin/data/util/sqlcontainer/RowItem.java | 133 + .../data/util/sqlcontainer/SQLContainer.java | 1716 ++++++ .../com/vaadin/data/util/sqlcontainer/SQLUtil.java | 36 + .../data/util/sqlcontainer/TemporaryRowId.java | 32 + .../connection/J2EEConnectionPool.java | 72 + .../connection/JDBCConnectionPool.java | 41 + .../connection/SimpleJDBCConnectionPool.java | 168 + .../util/sqlcontainer/query/FreeformQuery.java | 507 ++ .../sqlcontainer/query/FreeformQueryDelegate.java | 118 + .../query/FreeformStatementDelegate.java | 57 + .../data/util/sqlcontainer/query/OrderBy.java | 46 + .../util/sqlcontainer/query/QueryDelegate.java | 211 + .../data/util/sqlcontainer/query/TableQuery.java | 715 +++ .../query/generator/DefaultSQLGenerator.java | 367 ++ .../query/generator/MSSQLGenerator.java | 101 + .../query/generator/OracleGenerator.java | 112 + .../sqlcontainer/query/generator/SQLGenerator.java | 88 + .../query/generator/StatementHelper.java | 163 + .../query/generator/filter/AndTranslator.java | 23 + .../query/generator/filter/BetweenTranslator.java | 25 + .../query/generator/filter/CompareTranslator.java | 38 + .../query/generator/filter/FilterTranslator.java | 16 + .../query/generator/filter/IsNullTranslator.java | 22 + .../query/generator/filter/LikeTranslator.java | 30 + .../query/generator/filter/NotTranslator.java | 29 + .../query/generator/filter/OrTranslator.java | 23 + .../query/generator/filter/QueryBuilder.java | 98 + .../generator/filter/SimpleStringTranslator.java | 30 + .../query/generator/filter/StringDecorator.java | 58 + .../data/validator/AbstractStringValidator.java | 42 + .../vaadin/data/validator/AbstractValidator.java | 139 + .../com/vaadin/data/validator/BeanValidator.java | 176 + .../vaadin/data/validator/CompositeValidator.java | 259 + .../vaadin/data/validator/DateRangeValidator.java | 51 + .../data/validator/DoubleRangeValidator.java | 37 + .../com/vaadin/data/validator/DoubleValidator.java | 58 + .../com/vaadin/data/validator/EmailValidator.java | 35 + .../data/validator/IntegerRangeValidator.java | 37 + .../vaadin/data/validator/IntegerValidator.java | 58 + .../com/vaadin/data/validator/NullValidator.java | 92 + .../com/vaadin/data/validator/RangeValidator.java | 186 + .../com/vaadin/data/validator/RegexpValidator.java | 97 + .../data/validator/StringLengthValidator.java | 139 + server/src/com/vaadin/data/validator/package.html | 23 + server/src/com/vaadin/event/Action.java | 195 + server/src/com/vaadin/event/ActionManager.java | 249 + .../com/vaadin/event/ComponentEventListener.java | 11 + .../com/vaadin/event/DataBoundTransferable.java | 66 + server/src/com/vaadin/event/EventRouter.java | 201 + server/src/com/vaadin/event/FieldEvents.java | 275 + server/src/com/vaadin/event/ItemClickEvent.java | 121 + server/src/com/vaadin/event/LayoutEvents.java | 138 + server/src/com/vaadin/event/ListenerMethod.java | 663 +++ server/src/com/vaadin/event/MethodEventSource.java | 157 + server/src/com/vaadin/event/MouseEvents.java | 234 + server/src/com/vaadin/event/ShortcutAction.java | 373 ++ server/src/com/vaadin/event/ShortcutListener.java | 33 + server/src/com/vaadin/event/Transferable.java | 57 + server/src/com/vaadin/event/TransferableImpl.java | 47 + .../src/com/vaadin/event/dd/DragAndDropEvent.java | 50 + server/src/com/vaadin/event/dd/DragSource.java | 52 + server/src/com/vaadin/event/dd/DropHandler.java | 61 + server/src/com/vaadin/event/dd/DropTarget.java | 42 + server/src/com/vaadin/event/dd/TargetDetails.java | 37 + .../src/com/vaadin/event/dd/TargetDetailsImpl.java | 46 + .../vaadin/event/dd/acceptcriteria/AcceptAll.java | 36 + .../event/dd/acceptcriteria/AcceptCriterion.java | 75 + .../com/vaadin/event/dd/acceptcriteria/And.java | 54 + .../dd/acceptcriteria/ClientSideCriterion.java | 61 + .../dd/acceptcriteria/ContainsDataFlavor.java | 53 + .../com/vaadin/event/dd/acceptcriteria/Not.java | 39 + .../src/com/vaadin/event/dd/acceptcriteria/Or.java | 52 + .../dd/acceptcriteria/ServerSideCriterion.java | 57 + .../vaadin/event/dd/acceptcriteria/SourceIs.java | 67 + .../event/dd/acceptcriteria/SourceIsTarget.java | 51 + .../event/dd/acceptcriteria/TargetDetailIs.java | 72 + server/src/com/vaadin/event/package.html | 58 + server/src/com/vaadin/external/json/JSONArray.java | 963 ++++ .../com/vaadin/external/json/JSONException.java | 32 + .../src/com/vaadin/external/json/JSONObject.java | 1693 ++++++ .../src/com/vaadin/external/json/JSONString.java | 21 + .../src/com/vaadin/external/json/JSONStringer.java | 84 + .../src/com/vaadin/external/json/JSONTokener.java | 451 ++ .../src/com/vaadin/external/json/JSONWriter.java | 355 ++ server/src/com/vaadin/external/json/README | 68 + .../src/com/vaadin/navigator/FragmentManager.java | 38 + server/src/com/vaadin/navigator/Navigator.java | 656 +++ server/src/com/vaadin/navigator/View.java | 36 + .../com/vaadin/navigator/ViewChangeListener.java | 118 + server/src/com/vaadin/navigator/ViewDisplay.java | 29 + server/src/com/vaadin/navigator/ViewProvider.java | 44 + server/src/com/vaadin/package.html | 27 + .../portal/gwt/PortalDefaultWidgetSet.gwt.xml | 6 + .../src/com/vaadin/service/ApplicationContext.java | 165 + .../src/com/vaadin/service/FileTypeResolver.java | 385 ++ server/src/com/vaadin/service/package.html | 20 + .../vaadin/terminal/AbstractClientConnector.java | 510 ++ .../com/vaadin/terminal/AbstractErrorMessage.java | 176 + .../src/com/vaadin/terminal/AbstractExtension.java | 76 + .../terminal/AbstractJavaScriptExtension.java | 162 + .../com/vaadin/terminal/ApplicationResource.java | 75 + server/src/com/vaadin/terminal/ClassResource.java | 178 + .../src/com/vaadin/terminal/CombinedRequest.java | 187 + .../com/vaadin/terminal/CompositeErrorMessage.java | 112 + .../vaadin/terminal/DeploymentConfiguration.java | 123 + server/src/com/vaadin/terminal/DownloadStream.java | 335 ++ server/src/com/vaadin/terminal/ErrorMessage.java | 126 + server/src/com/vaadin/terminal/Extension.java | 27 + .../src/com/vaadin/terminal/ExternalResource.java | 118 + server/src/com/vaadin/terminal/FileResource.java | 174 + .../vaadin/terminal/JavaScriptCallbackHelper.java | 116 + server/src/com/vaadin/terminal/KeyMapper.java | 86 + server/src/com/vaadin/terminal/LegacyPaint.java | 85 + server/src/com/vaadin/terminal/Page.java | 646 +++ server/src/com/vaadin/terminal/PaintException.java | 54 + server/src/com/vaadin/terminal/PaintTarget.java | 509 ++ server/src/com/vaadin/terminal/RequestHandler.java | 36 + server/src/com/vaadin/terminal/Resource.java | 26 + server/src/com/vaadin/terminal/Scrollable.java | 80 + server/src/com/vaadin/terminal/Sizeable.java | 242 + server/src/com/vaadin/terminal/StreamResource.java | 222 + server/src/com/vaadin/terminal/StreamVariable.java | 157 + server/src/com/vaadin/terminal/SystemError.java | 82 + server/src/com/vaadin/terminal/Terminal.java | 80 + server/src/com/vaadin/terminal/ThemeResource.java | 96 + server/src/com/vaadin/terminal/UserError.java | 70 + .../src/com/vaadin/terminal/Vaadin6Component.java | 44 + server/src/com/vaadin/terminal/VariableOwner.java | 85 + server/src/com/vaadin/terminal/WrappedRequest.java | 277 + .../src/com/vaadin/terminal/WrappedResponse.java | 147 + .../gwt/server/AbstractApplicationPortlet.java | 1079 ++++ .../gwt/server/AbstractApplicationServlet.java | 1623 ++++++ .../gwt/server/AbstractCommunicationManager.java | 2790 ++++++++++ .../server/AbstractDeploymentConfiguration.java | 143 + .../gwt/server/AbstractStreamingEvent.java | 46 + .../gwt/server/AbstractWebApplicationContext.java | 268 + .../vaadin/terminal/gwt/server/AddonContext.java | 80 + .../terminal/gwt/server/AddonContextEvent.java | 19 + .../terminal/gwt/server/AddonContextListener.java | 13 + .../terminal/gwt/server/ApplicationPortlet2.java | 38 + .../gwt/server/ApplicationResourceHandler.java | 55 + .../terminal/gwt/server/ApplicationServlet.java | 78 + .../gwt/server/ApplicationStartedEvent.java | 28 + .../gwt/server/ApplicationStartedListener.java | 11 + .../vaadin/terminal/gwt/server/BootstrapDom.java | 9 + .../gwt/server/BootstrapFragmentResponse.java | 28 + .../terminal/gwt/server/BootstrapHandler.java | 570 ++ .../terminal/gwt/server/BootstrapListener.java | 13 + .../terminal/gwt/server/BootstrapPageResponse.java | 39 + .../terminal/gwt/server/BootstrapResponse.java | 45 + .../gwt/server/ChangeVariablesErrorEvent.java | 39 + .../terminal/gwt/server/ClientConnector.java | 149 + .../gwt/server/ClientMethodInvocation.java | 71 + .../terminal/gwt/server/CommunicationManager.java | 122 + .../gwt/server/ComponentSizeValidator.java | 664 +++ .../com/vaadin/terminal/gwt/server/Constants.java | 80 + .../terminal/gwt/server/DragAndDropService.java | 313 ++ .../terminal/gwt/server/GAEApplicationServlet.java | 417 ++ .../gwt/server/HttpServletRequestListener.java | 54 + .../com/vaadin/terminal/gwt/server/JsonCodec.java | 792 +++ .../terminal/gwt/server/JsonPaintTarget.java | 1022 ++++ .../server/LegacyChangeVariablesInvocation.java | 38 + .../gwt/server/NoInputStreamException.java | 9 + .../gwt/server/NoOutputStreamException.java | 9 + .../gwt/server/PortletApplicationContext2.java | 398 ++ .../gwt/server/PortletCommunicationManager.java | 170 + .../gwt/server/PortletRequestListener.java | 56 + .../vaadin/terminal/gwt/server/RequestTimer.java | 43 + .../terminal/gwt/server/ResourceReference.java | 67 + .../gwt/server/RestrictedRenderResponse.java | 172 + .../com/vaadin/terminal/gwt/server/RpcManager.java | 48 + .../com/vaadin/terminal/gwt/server/RpcTarget.java | 28 + .../terminal/gwt/server/ServerRpcManager.java | 142 + .../gwt/server/ServerRpcMethodInvocation.java | 113 + .../terminal/gwt/server/ServletPortletHelper.java | 120 + .../gwt/server/SessionExpiredException.java | 9 + .../terminal/gwt/server/StreamingEndEventImpl.java | 16 + .../gwt/server/StreamingErrorEventImpl.java | 25 + .../gwt/server/StreamingProgressEventImpl.java | 17 + .../gwt/server/StreamingStartEventImpl.java | 28 + .../gwt/server/SystemMessageException.java | 57 + .../gwt/server/UnsupportedBrowserHandler.java | 89 + .../terminal/gwt/server/UploadException.java | 15 + .../terminal/gwt/server/WebApplicationContext.java | 180 + .../com/vaadin/terminal/gwt/server/WebBrowser.java | 462 ++ .../gwt/server/WrappedHttpServletRequest.java | 118 + .../gwt/server/WrappedHttpServletResponse.java | 75 + .../terminal/gwt/server/WrappedPortletRequest.java | 217 + .../gwt/server/WrappedPortletResponse.java | 111 + server/src/com/vaadin/terminal/package.html | 21 + server/src/com/vaadin/tools/ReflectTools.java | 126 + server/src/com/vaadin/tools/WidgetsetCompiler.java | 94 + server/src/com/vaadin/ui/AbsoluteLayout.java | 632 +++ server/src/com/vaadin/ui/AbstractComponent.java | 1382 +++++ .../com/vaadin/ui/AbstractComponentContainer.java | 351 ++ server/src/com/vaadin/ui/AbstractField.java | 1657 ++++++ .../com/vaadin/ui/AbstractJavaScriptComponent.java | 165 + server/src/com/vaadin/ui/AbstractLayout.java | 77 + server/src/com/vaadin/ui/AbstractMedia.java | 196 + .../src/com/vaadin/ui/AbstractOrderedLayout.java | 383 ++ server/src/com/vaadin/ui/AbstractSelect.java | 2029 ++++++++ server/src/com/vaadin/ui/AbstractSplitPanel.java | 521 ++ server/src/com/vaadin/ui/AbstractTextField.java | 674 +++ server/src/com/vaadin/ui/Accordion.java | 19 + server/src/com/vaadin/ui/Alignment.java | 158 + server/src/com/vaadin/ui/Audio.java | 55 + server/src/com/vaadin/ui/Button.java | 539 ++ server/src/com/vaadin/ui/CheckBox.java | 141 + server/src/com/vaadin/ui/ComboBox.java | 116 + server/src/com/vaadin/ui/Component.java | 1047 ++++ server/src/com/vaadin/ui/ComponentContainer.java | 222 + server/src/com/vaadin/ui/ConnectorTracker.java | 320 ++ server/src/com/vaadin/ui/CssLayout.java | 308 ++ server/src/com/vaadin/ui/CustomComponent.java | 189 + server/src/com/vaadin/ui/CustomField.java | 237 + server/src/com/vaadin/ui/CustomLayout.java | 329 ++ server/src/com/vaadin/ui/DateField.java | 869 ++++ server/src/com/vaadin/ui/DefaultFieldFactory.java | 146 + server/src/com/vaadin/ui/DragAndDropWrapper.java | 407 ++ server/src/com/vaadin/ui/Embedded.java | 531 ++ server/src/com/vaadin/ui/Field.java | 97 + server/src/com/vaadin/ui/Form.java | 1420 +++++ server/src/com/vaadin/ui/FormFieldFactory.java | 41 + server/src/com/vaadin/ui/FormLayout.java | 31 + server/src/com/vaadin/ui/GridLayout.java | 1415 +++++ server/src/com/vaadin/ui/HasComponents.java | 49 + server/src/com/vaadin/ui/HorizontalLayout.java | 24 + server/src/com/vaadin/ui/HorizontalSplitPanel.java | 34 + server/src/com/vaadin/ui/Html5File.java | 65 + server/src/com/vaadin/ui/InlineDateField.java | 46 + server/src/com/vaadin/ui/JavaScript.java | 157 + server/src/com/vaadin/ui/JavaScriptFunction.java | 41 + server/src/com/vaadin/ui/Label.java | 483 ++ server/src/com/vaadin/ui/Layout.java | 229 + server/src/com/vaadin/ui/Link.java | 242 + server/src/com/vaadin/ui/ListSelect.java | 96 + server/src/com/vaadin/ui/LoginForm.java | 353 ++ server/src/com/vaadin/ui/MenuBar.java | 890 ++++ server/src/com/vaadin/ui/NativeButton.java | 21 + server/src/com/vaadin/ui/NativeSelect.java | 91 + server/src/com/vaadin/ui/Notification.java | 367 ++ server/src/com/vaadin/ui/OptionGroup.java | 203 + server/src/com/vaadin/ui/Panel.java | 486 ++ server/src/com/vaadin/ui/PasswordField.java | 67 + server/src/com/vaadin/ui/PopupDateField.java | 80 + server/src/com/vaadin/ui/PopupView.java | 453 ++ server/src/com/vaadin/ui/ProgressIndicator.java | 257 + server/src/com/vaadin/ui/RichTextArea.java | 344 ++ server/src/com/vaadin/ui/Root.java | 1227 +++++ server/src/com/vaadin/ui/Select.java | 803 +++ server/src/com/vaadin/ui/Slider.java | 372 ++ server/src/com/vaadin/ui/TabSheet.java | 1328 +++++ server/src/com/vaadin/ui/Table.java | 5449 ++++++++++++++++++++ server/src/com/vaadin/ui/TableFieldFactory.java | 45 + server/src/com/vaadin/ui/TextArea.java | 121 + server/src/com/vaadin/ui/TextField.java | 92 + server/src/com/vaadin/ui/Tree.java | 1615 ++++++ server/src/com/vaadin/ui/TreeTable.java | 824 +++ server/src/com/vaadin/ui/TwinColSelect.java | 180 + server/src/com/vaadin/ui/UniqueSerializable.java | 30 + server/src/com/vaadin/ui/Upload.java | 1055 ++++ server/src/com/vaadin/ui/VerticalLayout.java | 25 + server/src/com/vaadin/ui/VerticalSplitPanel.java | 30 + server/src/com/vaadin/ui/Video.java | 81 + server/src/com/vaadin/ui/Window.java | 853 +++ .../ui/doc-files/component_class_hierarchy.gif | Bin 0 -> 11077 bytes .../vaadin/ui/doc-files/component_interfaces.gif | Bin 0 -> 2272 bytes server/src/com/vaadin/ui/package.html | 76 + server/src/com/vaadin/ui/themes/BaseTheme.java | 59 + .../src/com/vaadin/ui/themes/ChameleonTheme.java | 365 ++ server/src/com/vaadin/ui/themes/LiferayTheme.java | 31 + server/src/com/vaadin/ui/themes/Reindeer.java | 217 + server/src/com/vaadin/ui/themes/Runo.java | 183 + server/src/com/vaadin/util/SerializerHelper.java | 145 + server/src/org/jsoup/Connection.java | 481 ++ server/src/org/jsoup/Jsoup.java | 229 + server/src/org/jsoup/examples/HtmlToPlainText.java | 109 + server/src/org/jsoup/examples/ListLinks.java | 56 + server/src/org/jsoup/examples/package-info.java | 4 + server/src/org/jsoup/helper/DataUtil.java | 135 + .../org/jsoup/helper/DescendableLinkedList.java | 82 + server/src/org/jsoup/helper/HttpConnection.java | 658 +++ server/src/org/jsoup/helper/StringUtil.java | 140 + server/src/org/jsoup/helper/Validate.java | 112 + server/src/org/jsoup/nodes/Attribute.java | 131 + server/src/org/jsoup/nodes/Attributes.java | 249 + server/src/org/jsoup/nodes/Comment.java | 46 + server/src/org/jsoup/nodes/DataNode.java | 62 + server/src/org/jsoup/nodes/Document.java | 350 ++ server/src/org/jsoup/nodes/DocumentType.java | 46 + server/src/org/jsoup/nodes/Element.java | 1119 ++++ server/src/org/jsoup/nodes/Entities.java | 184 + server/src/org/jsoup/nodes/Node.java | 615 +++ server/src/org/jsoup/nodes/TextNode.java | 175 + server/src/org/jsoup/nodes/XmlDeclaration.java | 48 + .../src/org/jsoup/nodes/entities-base.properties | 106 + .../src/org/jsoup/nodes/entities-full.properties | 2032 ++++++++ server/src/org/jsoup/nodes/package-info.java | 4 + server/src/org/jsoup/package-info.java | 4 + server/src/org/jsoup/parser/CharacterReader.java | 230 + server/src/org/jsoup/parser/HtmlTreeBuilder.java | 672 +++ .../src/org/jsoup/parser/HtmlTreeBuilderState.java | 1482 ++++++ server/src/org/jsoup/parser/ParseError.java | 40 + server/src/org/jsoup/parser/ParseErrorList.java | 34 + server/src/org/jsoup/parser/Parser.java | 157 + server/src/org/jsoup/parser/Tag.java | 262 + server/src/org/jsoup/parser/Token.java | 252 + server/src/org/jsoup/parser/TokenQueue.java | 393 ++ server/src/org/jsoup/parser/Tokeniser.java | 230 + server/src/org/jsoup/parser/TokeniserState.java | 1778 +++++++ server/src/org/jsoup/parser/TreeBuilder.java | 60 + server/src/org/jsoup/parser/XmlTreeBuilder.java | 111 + server/src/org/jsoup/parser/package-info.java | 4 + server/src/org/jsoup/safety/Cleaner.java | 129 + server/src/org/jsoup/safety/Whitelist.java | 451 ++ server/src/org/jsoup/safety/package-info.java | 4 + server/src/org/jsoup/select/Collector.java | 51 + .../src/org/jsoup/select/CombiningEvaluator.java | 94 + server/src/org/jsoup/select/Elements.java | 536 ++ server/src/org/jsoup/select/Evaluator.java | 454 ++ server/src/org/jsoup/select/NodeTraversor.java | 47 + server/src/org/jsoup/select/NodeVisitor.java | 30 + server/src/org/jsoup/select/QueryParser.java | 293 ++ server/src/org/jsoup/select/Selector.java | 126 + .../src/org/jsoup/select/StructuralEvaluator.java | 132 + server/src/org/jsoup/select/package-info.java | 4 + 408 files changed, 107042 insertions(+) create mode 100644 server/src/com/vaadin/Application.java create mode 100644 server/src/com/vaadin/RootRequiresMoreInformationException.java create mode 100644 server/src/com/vaadin/Vaadin.gwt.xml create mode 100644 server/src/com/vaadin/Version.java create mode 100644 server/src/com/vaadin/annotations/AutoGenerated.java create mode 100644 server/src/com/vaadin/annotations/EagerInit.java create mode 100644 server/src/com/vaadin/annotations/JavaScript.java create mode 100644 server/src/com/vaadin/annotations/StyleSheet.java create mode 100644 server/src/com/vaadin/annotations/Theme.java create mode 100644 server/src/com/vaadin/annotations/Widgetset.java create mode 100644 server/src/com/vaadin/annotations/package.html create mode 100644 server/src/com/vaadin/data/Buffered.java create mode 100644 server/src/com/vaadin/data/BufferedValidatable.java create mode 100644 server/src/com/vaadin/data/Collapsible.java create mode 100644 server/src/com/vaadin/data/Container.java create mode 100644 server/src/com/vaadin/data/Item.java create mode 100644 server/src/com/vaadin/data/Property.java create mode 100644 server/src/com/vaadin/data/Validatable.java create mode 100644 server/src/com/vaadin/data/Validator.java create mode 100644 server/src/com/vaadin/data/fieldgroup/BeanFieldGroup.java create mode 100644 server/src/com/vaadin/data/fieldgroup/Caption.java create mode 100644 server/src/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java create mode 100644 server/src/com/vaadin/data/fieldgroup/FieldGroup.java create mode 100644 server/src/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java create mode 100644 server/src/com/vaadin/data/fieldgroup/PropertyId.java create mode 100644 server/src/com/vaadin/data/package.html create mode 100644 server/src/com/vaadin/data/util/AbstractBeanContainer.java create mode 100644 server/src/com/vaadin/data/util/AbstractContainer.java create mode 100644 server/src/com/vaadin/data/util/AbstractInMemoryContainer.java create mode 100644 server/src/com/vaadin/data/util/AbstractProperty.java create mode 100644 server/src/com/vaadin/data/util/BeanContainer.java create mode 100644 server/src/com/vaadin/data/util/BeanItem.java create mode 100644 server/src/com/vaadin/data/util/BeanItemContainer.java create mode 100644 server/src/com/vaadin/data/util/ContainerHierarchicalWrapper.java create mode 100644 server/src/com/vaadin/data/util/ContainerOrderedWrapper.java create mode 100644 server/src/com/vaadin/data/util/DefaultItemSorter.java create mode 100644 server/src/com/vaadin/data/util/FilesystemContainer.java create mode 100644 server/src/com/vaadin/data/util/HierarchicalContainer.java create mode 100644 server/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapper.java create mode 100644 server/src/com/vaadin/data/util/IndexedContainer.java create mode 100644 server/src/com/vaadin/data/util/ItemSorter.java create mode 100644 server/src/com/vaadin/data/util/ListSet.java create mode 100644 server/src/com/vaadin/data/util/MethodProperty.java create mode 100644 server/src/com/vaadin/data/util/MethodPropertyDescriptor.java create mode 100644 server/src/com/vaadin/data/util/NestedMethodProperty.java create mode 100644 server/src/com/vaadin/data/util/NestedPropertyDescriptor.java create mode 100644 server/src/com/vaadin/data/util/ObjectProperty.java create mode 100644 server/src/com/vaadin/data/util/PropertyFormatter.java create mode 100644 server/src/com/vaadin/data/util/PropertysetItem.java create mode 100644 server/src/com/vaadin/data/util/QueryContainer.java create mode 100644 server/src/com/vaadin/data/util/TextFileProperty.java create mode 100644 server/src/com/vaadin/data/util/TransactionalPropertyWrapper.java create mode 100644 server/src/com/vaadin/data/util/VaadinPropertyDescriptor.java create mode 100644 server/src/com/vaadin/data/util/converter/Converter.java create mode 100644 server/src/com/vaadin/data/util/converter/ConverterFactory.java create mode 100644 server/src/com/vaadin/data/util/converter/ConverterUtil.java create mode 100644 server/src/com/vaadin/data/util/converter/DateToLongConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/DefaultConverterFactory.java create mode 100644 server/src/com/vaadin/data/util/converter/ReverseConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/StringToDateConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/StringToDoubleConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/StringToIntegerConverter.java create mode 100644 server/src/com/vaadin/data/util/converter/StringToNumberConverter.java create mode 100644 server/src/com/vaadin/data/util/filter/AbstractJunctionFilter.java create mode 100644 server/src/com/vaadin/data/util/filter/And.java create mode 100644 server/src/com/vaadin/data/util/filter/Between.java create mode 100644 server/src/com/vaadin/data/util/filter/Compare.java create mode 100644 server/src/com/vaadin/data/util/filter/IsNull.java create mode 100644 server/src/com/vaadin/data/util/filter/Like.java create mode 100644 server/src/com/vaadin/data/util/filter/Not.java create mode 100644 server/src/com/vaadin/data/util/filter/Or.java create mode 100644 server/src/com/vaadin/data/util/filter/SimpleStringFilter.java create mode 100644 server/src/com/vaadin/data/util/filter/UnsupportedFilterException.java create mode 100644 server/src/com/vaadin/data/util/package.html create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/CacheFlushNotifier.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/CacheMap.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/ColumnProperty.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/OptimisticLockException.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/ReadOnlyRowId.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/Reference.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/RowId.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/RowItem.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/SQLContainer.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/SQLUtil.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/TemporaryRowId.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPool.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/connection/JDBCConnectionPool.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPool.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQuery.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/FreeformQueryDelegate.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/FreeformStatementDelegate.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/OrderBy.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/QueryDelegate.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/TableQuery.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/DefaultSQLGenerator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/MSSQLGenerator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/OracleGenerator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/SQLGenerator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/StatementHelper.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/AndTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/BetweenTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/CompareTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/FilterTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/IsNullTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/LikeTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/NotTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/OrTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/QueryBuilder.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/SimpleStringTranslator.java create mode 100644 server/src/com/vaadin/data/util/sqlcontainer/query/generator/filter/StringDecorator.java create mode 100644 server/src/com/vaadin/data/validator/AbstractStringValidator.java create mode 100644 server/src/com/vaadin/data/validator/AbstractValidator.java create mode 100644 server/src/com/vaadin/data/validator/BeanValidator.java create mode 100644 server/src/com/vaadin/data/validator/CompositeValidator.java create mode 100644 server/src/com/vaadin/data/validator/DateRangeValidator.java create mode 100644 server/src/com/vaadin/data/validator/DoubleRangeValidator.java create mode 100644 server/src/com/vaadin/data/validator/DoubleValidator.java create mode 100644 server/src/com/vaadin/data/validator/EmailValidator.java create mode 100644 server/src/com/vaadin/data/validator/IntegerRangeValidator.java create mode 100644 server/src/com/vaadin/data/validator/IntegerValidator.java create mode 100644 server/src/com/vaadin/data/validator/NullValidator.java create mode 100644 server/src/com/vaadin/data/validator/RangeValidator.java create mode 100644 server/src/com/vaadin/data/validator/RegexpValidator.java create mode 100644 server/src/com/vaadin/data/validator/StringLengthValidator.java create mode 100644 server/src/com/vaadin/data/validator/package.html create mode 100644 server/src/com/vaadin/event/Action.java create mode 100644 server/src/com/vaadin/event/ActionManager.java create mode 100644 server/src/com/vaadin/event/ComponentEventListener.java create mode 100644 server/src/com/vaadin/event/DataBoundTransferable.java create mode 100644 server/src/com/vaadin/event/EventRouter.java create mode 100644 server/src/com/vaadin/event/FieldEvents.java create mode 100644 server/src/com/vaadin/event/ItemClickEvent.java create mode 100644 server/src/com/vaadin/event/LayoutEvents.java create mode 100644 server/src/com/vaadin/event/ListenerMethod.java create mode 100644 server/src/com/vaadin/event/MethodEventSource.java create mode 100644 server/src/com/vaadin/event/MouseEvents.java create mode 100644 server/src/com/vaadin/event/ShortcutAction.java create mode 100644 server/src/com/vaadin/event/ShortcutListener.java create mode 100644 server/src/com/vaadin/event/Transferable.java create mode 100644 server/src/com/vaadin/event/TransferableImpl.java create mode 100644 server/src/com/vaadin/event/dd/DragAndDropEvent.java create mode 100644 server/src/com/vaadin/event/dd/DragSource.java create mode 100644 server/src/com/vaadin/event/dd/DropHandler.java create mode 100644 server/src/com/vaadin/event/dd/DropTarget.java create mode 100644 server/src/com/vaadin/event/dd/TargetDetails.java create mode 100644 server/src/com/vaadin/event/dd/TargetDetailsImpl.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/AcceptAll.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/AcceptCriterion.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/And.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/ClientSideCriterion.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/ContainsDataFlavor.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/Not.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/Or.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/ServerSideCriterion.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/SourceIs.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/SourceIsTarget.java create mode 100644 server/src/com/vaadin/event/dd/acceptcriteria/TargetDetailIs.java create mode 100644 server/src/com/vaadin/event/package.html create mode 100644 server/src/com/vaadin/external/json/JSONArray.java create mode 100644 server/src/com/vaadin/external/json/JSONException.java create mode 100644 server/src/com/vaadin/external/json/JSONObject.java create mode 100644 server/src/com/vaadin/external/json/JSONString.java create mode 100644 server/src/com/vaadin/external/json/JSONStringer.java create mode 100644 server/src/com/vaadin/external/json/JSONTokener.java create mode 100644 server/src/com/vaadin/external/json/JSONWriter.java create mode 100644 server/src/com/vaadin/external/json/README create mode 100644 server/src/com/vaadin/navigator/FragmentManager.java create mode 100644 server/src/com/vaadin/navigator/Navigator.java create mode 100644 server/src/com/vaadin/navigator/View.java create mode 100644 server/src/com/vaadin/navigator/ViewChangeListener.java create mode 100644 server/src/com/vaadin/navigator/ViewDisplay.java create mode 100644 server/src/com/vaadin/navigator/ViewProvider.java create mode 100644 server/src/com/vaadin/package.html create mode 100644 server/src/com/vaadin/portal/gwt/PortalDefaultWidgetSet.gwt.xml create mode 100644 server/src/com/vaadin/service/ApplicationContext.java create mode 100644 server/src/com/vaadin/service/FileTypeResolver.java create mode 100644 server/src/com/vaadin/service/package.html create mode 100644 server/src/com/vaadin/terminal/AbstractClientConnector.java create mode 100644 server/src/com/vaadin/terminal/AbstractErrorMessage.java create mode 100644 server/src/com/vaadin/terminal/AbstractExtension.java create mode 100644 server/src/com/vaadin/terminal/AbstractJavaScriptExtension.java create mode 100644 server/src/com/vaadin/terminal/ApplicationResource.java create mode 100644 server/src/com/vaadin/terminal/ClassResource.java create mode 100644 server/src/com/vaadin/terminal/CombinedRequest.java create mode 100644 server/src/com/vaadin/terminal/CompositeErrorMessage.java create mode 100644 server/src/com/vaadin/terminal/DeploymentConfiguration.java create mode 100644 server/src/com/vaadin/terminal/DownloadStream.java create mode 100644 server/src/com/vaadin/terminal/ErrorMessage.java create mode 100644 server/src/com/vaadin/terminal/Extension.java create mode 100644 server/src/com/vaadin/terminal/ExternalResource.java create mode 100644 server/src/com/vaadin/terminal/FileResource.java create mode 100644 server/src/com/vaadin/terminal/JavaScriptCallbackHelper.java create mode 100644 server/src/com/vaadin/terminal/KeyMapper.java create mode 100644 server/src/com/vaadin/terminal/LegacyPaint.java create mode 100644 server/src/com/vaadin/terminal/Page.java create mode 100644 server/src/com/vaadin/terminal/PaintException.java create mode 100644 server/src/com/vaadin/terminal/PaintTarget.java create mode 100644 server/src/com/vaadin/terminal/RequestHandler.java create mode 100644 server/src/com/vaadin/terminal/Resource.java create mode 100644 server/src/com/vaadin/terminal/Scrollable.java create mode 100644 server/src/com/vaadin/terminal/Sizeable.java create mode 100644 server/src/com/vaadin/terminal/StreamResource.java create mode 100644 server/src/com/vaadin/terminal/StreamVariable.java create mode 100644 server/src/com/vaadin/terminal/SystemError.java create mode 100644 server/src/com/vaadin/terminal/Terminal.java create mode 100644 server/src/com/vaadin/terminal/ThemeResource.java create mode 100644 server/src/com/vaadin/terminal/UserError.java create mode 100644 server/src/com/vaadin/terminal/Vaadin6Component.java create mode 100644 server/src/com/vaadin/terminal/VariableOwner.java create mode 100644 server/src/com/vaadin/terminal/WrappedRequest.java create mode 100644 server/src/com/vaadin/terminal/WrappedResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractDeploymentConfiguration.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractStreamingEvent.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AbstractWebApplicationContext.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AddonContext.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AddonContextEvent.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/AddonContextListener.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ApplicationPortlet2.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ApplicationResourceHandler.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ApplicationStartedEvent.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ApplicationStartedListener.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapDom.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapFragmentResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapHandler.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapListener.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapPageResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/BootstrapResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ClientConnector.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ClientMethodInvocation.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/CommunicationManager.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/Constants.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/DragAndDropService.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/HttpServletRequestListener.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/JsonCodec.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/LegacyChangeVariablesInvocation.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/NoInputStreamException.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/NoOutputStreamException.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/PortletApplicationContext2.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/PortletCommunicationManager.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/PortletRequestListener.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/RequestTimer.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ResourceReference.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/RestrictedRenderResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/RpcManager.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/RpcTarget.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ServerRpcManager.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ServerRpcMethodInvocation.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/SessionExpiredException.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/StreamingEndEventImpl.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/StreamingErrorEventImpl.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/StreamingProgressEventImpl.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/StreamingStartEventImpl.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/SystemMessageException.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/UnsupportedBrowserHandler.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/UploadException.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WebBrowser.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletRequest.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WrappedHttpServletResponse.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WrappedPortletRequest.java create mode 100644 server/src/com/vaadin/terminal/gwt/server/WrappedPortletResponse.java create mode 100644 server/src/com/vaadin/terminal/package.html create mode 100644 server/src/com/vaadin/tools/ReflectTools.java create mode 100644 server/src/com/vaadin/tools/WidgetsetCompiler.java create mode 100644 server/src/com/vaadin/ui/AbsoluteLayout.java create mode 100644 server/src/com/vaadin/ui/AbstractComponent.java create mode 100644 server/src/com/vaadin/ui/AbstractComponentContainer.java create mode 100644 server/src/com/vaadin/ui/AbstractField.java create mode 100644 server/src/com/vaadin/ui/AbstractJavaScriptComponent.java create mode 100644 server/src/com/vaadin/ui/AbstractLayout.java create mode 100644 server/src/com/vaadin/ui/AbstractMedia.java create mode 100644 server/src/com/vaadin/ui/AbstractOrderedLayout.java create mode 100644 server/src/com/vaadin/ui/AbstractSelect.java create mode 100644 server/src/com/vaadin/ui/AbstractSplitPanel.java create mode 100644 server/src/com/vaadin/ui/AbstractTextField.java create mode 100644 server/src/com/vaadin/ui/Accordion.java create mode 100644 server/src/com/vaadin/ui/Alignment.java create mode 100644 server/src/com/vaadin/ui/Audio.java create mode 100644 server/src/com/vaadin/ui/Button.java create mode 100644 server/src/com/vaadin/ui/CheckBox.java create mode 100644 server/src/com/vaadin/ui/ComboBox.java create mode 100644 server/src/com/vaadin/ui/Component.java create mode 100644 server/src/com/vaadin/ui/ComponentContainer.java create mode 100644 server/src/com/vaadin/ui/ConnectorTracker.java create mode 100644 server/src/com/vaadin/ui/CssLayout.java create mode 100644 server/src/com/vaadin/ui/CustomComponent.java create mode 100644 server/src/com/vaadin/ui/CustomField.java create mode 100644 server/src/com/vaadin/ui/CustomLayout.java create mode 100644 server/src/com/vaadin/ui/DateField.java create mode 100644 server/src/com/vaadin/ui/DefaultFieldFactory.java create mode 100644 server/src/com/vaadin/ui/DragAndDropWrapper.java create mode 100644 server/src/com/vaadin/ui/Embedded.java create mode 100644 server/src/com/vaadin/ui/Field.java create mode 100644 server/src/com/vaadin/ui/Form.java create mode 100644 server/src/com/vaadin/ui/FormFieldFactory.java create mode 100644 server/src/com/vaadin/ui/FormLayout.java create mode 100644 server/src/com/vaadin/ui/GridLayout.java create mode 100644 server/src/com/vaadin/ui/HasComponents.java create mode 100644 server/src/com/vaadin/ui/HorizontalLayout.java create mode 100644 server/src/com/vaadin/ui/HorizontalSplitPanel.java create mode 100644 server/src/com/vaadin/ui/Html5File.java create mode 100644 server/src/com/vaadin/ui/InlineDateField.java create mode 100644 server/src/com/vaadin/ui/JavaScript.java create mode 100644 server/src/com/vaadin/ui/JavaScriptFunction.java create mode 100644 server/src/com/vaadin/ui/Label.java create mode 100644 server/src/com/vaadin/ui/Layout.java create mode 100644 server/src/com/vaadin/ui/Link.java create mode 100644 server/src/com/vaadin/ui/ListSelect.java create mode 100644 server/src/com/vaadin/ui/LoginForm.java create mode 100644 server/src/com/vaadin/ui/MenuBar.java create mode 100644 server/src/com/vaadin/ui/NativeButton.java create mode 100644 server/src/com/vaadin/ui/NativeSelect.java create mode 100644 server/src/com/vaadin/ui/Notification.java create mode 100644 server/src/com/vaadin/ui/OptionGroup.java create mode 100644 server/src/com/vaadin/ui/Panel.java create mode 100644 server/src/com/vaadin/ui/PasswordField.java create mode 100644 server/src/com/vaadin/ui/PopupDateField.java create mode 100644 server/src/com/vaadin/ui/PopupView.java create mode 100644 server/src/com/vaadin/ui/ProgressIndicator.java create mode 100644 server/src/com/vaadin/ui/RichTextArea.java create mode 100644 server/src/com/vaadin/ui/Root.java create mode 100644 server/src/com/vaadin/ui/Select.java create mode 100644 server/src/com/vaadin/ui/Slider.java create mode 100644 server/src/com/vaadin/ui/TabSheet.java create mode 100644 server/src/com/vaadin/ui/Table.java create mode 100644 server/src/com/vaadin/ui/TableFieldFactory.java create mode 100644 server/src/com/vaadin/ui/TextArea.java create mode 100644 server/src/com/vaadin/ui/TextField.java create mode 100644 server/src/com/vaadin/ui/Tree.java create mode 100644 server/src/com/vaadin/ui/TreeTable.java create mode 100644 server/src/com/vaadin/ui/TwinColSelect.java create mode 100644 server/src/com/vaadin/ui/UniqueSerializable.java create mode 100644 server/src/com/vaadin/ui/Upload.java create mode 100644 server/src/com/vaadin/ui/VerticalLayout.java create mode 100644 server/src/com/vaadin/ui/VerticalSplitPanel.java create mode 100644 server/src/com/vaadin/ui/Video.java create mode 100644 server/src/com/vaadin/ui/Window.java create mode 100644 server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif create mode 100644 server/src/com/vaadin/ui/doc-files/component_interfaces.gif create mode 100644 server/src/com/vaadin/ui/package.html create mode 100644 server/src/com/vaadin/ui/themes/BaseTheme.java create mode 100644 server/src/com/vaadin/ui/themes/ChameleonTheme.java create mode 100644 server/src/com/vaadin/ui/themes/LiferayTheme.java create mode 100644 server/src/com/vaadin/ui/themes/Reindeer.java create mode 100644 server/src/com/vaadin/ui/themes/Runo.java create mode 100644 server/src/com/vaadin/util/SerializerHelper.java create mode 100644 server/src/org/jsoup/Connection.java create mode 100644 server/src/org/jsoup/Jsoup.java create mode 100644 server/src/org/jsoup/examples/HtmlToPlainText.java create mode 100644 server/src/org/jsoup/examples/ListLinks.java create mode 100644 server/src/org/jsoup/examples/package-info.java create mode 100644 server/src/org/jsoup/helper/DataUtil.java create mode 100644 server/src/org/jsoup/helper/DescendableLinkedList.java create mode 100644 server/src/org/jsoup/helper/HttpConnection.java create mode 100644 server/src/org/jsoup/helper/StringUtil.java create mode 100644 server/src/org/jsoup/helper/Validate.java create mode 100644 server/src/org/jsoup/nodes/Attribute.java create mode 100644 server/src/org/jsoup/nodes/Attributes.java create mode 100644 server/src/org/jsoup/nodes/Comment.java create mode 100644 server/src/org/jsoup/nodes/DataNode.java create mode 100644 server/src/org/jsoup/nodes/Document.java create mode 100644 server/src/org/jsoup/nodes/DocumentType.java create mode 100644 server/src/org/jsoup/nodes/Element.java create mode 100644 server/src/org/jsoup/nodes/Entities.java create mode 100644 server/src/org/jsoup/nodes/Node.java create mode 100644 server/src/org/jsoup/nodes/TextNode.java create mode 100644 server/src/org/jsoup/nodes/XmlDeclaration.java create mode 100644 server/src/org/jsoup/nodes/entities-base.properties create mode 100644 server/src/org/jsoup/nodes/entities-full.properties create mode 100644 server/src/org/jsoup/nodes/package-info.java create mode 100644 server/src/org/jsoup/package-info.java create mode 100644 server/src/org/jsoup/parser/CharacterReader.java create mode 100644 server/src/org/jsoup/parser/HtmlTreeBuilder.java create mode 100644 server/src/org/jsoup/parser/HtmlTreeBuilderState.java create mode 100644 server/src/org/jsoup/parser/ParseError.java create mode 100644 server/src/org/jsoup/parser/ParseErrorList.java create mode 100644 server/src/org/jsoup/parser/Parser.java create mode 100644 server/src/org/jsoup/parser/Tag.java create mode 100644 server/src/org/jsoup/parser/Token.java create mode 100644 server/src/org/jsoup/parser/TokenQueue.java create mode 100644 server/src/org/jsoup/parser/Tokeniser.java create mode 100644 server/src/org/jsoup/parser/TokeniserState.java create mode 100644 server/src/org/jsoup/parser/TreeBuilder.java create mode 100644 server/src/org/jsoup/parser/XmlTreeBuilder.java create mode 100644 server/src/org/jsoup/parser/package-info.java create mode 100644 server/src/org/jsoup/safety/Cleaner.java create mode 100644 server/src/org/jsoup/safety/Whitelist.java create mode 100644 server/src/org/jsoup/safety/package-info.java create mode 100644 server/src/org/jsoup/select/Collector.java create mode 100644 server/src/org/jsoup/select/CombiningEvaluator.java create mode 100644 server/src/org/jsoup/select/Elements.java create mode 100644 server/src/org/jsoup/select/Evaluator.java create mode 100644 server/src/org/jsoup/select/NodeTraversor.java create mode 100644 server/src/org/jsoup/select/NodeVisitor.java create mode 100644 server/src/org/jsoup/select/QueryParser.java create mode 100644 server/src/org/jsoup/select/Selector.java create mode 100644 server/src/org/jsoup/select/StructuralEvaluator.java create mode 100644 server/src/org/jsoup/select/package-info.java (limited to 'server') 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; + +/** + *

+ * 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. + *

+ * + *

+ * 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 Application + * needs to do is implement the init 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. + *

+ * + *

+ * See the class com.vaadin.demo.HelloWorld for a simple example of + * a fully working application. + *

+ * + *

+ * Window access. Application provides methods to + * list, add and remove the windows it contains. + *

+ * + *

+ * Execution control. 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. + *

+ * + *

+ * Theme selection. 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. + *

+ * + * @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 legacyRootNames = new HashMap(); + + /** + * 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. + * + *

+ * The main window is the window attached to the application URL ( + * {@link #getURL()}) and thus which is show by default to the user. + *

+ *

+ * Note that each application must have at least one main window. + *

+ * + * @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. + *

+ * Note that this theme can be overridden for a specific root with + * {@link Application#getThemeForRoot(Root)}. Setting theme to be + * null selects the default theme. For the available theme + * names, see the contents of the VAADIN/themes directory. + *

+ * + * @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, + * null 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)} + *

+ * {@inheritDoc} + */ + + @Override + public String getThemeForRoot(Root root) { + return theme; + } + + /** + *

+ * Gets a root by name. Returns null if the application is + * not running or it does not contain a window corresponding to the + * name. + *

+ * + * @param name + * the name of the requested window + * @return a root corresponding to the name, or null 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)}. + * + *

+ * Note that removing window from the application does not close the + * browser window - the window is only removed from the server-side. + *

+ * + * @param root + * the root to remove + */ + public void removeWindow(Root.LegacyWindow root) { + for (Entry entry : legacyRootNames + .entrySet()) { + if (entry.getValue() == root) { + legacyRootNames.remove(entry.getKey()); + } + } + } + + /** + * Gets the set of windows contained by the application. + * + *

+ * Note that the returned set of windows can not be modified. + *

+ * + * @return the unmodifiable collection of windows. + */ + public Collection 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 + * null 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 true if in production mode, else + * false + * + * @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 null 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 userChangeListeners = null; + + /** + * Application resource mapping: key <-> resource. + */ + private final Hashtable resourceKeyMap = new Hashtable(); + + private final Hashtable keyResourceMap = new Hashtable(); + + 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 requestHandlers = new LinkedList(); + + private int nextRootId = 0; + private Map roots = new HashMap(); + + private boolean productionMode = true; + + private final Map retainOnRefreshRoots = new HashMap(); + + private final EventRouter eventRouter = new EventRouter(); + + /** + * Keeps track of which roots have been inited. + *

+ * TODO Investigate whether this might be derived from the different states + * in getRootForRrequest. + *

+ */ + private Set initedRoots = new HashSet(); + + /** + * Gets the user of the application. + * + *

+ * 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)}. + *

+ * + * @return the User of the application. + */ + public Object getUser() { + return user; + } + + /** + *

+ * 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. + *

+ *

+ * 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. + *

+ *

+ * 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()}. + *

+ * + * @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. + * + *

+ * 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()}). + *

+ * + * @return the application's URL. + */ + public URL getURL() { + return applicationUrl; + } + + /** + * Ends the Application. + * + *

+ * 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. + *

+ * . + */ + public void close() { + applicationIsRunning = false; + } + + /** + * Starts the application on the given URL. + * + *

+ * 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. + *

+ * + *

+ * 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}. + *

+ * + * @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. + * + *

+ * Application starts running when its + * {@link #start(URL, Properties, ApplicationContext)} method has been + * called and stops when the {@link #close()} is called. + *

+ * + * @return true if the application is running, + * false if not. + */ + public boolean isRunning() { + return applicationIsRunning; + } + + /** + *

+ * Main initializer of the application. The init method is + * called by the framework when the application is started, and it should + * perform whatever initialization operations the application needs. + *

+ */ + public void init() { + // Default implementation does nothing + } + + /** + * Returns an enumeration of all the names in this application. + * + *

+ * See {@link #start(URL, Properties, ApplicationContext)} how properties + * are defined. + *

+ * + * @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 null 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; + } + + /** + *

+ * An event that characterizes a change in the current selection. + *

+ * 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 null + */ + public Object getPreviousUser() { + return prevUser; + } + + /** + * Gets the application where the user change occurred. + * + * @return the Application. + */ + public Application getApplication() { + return (Application) getSource(); + } + } + + /** + * The UserChangeListener interface for listening application + * user changes. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public interface UserChangeListener extends EventListener, Serializable { + + /** + * The applicationUserChanged 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(); + } + 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 + * null, 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. + *

+ * + * @return the URL. + */ + public String getLogoutURL() { + return logoutURL; + } + + /** + * Sets the URL user is redirected to on application close. If the URL is + * null, 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; + } + + /** + *

+ * Invoked by the terminal on any exception that occurs in application and + * is thrown by the setVariable to the terminal. The default + * implementation sets the exceptions as ComponentErrors to the + * component that initiated the exception and prints stack trace to standard + * error stream. + *

+ *

+ * You can safely override this method in your application in order to + * direct the errors to some other destination (for example log). + *

+ * + * @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. + *

+ * 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. + *

+ *

+ * 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}. + *

+ * + * @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. + *

+ * 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). + *

+ *

+ * 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)}. + *

+ *

+ * 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. + *

+ * Customize by overriding the static + * {@link Application#getSystemMessages()} and returning + * {@link CustomizedSystemMessages}. + *

+ *

+ * The defaults defined in this class are: + *

    + *
  • sessionExpiredURL = null
  • + *
  • sessionExpiredNotificationEnabled = true
  • + *
  • sessionExpiredCaption = ""
  • + *
  • sessionExpiredMessage = + * "Take note of any unsaved data, and click here to continue."
  • + *
  • communicationErrorURL = null
  • + *
  • communicationErrorNotificationEnabled = true
  • + *
  • communicationErrorCaption = "Communication problem"
  • + *
  • communicationErrorMessage = + * "Take note of any unsaved data, and click here to continue."
  • + *
  • internalErrorURL = null
  • + *
  • internalErrorNotificationEnabled = true
  • + *
  • internalErrorCaption = "Internal error"
  • + *
  • internalErrorMessage = "Please notify the administrator.
    + * Take note of any unsaved data, and click here to continue."
  • + *
  • outOfSyncURL = null
  • + *
  • outOfSyncNotificationEnabled = true
  • + *
  • outOfSyncCaption = "Out of sync"
  • + *
  • outOfSyncMessage = "Something has caused us to be out of sync + * with the server.
    + * Take note of any unsaved data, and click here to re-sync."
  • + *
  • cookiesDisabledURL = null
  • + *
  • cookiesDisabledNotificationEnabled = true
  • + *
  • cookiesDisabledCaption = "Cookies disabled"
  • + *
  • cookiesDisabledMessage = "This application requires cookies to + * function.
    + * Please enable cookies in your browser and click here to try again. + *
  • + *
+ *

+ * + */ + 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 click here 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 click here 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 click here to continue."; + + protected String internalErrorURL = null; + protected boolean internalErrorNotificationEnabled = true; + protected String internalErrorCaption = "Internal error"; + protected String internalErrorMessage = "Please notify the administrator.
Take note of any unsaved data, and click here 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.
Take note of any unsaved data, and click here 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.
Please enable cookies in your browser and click here 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 click here 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 click here 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 click here 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.
+ * Take note of any unsaved data, and click here 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.
+ * Take note of any unsaved data, and click here 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. + *

+ * 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. + *

+ *

+ * The default behavior is to show a notification, and restart the + * application the the user clicks the message.
+ * Instead of restarting the application, you can set a specific URL that + * the user is taken to.
+ * 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. + *

+ *

+ * The situations are: + *

  • Session expired: the user session has expired, usually due to + * inactivity.
  • + *
  • Communication error: the client failed to contact the server, or the + * server returned and invalid response.
  • + *
  • Internal error: unhandled critical server error (e.g out of memory, + * database crash) + *
  • 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. + *

    + */ + + 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. + * + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + * @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 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 Root class that should be used for + * a request. The class must have an accessible no-args constructor. + *

    + * The default implementation uses the {@value #ROOT_PARAMETER} parameter + * from web.xml. + *

    + *

    + * 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. + *

    + * + * @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, null 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 null 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, null 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 null 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 null if the + * annotation is not present on the class + */ + private static T getAnnotationFor(Class type, + Class 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}). + *

    + * 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. + *

    + * + * @param request + * the wrapped request to get information from + * @param response + * the response to which data can be written + * @return returns true if a {@link RequestHandler} has + * produced a response and false 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( + 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. + *

    + * Handlers are called in reverse order of addition, so the most recently + * added handler will be called first. + *

    + * + * @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 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 + * null 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 currentApplication = new ThreadLocal(); + + 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 + * null + * + * @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. + *

    + * 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. + *

    + * + * @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. + *

    + * Please note that this method can also return a newly created + * Root 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. + *

    + * + * @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 null 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. + *

    + * 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. + *

    + * + * @param rootPreserved + * trueif 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 trueif 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 true of the initialization is pending, + * false 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 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. + *

    + * This is meant for framework internal use. + *

    + * + * @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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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. + *

    + * 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. + *

    + * 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. + *

    + * 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 @@ + + + + + + + +

    Contains annotations used in Vaadin. Note that some annotations +are also found in other packages e.g., {@link com.vaadin.ui.ClientWidget}.

    + + + 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; + +/** + *

    + * Defines the interface to commit and discard changes to an object, supporting + * read-through and write-through modes. + *

    + * + *

    + * Read-through mode means that the value read from the buffered object + * is constantly up to date with the data source. Write-through mode + * means that all changes to the object are immediately updated to the data + * source. + *

    + * + *

    + * Since these modes are independent, their combinations may result in some + * behaviour that may sound surprising. + *

    + * + *

    + * For example, if a Buffered 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. + *

    + * + * @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 commit 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 + * commit being called after the modification. + * + * @return true if the object is in write-through mode, + * false 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 commit 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. + *

    + * 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. + *

    + * + * @return true if the object is in read-through mode, + * false 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. + *

    + * 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. + *

    + *

    + * 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. + *

    + *

    + * 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. + *

    + * + * @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. + *

    + * This method only returns true if both read and write buffering is used. + *

    + * + * @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 true if the value in the object has been modified + * since the last data source update, false 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; + +/** + *

    + * This interface defines the combination of Validatable and + * Buffered interfaces. The combination of the interfaces defines + * if the invalid data is committed to datasource. + *

    + * + * @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 + * false. + */ + public boolean isInvalidCommitted(); + + /** + * Sets if the invalid data should be committed to datasource. The default + * is false. + */ + 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. + *

    + * 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. + *

    + * 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. + *

    + * + */ +public interface Collapsible extends Hierarchical, Ordered { + + /** + *

    + * Collapsing the {@link Item} indicated by itemId hides all + * children, and their respective children, from the {@link Container}. + *

    + * + *

    + * If called on a leaf {@link Item}, this method does nothing. + *

    + * + * @param itemId + * the identifier of the collapsed {@link Item} + * @param collapsed + * true if you want to collapse the children below + * this {@link Item}. false if you want to + * uncollapse the children. + */ + public void setCollapsed(Object itemId, boolean collapsed); + + /** + *

    + * Checks whether the {@link Item}, identified by itemId is + * collapsed or not. + *

    + * + *

    + * If an {@link Item} is "collapsed" its children are not included in + * methods used to list Items in this container. + *

    + * + * @param itemId + * The {@link Item}'s identifier that is to be checked. + * @return true iff the {@link Item} identified by + * itemId is currently collapsed, otherwise + * false. + */ + 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; + +/** + *

    + * 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: + *

    + * + *
      + *
    • All Items in the Container must have the same number of Properties. + *
    • All Items in the Container must have the same Property ID's (see + * {@link Item#getItemPropertyIds()}). + *
    • All Properties in the Items corresponding to the same Property ID must + * have the same data type. + *
    • All Items within a container are uniquely identified by their non-null + * IDs. + *
    + * + *

    + * 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 null values. + *

    + * + *

    + * Note that though uniquely identified, the Items in a Container are not + * necessarily {@link Container.Ordered ordered} or {@link Container.Indexed + * indexed}. + *

    + * + *

    + * Containers can derive Item ID's from the item properties or use other, + * container specific or user specified identifiers. + *

    + * + *

    + * 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). + *

    + * + *

    + * + *

    + * + *

    + * The Container interface is split to several subinterfaces so that a class can + * implement only the ones it needs. + *

    + * + * @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, null 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 null 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, null 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 null + */ + 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. + * + *

    + * The new Item is returned, and it is ready to have its Properties + * modified. Returns null if the operation fails or the + * Container already contains a Item with the given ID. + *

    + * + *

    + * This functionality is optional. + *

    + * + * @param itemId + * ID of the Item to be created + * @return Created new Item, or null 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. + * + *

    + * The new ID is returned, or null if the operation fails. + * After a successful call you can use the {@link #getItem(Object ItemId) + * getItem}method to fetch the Item. + *

    + * + *

    + * This functionality is optional. + *

    + * + * @return ID of the newly created Item, or null 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 ItemId from the Container. + * + *

    + * Containers that support filtering should also allow removing an item that + * is currently filtered out. + *

    + * + *

    + * This functionality is optional. + *

    + * + * @param itemId + * ID of the Item to remove + * @return true if the operation succeeded, false + * 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 true if the operation succeeded, false + * 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 true if the operation succeeded, false + * 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. + * + *

    + * Note that Property ID and type information is preserved. This + * functionality is optional. + *

    + * + * @return true if the operation succeeded, false + * 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. + * + *

    + * If the container is filtered or sorted, the traversal applies to the + * filtered and sorted view. + *

    + *

    + * The addItemAfter() 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. + *

    + */ + public interface Ordered extends Container { + + /** + * Gets the ID of the Item following the Item that corresponds to + * itemId. If the given Item is the last or not found in + * the Container, null is returned. + * + * @param itemId + * ID of a visible Item in the Container + * @return ID of the next visible Item or null + */ + public Object nextItemId(Object itemId); + + /** + * Gets the ID of the Item preceding the Item that corresponds to + * itemId. If the given Item is the first or not found in + * the Container, null is returned. + * + * @param itemId + * ID of a visible Item in the Container + * @return ID of the previous visible Item or null + */ + 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 true if the Item is first visible item in the + * Container, false 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 true if the Item is last visible item in the + * Container, false if not + */ + public boolean isLastId(Object itemId); + + /** + * Adds a new item after the given item. + *

    + * Adding an item after null item adds the item as first item of the + * ordered container. + *

    + * + * @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. + *

    + * Adding an item after null item adds the item as first item of the + * ordered container. + *

    + * + * @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. + *

    + * 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 addItem*() methods may add + * items that will be filtered out after addition or moved to another + * position based on sorting. + *

    + *

    + * 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. + *

    + *

    + * Depending on the container type, sorting a container may permanently + * change the internal order of items in the container. + *

    + */ + 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 true to + * sort in ascending order, false 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. + *

    + * If the container is filtered or sorted, all indices refer to the filtered + * and sorted view. However, the addItemAt() methods may add + * items that will be filtered out after addition or moved to another + * position based on sorting. + *

    + */ + public interface Indexed extends Ordered { + + /** + * Gets the index of the Item corresponding to the itemId. The following + * is true 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). + *

    + * The indices of the item currently in the given position and all the + * following items are incremented. + *

    + *

    + * 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. + *

    + * + * @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). + *

    + * The indexes of the item currently in the given position and all the + * following items are incremented. + *

    + *

    + * 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. + *

    + * + * @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; + + } + + /** + *

    + * Interface for Container 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: + *

    + * + *
      + *
    • The Item structure may have more than one root elements + *
    • The Items in the hierarchy can be declared explicitly to be able or + * unable to have children. + *
    + */ + 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 null 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 root 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(); + + /** + *

    + * Sets the parent of an Item. The new parent item must exist and be + * able to have children. ( + * {@link #areChildrenAllowed(Object)} == true ). It is + * also possible to detach a node from the hierarchy (and thus make it + * root) by setting the parent null. + *

    + * + *

    + * This operation is optional. + *

    + * + * @param itemId + * ID of the item to be set as the child of the Item + * identified with newParentId + * @param newParentId + * ID of the Item that's to be the new parent of the Item + * identified with itemId + * @return true if the operation succeeded, + * false 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 true if the specified Item exists in the + * Container and it can have children, false if + * it's not found from the container or it can't have children. + */ + public boolean areChildrenAllowed(Object itemId); + + /** + *

    + * Sets the given Item's capability to have children. If the Item + * identified with itemId already has children and + * {@link #areChildrenAllowed(Object)} is false this method + * fails and false 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)}. + *

    + * + *

    + * This operation is optional. If it is not implemented, the method + * always returns false. + *

    + * + * @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 true if the operation succeeded, + * false if not + */ + public boolean setChildrenAllowed(Object itemId, + boolean areChildrenAllowed) + throws UnsupportedOperationException; + + /** + * Tests if the Item specified with itemId 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 null for root Items. + * + * @param itemId + * ID of the Item whose root status is to be tested + * @return true if the specified Item is a root, + * false if not + */ + public boolean isRoot(Object itemId); + + /** + *

    + * Tests if the Item specified with itemId has child Items + * or if it is a leaf. The {@link #getChildren(Object itemId)} method + * always returns null for leaf Items. + *

    + * + *

    + * Note that being a leaf does not imply whether or not an Item is + * allowed to have children. + *

    + * . + * + * @param itemId + * ID of the Item to be tested + * @return true if the specified Item has children, + * false if not (is a leaf) + */ + public boolean hasChildren(Object itemId); + + /** + *

    + * Removes the Item identified by ItemId from the + * Container. + *

    + * + *

    + * Note that this does not remove any children the item might have. + *

    + * + * @param itemId + * ID of the Item to remove + * @return true if the operation succeeded, + * false 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. + *

    + * 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. + *

    + *

    + * 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. + *

    + *

    + * How filtering is performed when a {@link Hierarchical} container + * implements {@link SimpleFilterable} is implementation specific and should + * be documented in the implementing class. + *

    + *

    + * 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. + *

    + *

    + * The functionality of SimpleFilterable can be implemented using the + * {@link Filterable} API and {@link SimpleStringFilter}. + *

    + * + * @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. + *

    + * 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. + *

    + *

    + * 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 addItem*() methods may add items that + * will be filtered out after addition or moved to another position based on + * sorting. + *

    + *

    + * How filtering is performed when a {@link Hierarchical} container + * implements {@link Filterable} is implementation specific and should be + * documented in the implementing class. + *

    + *

    + * 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. + *

    + * + *

    + * This API replaces the old Filterable interface, renamed to + * {@link SimpleFilterable} in Vaadin 6.6. + *

    + * + * @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(); + + } + + /** + *

    + * 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. + *

    + *

    + * Note that not implementing the Container.Editor interface + * does not restrict the class from editing the Container contents + * internally. + *

    + */ + public interface Editor extends Container.Viewer, Serializable { + + } + + /* Contents change event */ + + /** + * An Event 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 ItemSetChangeEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate a ItemSetChangeEvent 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. + * + *

    + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + */ + 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 Event 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 PropertySetChangeEvent + * 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); + } + + /** + *

    + * The interface for adding and removing PropertySetChangeEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate a PropertySetChangeEvent when the set + * of property IDs supported by the container is modified. + *

    + * + *

    + * 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. + *

    + * + *

    + * Note that the general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + */ + 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; + +/** + *

    + * 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. + *

    + * + * @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, null is + * returned. + * + * @param id + * identifier of the Property to get + * @return the Property with the given ID or null + */ + 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. + * + *

    + * This functionality is optional. + *

    + * + * @param id + * ID of the new Property + * @param property + * the Property to be added and associated with the id + * @return true if the operation succeeded, false + * 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. + * + *

    + * This functionality is optional. + *

    + * + * @param id + * ID of the Property to be removed + * @return true if the operation succeeded + * @throws UnsupportedOperationException + * if the operation is not supported. false 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 Editor 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. + *

    + * Note : Not implementing the Item.Editor interface does not + * restrict the class from editing the contents of an internally. + *

    + */ + public interface Editor extends Item.Viewer, Serializable { + + } + + /* Property set change event */ + + /** + * An Event object specifying the Item whose contents has been + * changed through the Property interface. + *

    + * Note: The values stored in the Properties may change without triggering + * this event. + *

    + */ + 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 PropertySetChangeEvent + * 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 PropertySetChangeEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate a PropertySetChangeEvent when its + * Property set is modified. + *

    + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + */ + 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; + +/** + *

    + * The Property 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. + *

    + * + *

    + * The Property also defines the events + * ReadOnlyStatusChangeEvent and ValueChangeEvent, and + * the associated listener and notifier interfaces. + *

    + * + *

    + * The Property.Viewer 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 Property interface. + *

    + * + *

    + * The Property.editor interface should be implemented if the value + * needs to be changed through the implementing class. + *

    + * + * @param T + * type of values of the property + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Property 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. + *

    + * Implementing this functionality is optional. If the functionality is + * missing, one should declare the Property to be in read-only mode and + * throw Property.ReadOnlyException in this function. + *

    + * + * 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 getValue and + * setValue must be compatible with this type: one must be able + * to safely cast the value returned from getValue to the given + * type and pass any variable assignable to this type as an argument to + * setValue. + * + * @return type of the Property + */ + public Class getType(); + + /** + * Tests if the Property is in read-only mode. In read-only mode calls to + * the method setValue will throw + * ReadOnlyException and will not modify the value of the + * Property. + * + * @return true if the Property is in read-only mode, + * false 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 + * isReadOnly 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 + * The type of the property + * @author Vaadin Ltd + * @version @version@ + * @since 7.0 + */ + public interface Transactional extends Property { + + /** + * Starts a transaction. + * + *

    + * 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. + *

    + *

    + * {@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. + *

    + */ + public void startTransaction(); + + /** + * Commits and ends the transaction that is in progress. + *

    + * If the value is changed as a result of this operation, a + * {@link ValueChangeEvent} is emitted if such are supported. + *

    + * This method has no effect if there is no transaction is in progress. + *

    + * This method must never throw an exception. + */ + public void commit(); + + /** + * Aborts and rolls back the transaction that is in progress. + *

    + * The value is reset to the value before the transaction started. No + * {@link ValueChangeEvent} is emitted as a result of this. + *

    + * This method has no effect if there is no transaction is in progress. + *

    + * This method must never throw an exception. + */ + public void rollback(); + } + + /** + * Exception 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 ReadOnlyException without a detail + * message. + */ + public ReadOnlyException() { + } + + /** + * Constructs a new ReadOnlyException 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. + *

    + * 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 + * ReadOnlyException being thrown. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Editor extends Property.Viewer, Serializable { + + } + + /* Value change event */ + + /** + * An Event 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 listener interface for receiving + * ValueChangeEvent 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 ValueChangeEvent + * listeners. If a Property wishes to allow other objects to receive + * ValueChangeEvent generated by it, it must implement this + * interface. + *

    + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @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 Event 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 + * ReadOnlyStatusChangeEvent 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 + * ReadOnlyStatusChangeEvent listeners. If a Property wishes to + * allow other objects to receive ReadOnlyStatusChangeEvent + * generated by it, it must implement this interface. + *

    + * Note : The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @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; + +/** + *

    + * 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. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @see com.vaadin.data.Validator + */ +public interface Validatable extends Serializable { + + /** + *

    + * 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. + *

    + * + * @param validator + * the new validator + */ + void addValidator(Validator validator); + + /** + *

    + * Removes a previously registered validator from the object. The specified + * validator is removed from the object and its validate method + * is no longer called in {@link #isValid()}. + *

    + * + * @param validator + * the validator to remove + */ + void removeValidator(Validator validator); + + /** + *

    + * Lists all validators currently registered for the object. If no + * validators are registered, returns null. + *

    + * + * @return collection of validators or null + */ + public Collection getValidators(); + + /** + *

    + * 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 + * false. + *

    + * + * @return true if the registered validators concur that the + * value is valid, false otherwise + */ + public boolean isValid(); + + /** + *

    + * 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 + * Validator.InvalidValueException + *

    + * + * @throws Validator.InvalidValueException + * if the value is not valid + */ + public void validate() throws Validator.InvalidValueException; + + /** + *

    + * Checks the validabtable object accept invalid values.The default value is + * true. + *

    + * + */ + public boolean isInvalidAllowed(); + + /** + *

    + * Should the validabtable object accept invalid values. Supporting this + * configuration possibility is optional. By default invalid values are + * allowed. + *

    + * + * @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. + *

    + * Implementors of this class can be added to any + * {@link com.vaadin.data.Validatable Validatable} implementor to verify its + * value. + *

    + *

    + * {@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. + *

    + *

    + * Validators must not have any side effects. + *

    + *

    + * 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. + *

    + * + * @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. + * + *

    + * 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. + *

    + * + * @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 extends FieldGroup { + + private Class beanType; + + private static Boolean beanValidationImplementationAvailable = null; + + public BeanFieldGroup(Class 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 getItemDataSource() { + return (BeanItem) 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 createField(Class type, Class 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 createEnumField(Class type, + Class fieldType) { + if (AbstractSelect.class.isAssignableFrom(fieldType)) { + AbstractSelect s = createCompatibleSelect((Class) fieldType); + populateWithEnumData(s, (Class) type); + return (T) s; + } + + return null; + } + + protected AbstractSelect createCompatibleSelect( + Class 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 createBooleanField(Class 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) fieldType); + } + + return null; + } + + protected T createAbstractTextField( + Class fieldType) { + if (fieldType == AbstractTextField.class) { + fieldType = (Class) 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 + * 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 createDefaultField(Class type, + Class 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 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. + *

    + * 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. + *

    + *

    + * {@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. + *

    + * + * @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> propertyIdToField = new HashMap>(); + private LinkedHashMap, Object> fieldToPropertyId = new LinkedHashMap, Object>(); + private List commitHandlers = new ArrayList(); + + /** + * 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. + *

    + * + * @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. + *

    + * 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. + *

    + *

    + * The default is to use buffered mode. + *

    + * + * @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. + *

    + * 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. + *

    + * 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. + *

    + * The fields are not returned in any specific order. + *

    + * + * @return A collection with all bound Fields + */ + public Collection> 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)}. + *

    + * This method also adds validators when applicable. + *

    + * + * @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 Property.Transactional wrapInTransactionalProperty( + Property itemProperty) { + return new TransactionalPropertyWrapper(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. + *

    + * 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. + *

    + * 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. + *

    + * 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. + *

    + *

    + * No guarantee is given for the order of the property ids + *

    + * + * @return A collection of bound property ids + */ + public Collection 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. + *

    + * Will always return an empty collection before an item has been set using + * {@link #setItemDataSource(Item)}. + *

    + *

    + * No guarantee is given for the order of the property ids + *

    + * + * @return A collection of property ids that have not been bound to fields + */ + public Collection getUnboundPropertyIds() { + if (getItemDataSource() == null) { + return new ArrayList(); + } + List unboundPropertyIds = new ArrayList(); + unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds()); + unboundPropertyIds.removeAll(propertyIdToField.keySet()); + return unboundPropertyIds; + } + + /** + * Commits all changes done to the bound fields. + *

    + * 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. + *

    + * 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. + *

    + * 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}. + *

    + * Use {@link #addCommitHandler(CommitHandler)} and + * {@link #removeCommitHandler(CommitHandler)} to register or unregister a + * commit handler. + * + * @return A collection of commit handlers + */ + protected Collection 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. + *

    + * 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.. + *

    + * 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. + *

    + * 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. + *

    + * 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. + *

    + *

    + * For example: + * + *

    +     * 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);
    +     * 
    + * + *

    + * 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. + *

    + * 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. + *

    + *

    + * For example: + * + *

    +     * 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);
    +     * 
    + * + *

    + *

    + * 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. + *

    + * + * @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. + *

    + * 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. + *

    + * + * @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 fieldType = (Class) 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 buildAndBind(String caption, Object propertyId, + Class 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. + *

    + * 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. + *

    + * + * @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 build(String caption, Class dataType, + Class 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 createField(Class dataType, Class 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 @@ + + + + + + + +

    Contains interfaces for the data layer, mainly for binding typed +data and data collections to components, and for validating data.

    + +

    Data binding

    + +

    The package contains a three-tiered structure for typed data +objects and collections of them:

    + +
      +
    • A {@link com.vaadin.data.Property Property} represents a + single, typed data value. + +
    • An {@link com.vaadin.data.Item Item} embodies a set of Properties. + A locally unique (inside the {@link com.vaadin.data.Item Item}) + Property identifier corresponds to each Property inside the Item.
    • +
    • 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.
    • +
    + +

    For more information on the data model, see the Data model +chapter in Book of Vaadin.

    + +

    Buffering

    + +

    A {@link com.vaadin.data.Buffered Buffered} implementor is able +to track and buffer changes and commit or discard them later.

    + +

    Validation

    + +

    {@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.

    + + + + 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. + * + *

    + * 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. + *

    + * + *

    + * 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)}. + *

    + * + * @param + * The type of the item identifier + * @param + * The type of the Bean + * + * @since 6.5 + */ +public abstract class AbstractBeanContainer extends + AbstractInMemoryContainer> 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 + * @param + * + * @since 6.5 + */ + public static interface BeanIdResolver 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 { + + 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 pd = model.get(propertyId); + if (null == pd) { + throw new IllegalStateException("Property " + propertyId + + " not found"); + } + try { + Property property = (Property) 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 beanIdResolver = null; + + /** + * Maps all item ids in the container (including filtered) to their + * corresponding BeanItem. + */ + private final Map> itemIdToItem = new HashMap>(); + + /** + * The type of the beans in the container. + */ + private final Class 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> 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 type) { + if (type == null) { + throw new IllegalArgumentException( + "The bean type passed to AbstractBeanContainer must not be null"); + } + this.type = type; + model = BeanItem.getPropertyDescriptors((Class) 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 createBeanItem(BEANTYPE bean) { + return bean == null ? null : new BeanItem(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 getBeanType() { + return type; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getContainerPropertyIds() + */ + @Override + public Collection 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 getItem(Object itemId) { + // TODO return only if visible? + return getUnfilteredItem(itemId); + } + + @Override + protected BeanItem getUnfilteredItem(Object itemId) { + return itemIdToItem.get(itemId); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container#getItemIds() + */ + @Override + @SuppressWarnings("unchecked") + public List getItemIds() { + return (List) 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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> pds = BeanItem + .getPropertyDescriptors((Class) propertyType); + for (String subPropertyId : pds.keySet()) { + String qualifiedPropertyId = propertyId + "." + subPropertyId; + NestedPropertyDescriptor pd = new NestedPropertyDescriptor( + qualifiedPropertyId, (Class) type); + model.put(qualifiedPropertyId, pd); + model.remove(propertyId); + for (BeanItem 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 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 propertySetChangeListeners = null; + + /** + * List of all container Item set change event listeners. + */ + private Collection itemSetChangeListeners = null; + + /** + * An event 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 event 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()); + } + 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()); + } + 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 propertySetChangeListeners) { + this.propertySetChangeListeners = propertySetChangeListeners; + } + + /** + * Returns the property set change listener collection. For internal use + * only. + */ + protected Collection getPropertySetChangeListeners() { + return propertySetChangeListeners; + } + + /** + * Sets the item set change listener collection. For internal use only. + * + * @param itemSetChangeListeners + */ + protected void setItemSetChangeListeners( + Collection itemSetChangeListeners) { + this.itemSetChangeListeners = itemSetChangeListeners; + } + + /** + * Returns the item set change listener collection. For internal use only. + */ + protected Collection 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: + *
      + *
    • {@link Container.Ordered} + *
    • {@link Container.Indexed} + *
    • {@link Filterable} and {@link SimpleFilterable} (internal implementation, + * does not implement the interface directly) + *
    • {@link Sortable} (internal implementation, does not implement the + * interface directly) + *
    + * + * To implement {@link Sortable}, subclasses need to implement + * {@link #getSortablePropertyIds()} and call the superclass method + * {@link #sortContainer(Object[], boolean[])} in the method + * sort(Object[], boolean[]). + * + * 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 + * the class of item identifiers in the container, use Object if can + * be any class + * @param + * the class of property identifiers for the items in the container, + * use Object if can be any class + * @param + * the (base) class of the Item instances in the container, use + * {@link Item} if unknown + * + * @since 6.6 + */ +public abstract class AbstractInMemoryContainer + 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 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 filteredItemIds; + + /** + * Filters that are applied to the container to limit the items visible in + * it + */ + private Set filters = new HashSet(); + + /** + * 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()); + } + + // 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 cannot + // be cast to Collection + + // public abstract Collection getContainerPropertyIds(); + // public abstract Collection 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 originalFilteredItemIds = getFilteredItemIds(); + boolean wasUnfiltered = false; + if (originalFilteredItemIds == null) { + originalFilteredItemIds = Collections.emptyList(); + wasUnfiltered = true; + } + setFilteredItemIds(new ListSet()); + + // Filter + boolean equal = true; + Iterator origIt = originalFilteredItemIds.iterator(); + for (final Iterator 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 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 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 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 removed filters + */ + protected Collection removeFilters(Object propertyId) { + if (getFilters().isEmpty() || propertyId == null) { + return Collections.emptyList(); + } + List removedFilters = new LinkedList(); + for (Iterator 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 itemSorter has been prepared for the sort + * operation. Typically this method calls + * Collections.sort(aCollection, getItemSorter()) 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 sortables = new LinkedList(); + 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. + * + *

    + * Caller should initiate filtering after calling this method. + *

    + * + * 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 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 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 + */ + protected List 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 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 + */ + protected List 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 + * remove*Filter* (which also re-filter the container) instead + * when possible. + * + * @param filters + */ + protected void setFilters(Set filters) { + this.filters = filters; + } + + /** + * Returns the internal collection of filters. The returned collection + * should not be modified by callers outside this class. + * + * @return Set + */ + protected Set 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 implements Property, + Property.ValueChangeNotifier, Property.ReadOnlyStatusChangeNotifier { + + /** + * List of listeners who are interested in the read-only status changes of + * the Property + */ + private LinkedList readOnlyStatusChangeListeners = null; + + /** + * List of listeners who are interested in the value changes of the Property + */ + private LinkedList 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 Property 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 Event 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(); + } + 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 Event 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(); + } + 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. + * + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + *

    + * To use explicit item IDs, use the methods {@link #addItem(Object, Object)}, + * {@link #addItemAfter(Object, Object, Object)} and + * {@link #addItemAt(int, Object, Object)}. + *

    + * + *

    + * 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). + *

    + * + *

    + * 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. + *

    + * + *

    + * It is not possible to add additional properties to the container and nested + * bean properties are not supported. + *

    + * + * @param + * The type of the item identifier + * @param + * The type of the Bean + * + * @see AbstractBeanContainer + * @see BeanItemContainer + * + * @since 6.5 + */ +public class BeanContainer extends + AbstractBeanContainer { + + public BeanContainer(Class type) { + super(type); + } + + /** + * Adds the bean to the Container. + * + * @see com.vaadin.data.Container#addItem(Object) + */ + @Override + public BeanItem 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 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 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 beanIdResolver) { + super.setBeanIdResolver(beanIdResolver); + } + + @Override + // overridden to make public + public BeanItem addBean(BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBean(bean); + } + + @Override + // overridden to make public + public BeanItem addBeanAfter(IDTYPE previousItemId, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAfter(previousItemId, bean); + } + + @Override + // overridden to make public + public BeanItem addBeanAt(int index, BEANTYPE bean) + throws IllegalStateException, IllegalArgumentException { + return super.addBeanAt(index, bean); + } + + @Override + // overridden to make public + public void addAll(Collection 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 extends PropertysetItem { + + /** + * The bean which this Item is based on. + */ + private final BT bean; + + /** + *

    + * Creates a new instance of BeanItem and adds all properties + * of a Java Bean to it. The properties are identified by their respective + * bean names. + *

    + * + *

    + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone is and + * are methods are not supported. + *

    + * + * @param bean + * the Java Bean to copy properties from. + * + */ + public BeanItem(BT bean) { + this(bean, getPropertyDescriptors((Class) bean.getClass())); + } + + /** + *

    + * Creates a new instance of BeanItem using a pre-computed set + * of properties. The properties are identified by their respective bean + * names. + *

    + * + * @param bean + * the Java Bean to copy properties from. + * @param propertyDescriptors + * pre-computed property descriptors + */ + BeanItem(BT bean, + Map> propertyDescriptors) { + + this.bean = bean; + + for (VaadinPropertyDescriptor pd : propertyDescriptors.values()) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + /** + *

    + * Creates a new instance of BeanItem and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + *

    + * + *

    + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone is and + * are methods are not supported. + *

    + * + * @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> pds = getPropertyDescriptors((Class) bean + .getClass()); + + // Add all the bean properties as MethodProperties to this Item + for (Object id : propertyIds) { + VaadinPropertyDescriptor pd = pds.get(id); + if (pd != null) { + addItemProperty(pd.getName(), pd.createProperty(bean)); + } + } + + } + + /** + *

    + * Creates a new instance of BeanItem and adds all listed + * properties of a Java Bean to it - in specified order. The properties are + * identified by their respective bean names. + *

    + * + *

    + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone is and + * are methods are not supported. + *

    + * + * @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)); + } + + /** + *

    + * Perform introspection on a Java Bean class to find its properties. + *

    + * + *

    + * Note : This version only supports introspectable bean properties and + * their getter and setter methods. Stand-alone is and + * are methods are not supported. + *

    + * + * @param beanClass + * the Java Bean class to get properties for. + * @return an ordered map from property names to property descriptors + */ + static LinkedHashMap> getPropertyDescriptors( + final Class beanClass) { + final LinkedHashMap> pdMap = new LinkedHashMap>(); + + // Try to introspect, if it fails, we just have an empty Item + try { + List 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 vaadinPropertyDescriptor = new MethodPropertyDescriptor( + 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 getBeanPropertyDescriptor( + final Class beanClass) throws IntrospectionException { + // Oracle bug 4275879: Introspector does not consider superinterfaces of + // an interface + if (beanClass.isInterface()) { + List propertyDescriptors = new ArrayList(); + + 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 subPropertySet = new HashSet( + Arrays.asList(subPropertyIds)); + + if (0 == subPropertyIds.length) { + // Enumerate all sub-properties + Class propertyType = getItemProperty(propertyId).getType(); + Map 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( + 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. + * + *

    + * 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. + *

    + * + *

    + * 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()}. + *

    + * + *

    + * 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. + *

    + * + *

    + * It is not possible to add additional properties to the container and nested + * bean properties are not supported. + *

    + * + * @param + * The type of the Bean + * + * @since 5.4 + */ +@SuppressWarnings("serial") +public class BeanItemContainer extends + AbstractBeanContainer { + + /** + * 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 + * + * @since 6.5 + */ + private static class IdentityBeanIdResolver implements + BeanIdResolver { + + @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 type) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver()); + } + + /** + * 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 collection) + throws IllegalArgumentException { + // must assume the class is BT + // the class information is erased by the compiler + this((Class) getBeanClassForCollection(collection), + collection); + } + + /** + * Internal helper method to support the deprecated {@link Collection} + * container. + * + * @param + * @param collection + * @return + * @throws IllegalArgumentException + */ + @SuppressWarnings("unchecked") + @Deprecated + private static Class getBeanClassForCollection( + Collection 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) 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 type, + Collection collection) + throws IllegalArgumentException { + super(type); + super.setBeanIdResolver(new IdentityBeanIdResolver()); + + 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 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 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 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 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 addBean(BEANTYPE bean) { + return addItem(bean); + } + + /** + * Unsupported in BeanItemContainer. + */ + @Override + protected void setBeanIdResolver( + AbstractBeanContainer.BeanIdResolver 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; + +/** + *

    + * A wrapper class for adding external hierarchy to containers not implementing + * the {@link com.vaadin.data.Container.Hierarchical} interface. + *

    + * + *

    + * 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. + *

    + * + * @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 noChildrenAllowed = null; + + /** Mapping from Item ID to parent Item ID */ + private Hashtable parent = null; + + /** Mapping from Item ID to a list of child IDs */ + private Hashtable> children = null; + + /** List that contains all root elements of the container. */ + private LinkedHashSet 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, 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 + * Container.Hierarchical 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(); + parent = new Hashtable(); + children = new Hashtable>(); + roots = new LinkedHashSet(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(); + parent = new Hashtable(); + children = new Hashtable>(); + roots = new LinkedHashSet(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 basedOnOrderFromWrappedContainer = new ListedItemsFirstComparator( + itemIds); + + // Calculate the set of all items in the hierarchy + final HashSet s = new HashSet(); + s.addAll(parent.keySet()); + s.addAll(children.keySet()); + s.addAll(roots); + + // Remove unnecessary items + for (final Iterator 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(); + for (int i = 0; i < array.length; i++) { + roots.add(array[i]); + } + for (Object object : children.keySet()) { + LinkedList object2 = children.get(object); + Collections.sort(object2, basedOnOrderFromWrappedContainer); + } + + } + } + } + + /** + * Removes the specified Item from the wrapper's internal hierarchy + * structure. + *

    + * Note : The Item is not removed from the underlying Container. + *

    + * + * @param itemId + * the ID of the item to remove from the hierarchy. + */ + private void removeFromHierarchyWrapper(Object itemId) { + + LinkedList 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 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 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); + } + + /** + *

    + * 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 false 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)}. + *

    + * + * @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 true if the operation succeeded, false + * 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; + } + + /** + *

    + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (canHaveChildren(newParentId) == true). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent null. + *

    + * + * @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 true if the operation succeeded, false + * 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 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 pcl = children.get(newParentId); + if (pcl == null) { + pcl = new LinkedList(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Remove from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList 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 null 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 null 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 true if the operation succeeded, false + * 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 true if the operation succeeded, false + * 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 true if the operation succeeded, false + * 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. + *

    + * Note : The Property will be removed from all Items in the Container. + *

    + * + * @param propertyId + * the ID of the Property to remove. + * @return true if the operation succeeded, false + * 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; + +/** + *

    + * A wrapper class for adding external ordering to containers not implementing + * the {@link com.vaadin.data.Container.Ordered} interface. + *

    + * + *

    + * 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. + *

    + * + * @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 next; + + /** + * Reverse ordering information for convenience and performance reasons. + */ + private Hashtable 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. + *

    + * Note : The Item is not removed from the underlying Container. + *

    + * + * @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. + *

    + * 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. + *

    + */ + 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(); + prev = new Hashtable(); + } + + // Filter out all the missing items + final LinkedList l = new LinkedList(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 true if the operation succeeded, false + * 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 null 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 null 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 true if the operation succeeded, otherwise + * false + * @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 true if the operation succeeded, false + * 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. + *

    + * Note : The Property will be removed from all the Items in the Container. + *

    + * + * @param propertyId + * the ID of the Property to remove. + * @return true if the operation succeeded, false + * 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 + * DefaultItemSorter adheres to the + * {@link Sortable#sort(Object[], boolean[])} rules and sorts the container + * according to the properties given using + * {@link #setSortProperties(Sortable, Object[], boolean[])}. + *

    + * A Comparator is used for comparing the individual Property + * 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 propertyValueComparator; + + /** + * Constructs a DefaultItemSorter using the default Comparator + * for comparing Propertyvalues. + * + */ + public DefaultItemSorter() { + this(new DefaultPropertyValueComparator()); + } + + /** + * Constructs a DefaultItemSorter which uses the Comparator + * indicated by the propertyValueComparator parameter for + * comparing Propertyvalues. + * + * @param propertyValueComparator + * The comparator to use when comparing individual + * Property values + */ + public DefaultItemSorter(Comparator 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 propertyId in the items + * indicated by item1 and item2 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 sortDirection is false the + * returned value is negated. + *

    + * The comparator set for this DefaultItemSorter 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 ids = new ArrayList(); + final List orders = new ArrayList(); + 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 DefaultPropertyValueComparator assumes all objects it + * compares can be cast to Comparable. + * + */ + public static class DefaultPropertyValueComparator implements + Comparator, 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) 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 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(); + 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 FileSystemContainer 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 FileSystemContainer 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 FileSystemContainer 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 FileSystemContainer 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 FilesystemContainer. + * + * @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 + * FileSystemContainer contains files and directories, this + * method returns true for directory Items only. + * + * @param itemId + * the id of the item. + * @return true if the specified Item is a directory, + * false 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 getChildren(Object itemId) { + + if (!(itemId instanceof File)) { + return Collections.unmodifiableCollection(new LinkedList()); + } + File[] f; + if (filter != null) { + f = ((File) itemId).listFiles(filter); + } else { + f = ((File) itemId).listFiles(); + } + + if (f == null) { + return Collections.unmodifiableCollection(new LinkedList()); + } + + final List 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 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()); + } + + final List l = Arrays.asList(f); + Collections.sort(l); + + return Collections.unmodifiableCollection(l); + } + + /** + * Returns false 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 true if the operaton is successful otherwise + * false. + * @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 false 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 true if the operation is successful otherwise + * false. + * @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 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 ll = Arrays.asList(l); + Collections.sort(ll); + + for (final Iterator 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 getItemIds() { + + if (recursive) { + final Collection col = new ArrayList(); + 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()); + } + + final List 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, null 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 null + */ + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + + if (!(itemId instanceof File)) { + return null; + } + + if (propertyId.equals(PROPERTY_NAME)) { + return new MethodProperty(getType(propertyId), + new FileItem((File) itemId), FILEITEM_NAME, null); + } + + if (propertyId.equals(PROPERTY_ICON)) { + return new MethodProperty(getType(propertyId), + new FileItem((File) itemId), FILEITEM_ICON, null); + } + + if (propertyId.equals(PROPERTY_SIZE)) { + return new MethodProperty(getType(propertyId), + new FileItem((File) itemId), FILEITEM_SIZE, null); + } + + if (propertyId.equals(PROPERTY_LASTMODIFIED)) { + return new MethodProperty(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 getContainerPropertyIds() { + return FILE_PROPERTIES; + } + + /** + * Gets the specified property's data type. "Name" is a String, + * "Size" is a Long, "Last Modified" is a Date. If + * propertyId is not one of those, null is returned. + * + * @param propertyId + * the ID of the property whose type is requested. + * @return data type of the requested property, or null + */ + @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 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 true if the given object is the same as this + * object, false 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. null 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 true if container is recursive, false + * otherwise. + */ + public boolean isRecursive() { + return recursive; + } + + /** + * Sets the container recursive property. Set this to false to limit the + * files directly under the root file. + *

    + * Note : This is meaningful only if the root really is a directory. + *

    + * + * @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 noChildrenAllowed = new HashSet(); + + /** + * Mapping from Item ID to parent Item ID. + */ + private final HashMap parent = new HashMap(); + + /** + * Mapping from Item ID to parent Item ID for items included in the filtered + * container. + */ + private HashMap filteredParent = null; + + /** + * Mapping from Item ID to a list of child IDs. + */ + private final HashMap> children = new HashMap>(); + + /** + * Mapping from Item ID to a list of child IDs when filtered + */ + private HashMap> filteredChildren = null; + + /** + * List that contains all root elements of the container. + */ + private final LinkedList roots = new LinkedList(); + + /** + * List that contains all filtered root elements of the container. + */ + private LinkedList 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 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); + } + } + + /** + *

    + * 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 false 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)}. + *

    + * + * @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 true if the operation succeeded, false + * 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; + } + + /** + *

    + * Sets the parent of an Item. The new parent item must exist and be able to + * have children. (canHaveChildren(newParentId) == true). It is + * also possible to detach a node from the hierarchy (and thus make it root) + * by setting the parent null. + *

    + * + * @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 true if the operation succeeded, false + * 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 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 pcl = children.get(newParentId); + if (pcl == null) { + // Create an empty list for holding children if one were not + // previously created + pcl = new LinkedList(); + children.put(newParentId, pcl); + } + pcl.add(itemId); + + // Removes from old parent or root + if (oldParentId == null) { + roots.remove(itemId); + } else { + final LinkedList 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 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 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 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 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 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(); + filteredChildren = new HashMap>(); + filteredParent = new HashMap(); + + if (includeParentsWhenFiltering) { + // Filter so that parents for items that match the filter are also + // included + HashSet includedItems = new HashSet(); + 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 filteredItemIds = new LinkedHashSet( + 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 parentToChildrenList = filteredChildren + .get(parentItemId); + if (parentToChildrenList == null) { + parentToChildrenList = new LinkedList(); + 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 includedItems) { + LinkedList 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 includedItems) { + boolean toBeIncluded = passesFilters(itemId); + + LinkedList 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 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 {@link Container.Indexed} interface + * with all important features.

    + * + * Features: + *
      + *
    • {@link Container.Indexed} + *
    • {@link Container.Ordered} + *
    • {@link Container.Sortable} + *
    • {@link Container.Filterable} + *
    • {@link Cloneable} (deprecated, might be removed in the future) + *
    • Sends all needed events on content changes. + *
    + * + * @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 implements + Container.PropertySetChangeNotifier, Property.ValueChangeNotifier, + Container.Sortable, Cloneable, Container.Filterable, + Container.SimpleFilterable { + + /* Internal structure */ + + /** + * Linked list of ordered Property IDs. + */ + private ArrayList propertyIds = new ArrayList(); + + /** + * Property ID to type mapping. + */ + private Hashtable> types = new Hashtable>(); + + /** + * Hash of Items, where each Item is implemented as a mapping from Property + * ID to Property value. + */ + private Hashtable> items = new Hashtable>(); + + /** + * Set of properties that are read-only. + */ + private HashSet> readOnlyProperties = new HashSet>(); + + /** + * List of all Property value change event listeners listening all the + * properties. + */ + private LinkedList 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>> singlePropertyValueChangeListeners = null; + + private HashMap 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(); + } + 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 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 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 t = new Hashtable(); + items.put(newItemId, t); + addDefaultValues(t); + } + + /* Event notifiers */ + + /** + * An event 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 event 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(); + } + 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> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(source.propertyId); + if (propertySetToListenerListMap != null) { + final List 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>>(); + } + Map> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap == null) { + propertySetToListenerListMap = new Hashtable>(); + singlePropertyValueChangeListeners.put(propertyId, + propertySetToListenerListMap); + } + List listenerList = propertySetToListenerListMap + .get(itemId); + if (listenerList == null) { + listenerList = new LinkedList(); + 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> propertySetToListenerListMap = singlePropertyValueChangeListeners + .get(propertyId); + if (propertySetToListenerListMap != null) { + final List 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 String representation of the contents of the + * Item. The format of the string is a space separated catenation of the + * String representations of the values of the Properties + * contained by the Item. + * + * @return String 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 true if the given object is the same as this + * object, false 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, + 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 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 setValue + * method if the Property is not in read-only mode. + * + * @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 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 true if the given object is the same as this + * object, false 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) ((ListSet) getAllItemIds()) + .clone() : null); + nc.setItemSetChangeListeners(getItemSetChangeListeners() != null ? new LinkedList( + getItemSetChangeListeners()) : null); + nc.propertyIds = propertyIds != null ? (ArrayList) propertyIds + .clone() : null; + nc.setPropertySetChangeListeners(getPropertySetChangeListeners() != null ? new LinkedList( + getPropertySetChangeListeners()) : null); + nc.propertyValueChangeListeners = propertyValueChangeListeners != null ? (LinkedList) propertyValueChangeListeners + .clone() : null; + nc.readOnlyProperties = readOnlyProperties != null ? (HashSet>) readOnlyProperties + .clone() : null; + nc.singlePropertyValueChangeListeners = singlePropertyValueChangeListeners != null ? (Hashtable>>) singlePropertyValueChangeListeners + .clone() : null; + + nc.types = types != null ? (Hashtable>) types.clone() + : null; + + nc.setFilters((HashSet) ((HashSet) getFilters()) + .clone()); + + nc.setFilteredItemIds(getFilteredItemIds() == null ? null + : (ListSet) ((ListSet) getFilteredItemIds()) + .clone()); + + // Clone property-values + if (items == null) { + nc.items = null; + } else { + nc.items = new Hashtable>(); + for (final Iterator i = items.keySet().iterator(); i.hasNext();) { + final Object id = i.next(); + final Hashtable it = (Hashtable) items + .get(id); + nc.items.put(id, (Map) 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 ItemSorter interface can be used in Sortable + * implementations to provide a custom sorting method. + */ +public interface ItemSorter extends Comparator, Cloneable, Serializable { + + /** + * Sets the parameters for an upcoming sort operation. The parameters + * determine what container to sort and how the ItemSorter + * sorts the container. + * + * @param container + * The container that will be sorted. The container must contain + * the propertyIds given in the propertyId + * 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 + * container.getSortableContainerPropertyIds(). 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. + *

    + * The parameters for the ItemSorter compare() + * 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 extends ArrayList { + private HashSet 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 duplicates = new HashMap(); + + public ListSet() { + super(); + itemSet = new HashSet(); + } + + public ListSet(Collection c) { + super(c); + itemSet = new HashSet(c.size()); + itemSet.addAll(c); + } + + public ListSet(int initialCapacity) { + super(initialCapacity); + itemSet = new HashSet(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 c) { + boolean modified = false; + Iterator 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 c) { + ensureCapacity(size() + c.size()); + + boolean modified = false; + Iterator 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 toRemove = new HashSet(); + 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 v = (ListSet) super.clone(); + v.itemSet = new HashSet(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; + +/** + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + *

    + * A valid getter method must always be available, but instance of this class + * can be constructed with a null setter method in which case the + * resulting MethodProperty is read-only. + *

    + * + *

    + * 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. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class MethodProperty extends AbstractProperty { + + /** + * 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 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 class1 = (Class) 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); + } + }; + + /** + *

    + * Creates a new instance of MethodProperty 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. + *

    + *

    + * The getter method of a MethodProperty instantiated with this + * constructor will be called with no arguments, and the setter method with + * only the new value as the sole argument. + *

    + * + *

    + * If the setter method is unavailable, the resulting + * MethodProperty will be read-only, otherwise it will be + * read-write. + *

    + * + *

    + * 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. + *

    + * + * @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) convertPrimitiveType(returnType); + if (type.isPrimitive()) { + throw new MethodException(this, "Bean property " + + beanPropertyName + + " getter return type must not be void"); + } + } else { + type = (Class) returnType; + } + + setArguments(new Object[] {}, new Object[] { null }, 0); + this.instance = instance; + } + + /** + *

    + * Creates a new instance of MethodProperty from named getter + * and setter methods. The getter method of a MethodProperty + * instantiated with this constructor will be called with no arguments, and + * the setter method with only the new value as the sole argument. + *

    + * + *

    + * If the setter method is null, the resulting + * MethodProperty will be read-only, otherwise it will be + * read-write. + *

    + * + * @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 type, Object instance, + String getMethodName, String setMethodName) { + this(type, instance, getMethodName, setMethodName, new Object[] {}, + new Object[] { null }, 0); + } + + /** + *

    + * Creates a new instance of MethodProperty with the getter and + * setter methods. The getter method of a MethodProperty + * instantiated with this constructor will be called with no arguments, and + * the setter method with only the new value as the sole argument. + *

    + * + *

    + * If the setter method is null, the resulting + * MethodProperty will be read-only, otherwise it will be + * read-write. + *

    + * + * @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 type, Object instance, + Method getMethod, Method setMethod) { + this(type, instance, getMethod, setMethod, new Object[] {}, + new Object[] { null }, 0); + } + + /** + *

    + * Creates a new instance of MethodProperty from named getter + * and setter methods and argument lists. The getter method of a + * MethodProperty 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. + *

    + * + *

    + * For example, if the setArgs contains A, + * B and C, and setArgumentIndex = + * 1, the call methodProperty.setValue(X) would result + * in the setter method to be called with the parameter set of + * {A, X, C} + *

    + * + * @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 setArgs to be + * replaced with newValue when + * {@link #setValue(Object newValue)} is called. + */ + @SuppressWarnings("unchecked") + public MethodProperty(Class 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) convertPrimitiveType(type); + + setArguments(getArgs, setArgs, setArgumentIndex); + this.instance = instance; + } + + /** + *

    + * Creates a new instance of MethodProperty from the getter and + * setter methods, and argument lists. + *

    + *

    + * 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. + *

    + * + * @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 setArgs to be + * replaced with newValue when + * {@link #setValue(Object newValue)} is called. + */ + @SuppressWarnings("unchecked") + // cannot use "Class" 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 convertedType = (Class) 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 getValue and + * setValue must be compatible with this type: one must be able + * to safely cast the value returned from getValue to the given + * type and pass any variable assignable to this type as an argument to + * setValue. + * + * @return type of the Property + */ + @Override + public final Class getType() { + return type; + } + + /** + * Tests if the object is in read-only mode. In read-only mode calls to + * setValue will throw ReadOnlyException and will + * not modify the value of the Property. + * + * @return true if the object is in read-only mode, + * false 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); + } + } + + /** + *

    + * Sets the setter method and getter method argument lists. + *

    + * + * @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 setArgs to be + * replaced with newValue 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 Property.ReadOnlyException 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); + } + } + + /** + * Exception 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 MethodException 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 MethodException 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 + * bean type + * + * @since 6.6 + */ +public class MethodPropertyDescriptor implements + VaadinPropertyDescriptor { + + 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 class1 = (Class) 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(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 extends AbstractProperty { + + // needed for de-serialization + private String propertyName; + + // chain of getter methods + private transient List 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 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 getMethods = new ArrayList(); + + 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) MethodProperty + .convertPrimitiveType(type); + this.propertyName = propertyName; + this.getMethods = getMethods; + this.setMethod = setMethod; + } + + @Override + public Class 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 Property.ReadOnlyException 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 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 + * bean type + * + * @since 6.6 + */ +public class NestedPropertyDescriptor implements + VaadinPropertyDescriptor { + + 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 beanType) + throws IllegalArgumentException { + this.name = name; + NestedMethodProperty property = new NestedMethodProperty( + 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(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 extends AbstractProperty { + + /** + * The value contained by the Property. + */ + private T value; + + /** + * Data type of the Property's value. + */ + private final Class 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 + public ObjectProperty(T value) { + this(value, (Class) 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 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. value must be assignable + * to this type. + * @param readOnly + * Sets the read-only mode. + */ + public ObjectProperty(T value, Class type, boolean readOnly) { + this(value, type); + setReadOnly(readOnly); + } + + /** + * Returns the type of the ObjectProperty. The methods getValue + * and setValue must be compatible with this type: one must be + * able to safely cast the value returned from getValue to the + * given type and pass any variable assignable to this type as an argument + * to setValue. + * + * @return type of the Property + */ + @Override + public final Class 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 Property.ReadOnlyException 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}. + * + *

    + * 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. + *

    + * + *

    + * For example + *

    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);
    +            }
    +
    +        });
    adds formatter for Double-typed property that extends + * standard "1.0" notation with more zeroes. + *

    + * + * @param T + * type of the underlying property (a PropertyFormatter is always a + * Property<String>) + * + * @deprecated Since 7.0 replaced by {@link Converter} + * @author Vaadin Ltd. + * @since 5.3.0 + */ +@SuppressWarnings("serial") +@Deprecated +public abstract class PropertyFormatter extends AbstractProperty + implements Property.Viewer, Property.ValueChangeListener, + Property.ReadOnlyStatusChangeListener { + + /** Datasource that stores the actual value. */ + Property 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 propertyDataSource) { + + setPropertyDataSource(propertyDataSource); + } + + /** + * Gets the current data source of the formatter, if any. + * + * @return the current data source as a Property, or null if + * none defined. + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the specified Property as the data source for the formatter. + * + * + *

    + * Remember that new data sources getValue() must return objects that are + * compatible with parse() and format() methods. + *

    + * + * @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 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 MapItem 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> map = new HashMap>(); + + /** + * List of all property ids to maintain the order. + */ + private LinkedList list = new LinkedList(); + + /** + * List of property set modification listeners. + */ + private LinkedList 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, null is + * returned. + * + * @param id + * the identifier of the Property to get. + * @return the Property with the given ID or null + */ + @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 + * false. + * + * @param id + * the ID of the Property to be removed. + * @return true if the operation succeeded false + * 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 true if the operation succeeded, false + * 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 String representation of the contents of the Item. + * The format of the string is a space separated catenation of the + * String representations of the Properties contained by the + * Item. + * + * @return String 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 event 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 Item + */ + @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(); + } + 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. + *

    + * The method clone performs a shallow copy of the + * PropertysetItem. + *

    + *

    + * 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. + *

    + * + * @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) list.clone() : null; + npsi.propertySetChangeListeners = propertySetChangeListeners != null ? (LinkedList) propertySetChangeListeners + .clone() : null; + npsi.map = (HashMap>) 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; + +/** + *

    + * The QueryContainer 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. + *

    + * + *

    + * The QueryContainer 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. + *

    + * + *

    + * 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()} + *

    + * + * @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 propertyIds; + + private final HashMap> propertyTypes = new HashMap>(); + + private int size = -1; + + private Statement statement; + + /** + * Constructs new QueryContainer with the specified + * queryStatement. + * + * @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 QueryContainer 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 list = new ArrayList(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); + } + + /** + *

    + * Restores items in the container. This method will update the latest data + * to the container. + *

    + * 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 statement. + * + * @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 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 c = new ArrayList(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 + * null 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; null + * 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(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 true if given id is in the container; + * false 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 null 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 null 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 true if the operation succeeded; false + * 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 true if the operation succeeded; false + * 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 true if the operation succeeded; false + * 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 true if the operation succeeded; false + * 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 null 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 null 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 true 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 true 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 Row 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 true if the operation succeeded; + * false 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 null + */ + @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 getItemPropertyIds() { + return propertyIds; + } + + /** + * Removes given item property. + * + * @param id + * ID of the Property to be removed. + * @return true if the item property is removed; + * false 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 null 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 null 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 { + + 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 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 + */ +public class TransactionalPropertyWrapper extends AbstractProperty + implements ValueChangeNotifier, Property.Transactional { + + private Property wrappedProperty; + private boolean inTransaction = false; + private boolean valueChangePending; + private T valueBeforeTransaction; + + public TransactionalPropertyWrapper(Property 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 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 + * bean type + * + * @since 6.6 + */ +public interface VaadinPropertyDescriptor 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. + *

    + * 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. + *

    + *

    + * Converters must not have any side effects (never update UI from inside a + * converter). + *

    + *

    + * All Converters must be stateless and thread safe. + *

    + *

    + * If conversion of a value fails, a {@link ConversionException} is thrown. + *

    + * + * @param + * The model type. Must be compatible with what + * {@link #getModelType()} returns. + * @param + * The presentation type. Must be compatible with what + * {@link #getPresentationType()} returns. + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +public interface Converter extends Serializable { + + /** + * Converts the given value from target type to source type. + *

    + * A converter can optionally use locale to do the conversion. + *

    + * 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. + *

    + * A converter can optionally use locale to do the conversion. + *

    + * 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 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 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 ConversionException without a detail + * message. + */ + public ConversionException() { + } + + /** + * Constructs a new ConversionException 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 ConversionException 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 Converter createConverter( + Class presentationType, Class 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 + * The presentation type + * @param + * 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 Converter getConverter( + Class presentationType, + Class modelType, Application application) { + Converter 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 + * 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 convertFromModel( + MODELTYPE modelValue, + Class presentationType, + Converter 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 + * @param + * @param presentationValue + * @param modelType + * @param converter + * @param locale + * @return + * @throws Converter.ConversionException + */ + public static MODELTYPE convertToModel( + PRESENTATIONTYPE presentationValue, Class modelType, + Converter 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 { + + /* + * (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 getModelType() { + return Long.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class 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}.

    + *

    + * Custom converters can be provided by extending this class and using + * {@link Application#setConverterFactory(ConverterFactory)}. + *

    + * + * @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 Converter createConverter( + Class presentationType, Class modelType) { + Converter converter = findConverter( + presentationType, modelType); + if (converter != null) { + log.finest(getClass().getName() + " created a " + + converter.getClass()); + return converter; + } + + // Try to find a reverse converter + Converter reverseConverter = findConverter( + modelType, presentationType); + if (reverseConverter != null) { + log.finest(getClass().getName() + " created a reverse " + + reverseConverter.getClass()); + return new ReverseConverter(reverseConverter); + } + + log.finest(getClass().getName() + " could not find a converter for " + + presentationType.getName() + " to " + modelType.getName() + + " conversion"); + return null; + + } + + protected Converter findConverter( + Class presentationType, Class modelType) { + if (presentationType == String.class) { + // TextField converters and more + Converter converter = (Converter) createStringConverter(modelType); + if (converter != null) { + return converter; + } + } else if (presentationType == Date.class) { + // DateField converters and more + Converter converter = (Converter) createDateConverter(modelType); + if (converter != null) { + return converter; + } + } + + return null; + + } + + protected Converter createDateConverter(Class sourceType) { + if (Long.class.isAssignableFrom(sourceType)) { + return new DateToLongConverter(); + } else { + return null; + } + } + + protected Converter 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 + * The source type + * @param + * The target type + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class ReverseConverter implements + Converter { + + private Converter 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 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 getModelType() { + return realConverter.getPresentationType(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getTargetType() + */ + @Override + public Class 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(). + *

    + * Leading and trailing white spaces are ignored when converting from a String. + *

    + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToBooleanConverter implements Converter { + + /* + * (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 getModelType() { + return Boolean.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class 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. + *

    + * Leading and trailing white spaces are ignored when converting from a String. + *

    + *

    + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + *

    + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToDateConverter implements Converter { + + /** + * 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 getModelType() { + return Date.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class 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. + *

    + * Leading and trailing white spaces are ignored when converting from a String. + *

    + *

    + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + *

    + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToDoubleConverter implements Converter { + + /** + * 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 getModelType() { + return Double.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class 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. + *

    + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + *

    + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToIntegerConverter implements Converter { + + /** + * 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 getModelType() { + return Integer.class; + } + + @Override + public Class 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. + *

    + * Override and overwrite {@link #getFormat(Locale)} to use a different format. + *

    + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 7.0 + */ +public class StringToNumberConverter implements Converter { + + /** + * 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 getModelType() { + return Number.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.util.converter.Converter#getPresentationType() + */ + @Override + public Class 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 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 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 value. + * + * 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 @@ + + + + + + + + +

    Provides implementations of Property, Item and Container +interfaces, and utilities for the data layer.

    + +

    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.

    + + + 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> allInstances = new ArrayList>(); + private static ReferenceQueue deadInstances = new ReferenceQueue(); + + /** + * 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(c, deadInstances)); + } + } + + /** + * Removes dead references from instance list + */ + private static void removeDeadReferences() { + java.lang.ref.Reference 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 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 extends LinkedHashMap { + private static final long serialVersionUID = 679999766473555231L; + private int cacheLimit = SQLContainer.CACHE_RATIO + * SQLContainer.DEFAULT_PAGE_LENGTH; + + @Override + protected boolean removeEldestEntry(Map.Entry 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 null value was passed to + * the setValue method, but the value of this property can not + * be set to null. + */ + @SuppressWarnings("serial") + public class NotNullableException extends RuntimeException { + + /** + * Constructs a new NotNullableException without a detail + * message. + */ + public NotNullableException() { + } + + /** + * Constructs a new NotNullableException with the specified + * detail message. + * + * @param msg + * the detail message + */ + public NotNullableException(String msg) { + super(msg); + } + + /** + * Constructs a new NotNullableException 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 properties; + + /** + * Prevent instantiation without required parameters. + */ + @SuppressWarnings("unused") + private RowItem() { + } + + public RowItem(SQLContainer container, RowId id, + Collection 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 ids = new ArrayList(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 itemIndexes = new HashMap(); + private final CacheMap cachedItems = new CacheMap(); + + /** Container properties = column names, data types and statuses */ + private final List propertyIds = new ArrayList(); + private final Map> propertyTypes = new HashMap>(); + private final Map propertyReadOnly = new HashMap(); + private final Map propertyNullable = new HashMap(); + + /** Filters (WHERE) and sorters (ORDER BY) */ + private final List filters = new ArrayList(); + private final List sorters = new ArrayList(); + + /** + * 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 itemSetChangeListeners; + + /** Temporary storage for modified items and items to be removed and added */ + private final Map removedItems = new HashMap(); + private final List addedItems = new ArrayList(); + private final List modifiedItems = new ArrayList(); + + /** List of references to other SQLContainers */ + private final Map references = new HashMap(); + + /** 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 itemProperties = new ArrayList(); + 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. + * NOTE: 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 ids = new ArrayList(); + ResultSet rs = null; + try { + // Load ALL rows :( + delegate.beginTransaction(); + rs = delegate.getResults(0, 0); + List 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 toRemove = new ArrayList(); + 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 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 itemProperties = new ArrayList(); + /* 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 propertiesToAdd = new ArrayList( + 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 getFilteredAddedItems() { + ArrayList filtered = new ArrayList(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(); + } + 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: + * + *
  • ' is replaced with ''
  • \x00 is removed
  • \ is + * replaced with \\
  • " is replaced with \"
  • + * \x1a is removed
  • + * + * 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 availableConnections; + private transient Set 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(initialConnections); + reservedConnections = new HashSet(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 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 primaryKeyColumns, + JDBCConnectionPool connectionPool) { + if (primaryKeyColumns == null) { + primaryKeyColumns = new ArrayList(); + } + 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 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 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 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 ThingTM. 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 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 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 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 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 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 Event 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 RowIdChangeEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate a RowIdChangeEvent 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 primaryKeyColumns; + private String versionColumn; + + /** Currently set Filters and OrderBys */ + private List filters; + private List 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 rowIdChangeListeners; + /** Row ID change events, stored until commit() is called */ + private final List bufferedEvents = new ArrayList(); + + /** 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 ob = new ArrayList(); + 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 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 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 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 names = new ArrayList(); + 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 values = new HashMap(); + 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 newRowId = new ArrayList(); + 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 filtersAndKeys = new ArrayList(); + 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(); + } + 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 statementHelperClass = null; + + public DefaultSQLGenerator() { + + } + + /** + * Create a new DefaultSqlGenerator instance that uses the given + * implementation of {@link StatementHelper} + * + * @param statementHelper + */ + public DefaultSQLGenerator( + Class 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 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 filters, List 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 columnToValueMap = generateColumnToValueMap(item); + Map 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 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 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 generateColumnToValueMap(RowItem item) { + Map columnToValueMap = new HashMap(); + 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 generateRowIdentifiers(RowItem item) { + Map rowIdentifiers = new HashMap(); + 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 filters, List 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 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 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 filters, List 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 filters, List 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 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 parameters = new ArrayList(); + private Map> dataTypes = new HashMap>(); + + 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> 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 filterTranslators = new ArrayList(); + 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 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 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. + *

    + * 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. + *

    + * + * @author Vaadin Ltd. + * @version @VERSION@ + * @since 5.4 + */ +@SuppressWarnings("serial") +public abstract class AbstractStringValidator extends AbstractValidator { + + /** + * Constructs a validator for strings. + * + *

    + * Null and empty string values are always accepted. To reject empty values, + * set the field being validated as required. + *

    + * + * @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 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. + *

    + * 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. + *

    + *

    + * 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)}. + *

    + *

    + * 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. + *

    + * + * @param + * The type + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.4 + */ +public abstract class AbstractValidator 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 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 exceptions = new ArrayList(); + 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("
    "); + } + 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 CompositeValidator 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 + * AND and OR. + * + * @author Vaadin Ltd. + * @version @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CompositeValidator implements Validator { + + public enum CombinationMode { + /** + * The validators are combined with AND clause: validity of + * the composite implies validity of the all validators it is composed + * of must be valid. + */ + AND, + /** + * The validators are combined with OR 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 validators = new LinkedList(); + + /** + * Construct a composite validator in AND 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. + *

    + * The value is valid, if: + *

      + *
    • MODE_AND: All of the sub-validators are valid + *
    • MODE_OR: Any of the sub-validators are valid + *
    + * + * 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. + *

    + * + * @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: + *
      + *
    • {@link CombinationMode#AND} (default) + *
    • {@link CombinationMode#OR} + *
    + * + * @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. + * + *

    + * 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 AND mode composite + * validators. + *

    + * + *

    + * If the validator is in OR mode or does not contain any + * validators of given type null is returned. + *

    + * + * @param validatorType + * The type of validators to return + * + * @return Collection of validators compatible with given type + * that must apply or null if none found. + */ + public Collection getSubValidators(Class validatorType) { + if (mode != CombinationMode.AND) { + return null; + } + + final HashSet found = new HashSet(); + for (Validator v : validators) { + if (validatorType.isAssignableFrom(v.getClass())) { + found.add(v); + } + if (v instanceof CompositeValidator + && ((CompositeValidator) v).getMode() == MODE_AND) { + final Collection 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. + * + *

    + * 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. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ +public class DateRangeValidator extends RangeValidator { + + /** + * Creates a validator for checking that an Date 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. + *

    + *

    + * 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. + *

    + * + * @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 { + + /** + * 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 { + + /** + * 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 true if nulls are allowed otherwise + * false. + */ + 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. + *

    + * 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)}. + *

    + * + * @param + * 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 extends AbstractValidator { + + private T minValue = null; + private boolean minValueIncluded = true; + private T maxValue = null; + private boolean maxValueIncluded = true; + private Class 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 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 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. + * + *

    + * For the Java regular expression syntax, see + * {@link java.util.regex.Pattern#sum} + *

    + *

    + * See {@link com.vaadin.data.validator.AbstractStringValidator} for more + * information. + *

    + * + * @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 StringLengthValidator 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 true for valid value, otherwise false. + */ + @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 true if null strings are allowed. + * + * @return true if allows null string, otherwise + * false. + */ + @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 @@ + + + + + + + + + + +

    Provides various {@link com.vaadin.data.Validator} +implementations.

    + +

    {@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.

    + + + + 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 String. + */ + 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 addAction() + * -method, which in many cases is easier than implementing the + * Action.Handler interface.
    + * + */ + 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 void addAction(T action); + + public 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: + *

    + * 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) + *

    + * + * + */ +public class ActionManager implements Action.Container, Action.Handler, + Action.Notifier { + + private static final long serialVersionUID = 1641868163608066491L; + + /** List of action handlers */ + protected HashSet ownActions = null; + + /** List of action handlers */ + protected HashSet actionHandlers = null; + + /** Action mapper */ + protected KeyMapper actionMapper = null; + + protected Component viewer; + + private boolean clientHasActions = false; + + public ActionManager() { + + } + + public ActionManager( + T viewer) { + this.viewer = viewer; + } + + private void requestRepaint() { + if (viewer != null) { + viewer.requestRepaint(); + } + } + + public 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 void addAction(T action) { + if (ownActions == null) { + ownActions = new HashSet(); + } + if (ownActions.add(action)) { + requestRepaint(); + } + } + + @Override + public 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(); + } + + 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 actions = new HashSet(); + 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(); + + 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 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 actions = new HashSet(); + 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 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; + +/** + * EventRouter 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 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(); + } + 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(); + } + 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 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 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 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 listeners = new ArrayList(); + 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 FocusEvent listeners. + * By implementing this interface a class explicitly announces that it will + * generate a FocusEvent when it receives keyboard focus. + *

    + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @since 6.2 + * @see FocusListener + * @see FocusEvent + */ + public interface FocusNotifier extends Serializable { + /** + * Adds a FocusListener to the Component which gets fired + * when a Field receives keyboard focus. + * + * @param listener + * @see FocusListener + * @since 6.2 + */ + public void addListener(FocusListener listener); + + /** + * Removes a FocusListener from the Component. + * + * @param listener + * @see FocusListener + * @since 6.2 + */ + public void removeListener(FocusListener listener); + } + + /** + * The interface for adding and removing BlurEvent listeners. + * By implementing this interface a class explicitly announces that it will + * generate a BlurEvent when it loses keyboard focus. + *

    + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @since 6.2 + * @see BlurListener + * @see BlurEvent + */ + public interface BlurNotifier extends Serializable { + /** + * Adds a BlurListener to the Component which gets fired + * when a Field loses keyboard focus. + * + * @param listener + * @see BlurListener + * @since 6.2 + */ + public void addListener(BlurListener listener); + + /** + * Removes a BlurListener from the Component. + * + * @param listener + * @see BlurListener + * @since 6.2 + */ + public void removeListener(BlurListener listener); + } + + /** + * FocusEvent class for holding additional event information. + * Fired when a Field 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); + } + } + + /** + * FocusListener interface for listening for + * FocusEvent fired by a Field. + * + * @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); + } + + /** + * BlurEvent class for holding additional event information. + * Fired when a Field 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); + } + } + + /** + * BlurListener interface for listening for + * BlurEvent fired by a Field. + * + * @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. + *

    + * 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. + *

    + * 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 ItemClickEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate an ItemClickEvent when one of its + * items is clicked. + *

    + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @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 LayoutClickEvent + * listeners. By implementing this interface a class explicitly announces + * that it will generate a LayoutClickEvent when a component + * inside it is clicked and a LayoutClickListener is + * registered. + *

    + * Note: The general Java convention is not to explicitly declare that a + * class generates events, but to directly define the + * addListener and removeListener 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. + *

    + * + * @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; + +/** + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + *

    + * 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. + *

    + * + * @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 arguments 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; + } + + /** + *

    + * 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. + *

    + * + *

    + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + *

    + * + * @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 method is not a member of target + * . + */ + 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; + } + + /** + *

    + * 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 + * object, and java.lang.IllegalArgumentException + * is thrown unless exactly one match is found. + *

    + * + * @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 + * java.lang.IllegalArgumentException 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 methodName is found in + * target. + */ + 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; + } + + /** + *

    + * 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. + *

    + * + *

    + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + *

    + * + * @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 method is not a member of target + * . + */ + 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; + } + + /** + *

    + * 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. + *

    + * + *

    + * The actual trigger method is reflected from target, and + * java.lang.IllegalArgumentException is thrown unless exactly + * one match is found. + *

    + * + * @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 + * java.lang.IllegalArgumentException is thrown. + * @param arguments + * the arguments to be passed to the trigger method. + * @throws java.lang.IllegalArgumentException + * unless exactly one match methodName is found in + * object. + */ + 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; + } + + /** + *

    + * 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. + *

    + * + *

    + * This constructor gets the trigger method as a parameter so it does not + * need to reflect to find it out. + *

    + * + * @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 method is not a member of object + * . + */ + 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"); + } + } + + /** + *

    + * 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. + *

    + * + *

    + * The actual trigger method is reflected from object, and + * java.lang.IllegalArgumentException is thrown unless exactly + * one match is found. + *

    + * + * @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 + * java.lang.IllegalArgumentException is thrown. + * @throws java.lang.IllegalArgumentException + * unless exactly one match methodName is found in + * target. + */ + 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 EventRouter 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 true if target is the same object as + * the one stored in this object and eventType 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 true if target is the same object as + * the one stored in this object, eventType equals with + * the event type stored in this object and method + * 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 + * ListenerMethod invokes the target method, it may throw + * arbitrary exception. The original exception is wrapped into + * MethodException instance and rethrown by the ListenerMethod. + * + * @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; + +/** + *

    + * Interface for classes supporting registration of methods as event receivers. + *

    + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface MethodEventSource extends Serializable { + + /** + *

    + * 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. + *

    + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @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 method has exactly one match in + * object + */ + public void addListener(Class eventType, Object object, Method method); + + /** + *

    + * 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. + *

    + * + *

    + * This version of addListener gets the name of the activation + * method as a parameter. The actual method is reflected from + * object, and unless exactly one match is found, + * java.lang.IllegalArgumentException is thrown. + *

    + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @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 method has exactly one match in + * object + */ + 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 object's methods that are + * registered to listen to events of type eventType generated + * by this component. + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @param eventType + * the exact event type the object listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType 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. + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @param eventType + * the exact event type the object 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); + + /** + *

    + * 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. + *

    + * + *

    + * This version of removeListener 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, + * java.lang.IllegalArgumentException is thrown. + *

    + * + *

    + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

    + * + * @param eventType + * the exact event type the object listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType with one or more methods. + * @param methodName + * the name of the method owned by target that's + * registered to listen to events of type eventType. + */ + 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 + * Component. + * + * 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 Component. + * + * @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. + *

    + * The ShortcutAction is triggered when the user presses a given key in + * combination with the (optional) given modifier keys. + *

    + *

    + * 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}. + *

    + *

    + * 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. + *

    + * + * @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.
    + * 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.
    + * 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. + *

    + * Insert one or more modifier characters before the character to use as + * keycode. E.g "&Save" will make a shortcut responding to + * ALT-S, "E^xit" will respond to CTRL-X.
    + * Multiple modifiers can be used, e.g "&^Delete" will respond + * to CTRL-ALT-D (the order of the modifier characters is not important). + *

    + *

    + * 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 + * "Save&&&close" will respond to ALT-C, and the caption will + * say "Save&close". + *

    + * + * @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. + *

    + * 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.
    + * E.g + * new ShortcutAction("Do &stuff", new int[]{ShortcutAction.ModifierKey.CTRL})); + * will respond to CTRL-S. + *

    + * + * @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. + *

    + * 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 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 rawVariables = new HashMap(); + private Component sourceComponent; + + public TransferableImpl(Component sourceComponent, + Map 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 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. + *

    + * 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. + *

    + * 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. + * + *

    + * 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. + *

    + * + * @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 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. + *

    + * 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. + *

    + * Based on information from {@link AcceptCriterion} components may display + * some hints for the end user whether the drop will be accepted or not. + *

    + * 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. + *

    + * 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 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}. + *

    + * 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 data = new HashMap(); + private DropTarget dropTarget; + + protected TargetDetailsImpl(Map rawDropData) { + data.putAll(rawDropData); + } + + public TargetDetailsImpl(Map 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. + *

    + * 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. + *

    + * 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}). + *

    + * Subclasses should implement the + * {@link AcceptCriterion#accept(com.vaadin.event.dd.DragAndDropEvent)} method. + *

    + * 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. + *

    + * 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 @@ + + + + + + + + + +

    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.

    + +

    Package Specification

    + +

    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.

    + +

    The power of the event inheritance arises from the possibility of +receiving not only the events of the registered type, but also the +ones which are inherited from it. For example, let's assume that there +are the events GeneralEvent and SpecializedEvent +so that the latter inherits the former. Furthermore we have an object +A which registers to receive GeneralEvent type +events from the object B. A would of course +receive all GeneralEvents generated by B, but in +addition to this, A would also receive all +SpecializedEvents generated by B. However, if +B generates some other events that do not have +GeneralEvent as an ancestor, A would not receive +them unless it registers to listen for them, too.

    + +

    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.

    + +

    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.

    + +

    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.

    + + + + + 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 get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

    + * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

    + * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

    + * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

    + * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

      + *
    • An extra , (comma) may appear just + * before the closing bracket.
    • + *
    • The null value will be inserted when there is , + *  (comma) elision.
    • + *
    • Strings may be quoted with ' (single + * quote).
    • + *
    • 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: + * { } [ ] / \ : , = ; # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
    • + *
    • Values can be separated by ; (semicolon) as + * well as by , (comma).
    • + *
    • Numbers may have the 0x- (hex) prefix.
    • + *
    + * + * @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 [ (left + * bracket) and ends with ] + *  (right bracket). + * @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 + * separator 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. + *

    + * 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 [ (left + * bracket) and ending with ] + *  (right bracket). + * @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. + *

    + * 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 get and opt methods for accessing the + * values by name, and put methods for adding or replacing values + * by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL 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 get and + * opt methods, or to convert values into a JSON text using the + * put and toString methods. A get method + * returns a value if one can be found, and throws an exception if one cannot be + * found. An opt method returns a default value instead of throwing + * an exception, and so is useful for obtaining optional values. + *

    + * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt 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. + *

    + * The put methods add or replace values in an object. For example, + * + *

    + * myString = new JSONObject().put("JSON", "Hello, World!").toString();
    + * 
    + * + * produces the string {"JSON": "Hello, World"}. + *

    + * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

      + *
    • An extra , (comma) may appear just + * before the closing brace.
    • + *
    • Strings may be quoted with ' (single + * quote).
    • + *
    • 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: + * { } [ ] / \ : , = ; # and if they do not look like numbers and + * if they are not the reserved words true, false, or + * null.
    • + *
    • Keys can be followed by = or => as well as by + * :.
    • + *
    • Values can be followed by ; (semicolon) as + * well as by , (comma).
    • + *
    • Numbers may have the 0x- (hex) prefix.
    • + *
    + * + * @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 + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + 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 "get" or + * "is" 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 "get" or "is" + * 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 "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + * + * @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 { (left + * brace) and ending with } + *  (right brace). + * @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 = '\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. + *

    + * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + @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. + *

    + * 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 { (left + * brace) and ending with } (right + * brace). + * @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. + *

    + * 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 { (left + * brace) and ending with } (right + * brace). + * @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. + * + *

    + * 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 { (left + * brace) and ending with } (right + * brace). + * @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. + *

    + * 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 { (left + * brace) and ending with } (right + * brace). + * @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. + *

    + * 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 JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), and + * JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString extends Serializable { + /** + * The toJSONString 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. + *

    + * A JSONStringer instance provides a value method for appending + * values to the text, and a key method for adding keys before + * values in objects. There are array and endArray + * methods that make and bound array values, and object and + * endObject methods which make and bound object values. All of + * these methods return the JSONWriter instance, permitting cascade style. For + * example, + * + *

    + * myString = new JSONStringer().object().key("JSON").value("Hello, World!")
    + *         .endObject().toString();
    + * 
    + * + * which produces the string + * + *
    + * {"JSON":"Hello, World!"}
    + * 
    + *

    + * The first method called must be array or object. + * 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. + *

    + * 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 null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * + * @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 " + *  (double quote) or ' + *  (single quote). + * @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. + *

    + * A JSONWriter instance provides a value method for appending + * values to the text, and a key method for adding keys before + * values in objects. There are array and endArray + * methods that make and bound array values, and object and + * endObject methods which make and bound object values. All of + * these methods return the JSONWriter instance, permitting a cascade style. For + * example, + * + *

    + * new JSONWriter(myWriter).object().key("JSON").value("Hello, World!")
    + *         .endObject();
    + * 
    + * + * which writes + * + *
    + * {"JSON":"Hello, World!"}
    + * 
    + *

    + * The first method called must be array or object. + * 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. + *

    + * 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 + * endArray will be appended to this array. The + * endArray 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 + * array. + * + * @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 + * object. + * + * @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 + * endObject will be appended to this object. The + * endObject 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 true or the value false + * . + * + * @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 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 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 getViewClass() { + return viewClass; + } + } + + private final FragmentManager fragmentManager; + private final ViewDisplay display; + private View currentView = null; + private List listeners = new LinkedList(); + private List providers = new LinkedList(); + + /** + * 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. + * + *

    + * After all {@link View}s and {@link ViewProvider}s have been registered, + * the application should trigger navigation to the current fragment using + * e.g. + * + *

    +     * navigator.navigateTo(Page.getCurrent().getFragment());
    +     * 
    + * + * @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. + * + *

    + * After all {@link View}s and {@link ViewProvider}s have been registered, + * the application should trigger navigation to the current fragment using + * e.g. + * + *

    +     * navigator.navigateTo(Page.getCurrent().getFragment());
    +     * 
    + * + * @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 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 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 @@ + + + + + + + +

    The Vaadin base package. Contains the Application class, the +starting point of any application that uses Vaadin.

    + +

    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).

    + +

    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).

    + +

    All classes in Vaadin are serializable unless otherwise noted. +This allows Vaadin applications to run in cluster and cloud +environments.

    + + + + 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 @@ + + + + + + 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; + +/** + * ApplicationContext 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 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. + *

    + * Note : The icons are associated purely to mime-types, so a file may not have + * a custom icon accessible with this class. + *

    + * + * @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 extToMIMEMap = new Hashtable(); + + /** + * MIME type to Icon mapping. + */ + static private Hashtable MIMEToIconMap = new Hashtable(); + + 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 String 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 String + */ + 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 + * MIMEType. + * @param MIMEType + * the new mime-type for extension. + */ + 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 MIMEType. + */ + 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 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 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 @@ + + + + + + + + + +

    Provides some general service classes used throughout Vaadin +based applications.

    + + + + + + + + + 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, RpcManager> rpcManagerMap = new HashMap, RpcManager>(); + + /** + * A map from server to client RPC interface class to the RPC proxy that + * sends ourgoing RPC calls for that interface. + */ + private Map, ClientRpc> rpcProxyMap = new HashMap, 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 pendingInvocations = new ArrayList(); + + private String connectorId; + + private ArrayList extensions = new ArrayList(); + + 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 void registerRpc(T implementation, Class rpcInterfaceType) { + rpcManagerMap.put(rpcInterfaceType, new ServerRpcManager( + 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 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 rpcInterfaceType) if the Rpc implementation implements more than one interface"); + } + @SuppressWarnings("unchecked") + Class type = (Class) 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. + *

    + * 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. + *

    + *

    + * 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 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 getRpcProxy(final Class 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, Serializable { + private final ClientConnector connector; + + private AllChildrenIterable(ClientConnector connector) { + this.connector = connector; + } + + @Override + public Iterator iterator() { + CombinedIterator iterator = new CombinedIterator(); + 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 retrievePendingRpcCalls() { + if (pendingInvocations.isEmpty()) { + return Collections.emptyList(); + } else { + List result = pendingInvocations; + pendingInvocations = new ArrayList(); + 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, null is returned. + * + * @return The connector's application, or null 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. null 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 null 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 implements Iterator, + Serializable { + + private final Collection> iterators = new ArrayList>(); + + public void addIterator(Iterator iterator) { + iterators.add(iterator); + } + + @Override + public boolean hasNext() { + for (Iterator i : iterators) { + if (i.hasNext()) { + return true; + } + } + return false; + } + + @Override + public T next() { + for (Iterator 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 getAllChildrenIterable( + final ClientConnector connector) { + return new AllChildrenIterable(connector); + } + + @Override + public Collection 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} + * + *

    + * The {@link #getApplication()} and {@link #getRoot()} methods might return + * null after this method is called. + *

    + */ + @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 causes = new ArrayList(); + + 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 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 = "
    "
    +                    + AbstractApplicationServlet
    +                            .safeEscapeForHtml(getMessage()) + "
    "; + 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("
    "); + sb.append(childMessage); + sb.append("
    \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. + *

    + * Extensions can use shared state and RPC in the same way as components. + *

    + * 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 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 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. + *

    + * 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 com_example_MyExtension for the + * server-side + * com.example.MyExtension extends AbstractJavaScriptExtension + * class. If MyExtension instead extends com.example.SuperExtension + * , then com_example_SuperExtension will also be attempted if + * com_example_MyExtension has not been defined. + *

    + * + * The initialization function will be called with this pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + *

      + *
    • getConnectorId() - returns a string with the id of the + * connector.
    • + *
    • getParentId([connectorId]) - returns a string with the id of + * the connector's parent. If connectorId is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.
    • + *
    • getElement([connectorId]) - returns the DOM Element that is + * the root of a connector's widget. null is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * connectorId is not provided, the connector id of the current + * connector will be used.
    • + *
    • getState() - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.
    • + *
    • registerRpc([name, ] rpcObject) - registers the + * rpcObject as a RPC handler. rpcObject should be an + * object with field containing functions for all eligible RPC functions. If + * name is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * name 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.
    • + *
    • getRpcProxy([name]) - returns an RPC proxy object. If + * name 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 + * name 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.
    • + *
    • translateVaadinUri(uri) - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}
    • + *
    + * The connector wrapper also supports these special functions: + *
      + *
    • onStateChange - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.
    • + *
    • 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.
    • + *
    • 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.
    • + *
    + *

    + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + *

      + *
    • Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.
    • + *
    • The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.
    • + *
    • Java Strings are represented by JavaScript strings.
    • + *
    • List, Set and all arrays in Java are represented by JavaScript arrays.
    • + *
    • Map in Java is represented by JavaScript object with fields + * corresponding to the map keys.
    • + *
    • 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.
    • + *
    • A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.
    • + *
    • A Java Connector is represented by a JavaScript string containing the + * connector's id.
    • + *
    • A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.
    • + *
    + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptExtension extends AbstractExtension { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected void registerRpc(T implementation, Class 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 this). 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. + *

    + * ApplicationResource 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. + *

    + * + * @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. + * + *

    + * 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 DEFAULT_CACHETIME. + *

    + * + * @return Cache time in milliseconds + */ + public long getCacheTime(); + + /** + * Gets the size of the download buffer used for this resource. + * + *

    + * If the buffer size is 0, the buffer size is decided by the terminal + * adapter. The default value is 0. + *

    + * + * @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; + +/** + * ClassResource 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. + * + *

    + * 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; + } +} 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 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 map = new HashMap(); + 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 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 errorMessages) { + super(null); + setErrorLevel(ErrorLevel.INFORMATION); + + for (final Iterator 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 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 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. null indicates that the default class + * loader should be used. + * + * @return the class loader to use, or null + */ + 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 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 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(); + } + 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 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 + * DEFAULT_CACHETIME. + * + * @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 Location, 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 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, null 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, null 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. + *

    + * An extension can only be attached once. It is not supported to move an + * extension from one target to another. + *

    + * 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; + +/** + * ExternalResource 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; + +/** + * FileResources 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 + * DownloadStream.DEFAULT_CACHETIME. + * + * @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}. + *

    + * 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 callbacks = new HashMap(); + 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> rpcInterfaces = getConnectorState() + .getRpcInterfaces(); + String interfaceName = rpcInterfaceType.getName(); + if (!rpcInterfaces.containsKey(interfaceName)) { + Set methodNames = new HashSet(); + + 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; + +/** + * KeyMapper 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 implements Serializable { + + private int lastKey = 0; + + private final HashMap objectKeyMap = new HashMap(); + + private final HashMap keyObjectMap = new HashMap(); + + /** + * 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 { + /** + * + *

    + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + *

    + * + *

    + * 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. + *

    + * + *

    + * Do not override this to paint your component. Override + * {@link #paintContent(PaintTarget)} instead. + *

    + * + * + * @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. + *

    + * 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 openList = new LinkedList(); + + /** + * 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 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. + *

    + * 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 i = openList.iterator(); i + .hasNext();) { + (i.next()).paintContent(target); + } + openList.clear(); + } + + // Paint notifications + if (notifications != null) { + target.startTag("notifications"); + for (final Iterator 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. + *

    + * 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 + * null window name is also a special case. + *

    + *

    + * "", 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. + *

    + *

    + * "_blank" as {@code windowName} causes the resource to always be opened in + * a new window or tab (depends on the browser and browser settings). + *

    + *

    + * "_top" and "_parent" as {@code windowName} works as specified by the HTML + * standard. + *

    + *

    + * 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. + *

    + * + * @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(); + } + 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 + * null + */ + 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; + +/** + * PaintExcepection 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 PaintExeception with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public PaintException(String msg) { + super(msg); + } + + /** + * Constructs an instance of PaintExeception 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 PaintExeception 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. + *

    + * 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. + *

    + *

    + * This method may also add only a reference to the paintable and queue the + * paintable to be painted separately. + *

    + *

    + * Each paintable being painted should be closed by a matching + * {@link #endPaintable(Component)} regardless of the {@link PaintStatus} + * returned. + *

    + * + * @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. + * + *
    +     * Todo:
    +     * Checking of input values
    +     * 
    + * + * @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. + *

    + * 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)}. + *

    + * Note that in current terminal implementation StreamVariables are cleaned + * from the terminal only when: + *

      + *
    • a StreamVariable with same name replaces an old one + *
    • the variable owner is no more attached + *
    • the developer signals this by calling + * {@link StreamingStartEvent#disposeStreamVariable()} + *
    + * 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. + *

    + * Prints full XML section. The section data must be XML and it is + * surrounded by XML start and end-tags. + *

    + * + * @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 false 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; + +/** + * Resource 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; + +/** + *

    + * This interface is implemented by all visual objects that can be scrolled + * programmatically from the server-side. The unit of scrolling is pixel. + *

    + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Scrollable extends Serializable { + + /** + * Gets scroll left offset. + * + *

    + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + *

    + * + * @return Horizontal scrolling position in pixels. + */ + public int getScrollLeft(); + + /** + * Sets scroll left offset. + * + *

    + * Scrolling offset is the number of pixels this scrollable has been + * scrolled right. + *

    + * + * @param scrollLeft + * the xOffset. + */ + public void setScrollLeft(int scrollLeft); + + /** + * Gets scroll top offset. + * + *

    + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + *

    + * + * @return Vertical scrolling position in pixels. + */ + public int getScrollTop(); + + /** + * Sets scroll top offset. + * + *

    + * Scrolling offset is the number of pixels this scrollable has been + * scrolled down. + *

    + * + *

    + * 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. + *

    + * + * @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 CSS + * specification 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 CSS + * specification 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; + +/** + * StreamResource 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 StreamResource. 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 StreamResource. + * StreamSource 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. + * + *

    + * 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; + } + +} 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. + *

    + * 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. + *

    + * + * @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. + *

    + * {@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. + *

    + * 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; + +/** + * SystemError 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("

    "); + sb.append(AbstractApplicationServlet + .safeEscapeForHtml(getMessage())); + sb.append("

    "); + } + 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.

    + * + * @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. + *

    + * 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. + *

    + * + * @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. + * + *

    + * 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. + *

    + * + * @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; + +/** + * ThemeResource 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 true if the given object equals this Icon, + * false 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; + +/** + * UserError 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 { + + /** + *

    + * Paints the Paintable into a UIDL stream. This method creates the UIDL + * sequence describing it and outputs it to the given UIDL stream. + *

    + * + *

    + * 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. + *

    + * + * @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; + +/** + *

    + * Listener interface for UI variable changes. The user communicates with the + * application using the so-called variables. 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. + *

    + * + * @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 variables); + + /** + *

    + * Tests if the variable owner is enabled or not. The terminal should not + * send any variable changes to disabled variable owners. + *

    + * + * @return true if the variable owner is enabled, + * false if not + */ + public boolean isEnabled(); + + /** + *

    + * 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. + *

    + * + *

    + * Note: VariableOwner 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 false + * in {@link #isImmediate()}. + *

    + * + * @return true if the component is in immediate mode, + * false 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 null 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 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 null 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 null if no path information is available. Does + * always start with / if the path isn't null. + * + * @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 null 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 null 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 null 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 null 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 OutputStream for writing binary data in the + * response. + *

    + * Either this method or getWriter() may be called to write the response, + * not both. + * + * @return a OutputStream 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 PrintWriter object that can send character text to + * the client. The PrintWriter uses the character encoding defined using + * setContentType. + *

    + * Either this method or getOutputStream() may be called to write the + * response, not both. + * + * @return a PrintWriter 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 getParameterMap() { + Map 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 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 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("dummy page"); + 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 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 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 += "

    " + 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 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 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 += "

    " + 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 += ""; + } + if (caption != null) { + output += "" + caption + "
    "; + } + if (message != null) { + output += message; + output += "

    "; + } + + if (details != null) { + output += details; + output += "

    "; + } + if (url != null) { + output += "
    "; + } + 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 CHAR_BLACKLIST = new HashSet( + 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 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 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 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 applications = context.getApplications(); + + // Search for the application (using the application URI) from the list + for (final Iterator 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: + *

      + *
    • An application runner servlet that runs different Vaadin applications + * based on an identifier.
    • + *
    • 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)
    • + * + * @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 rootToClientCache = new HashMap(); + + 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 locales; + + private int pendingLocalesIndex; + + private int timeoutInterval = -1; + + private DragAndDropService dragAndDropService; + + private String requestThemeName; + + private int maxInactiveInterval; + + private Connector highlightedConnector; + + private Map> connectorResourceContexts = new HashMap>(); + + private Map> pidToNameToStreamVariable; + + private Map 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()); + } + } + 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()); + } + } + 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("download handled"); + 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 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 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 h = new LinkedList(); + 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 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 dirtyVisibleConnectors = new ArrayList(); + 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 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 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 rpcPendingQueue = new LinkedList( + dirtyVisibleConnectors); + List 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 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> usedClientConnectors = paintTarget + .getUsedClientConnectors(); + boolean typeMappingsOpen = false; + ClientCache clientCache = getClientCache(root); + + List> newConnectorTypes = new ArrayList>(); + + for (Class 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 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) 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>() { + @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 scriptDependencies = new ArrayList(); + List styleDependencies = new ArrayList(); + + for (Class 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 dirtyVisibleConnectors) + throws PaintException { + List legacyComponents = new ArrayList(); + 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 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() { + + @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 true if the connector is visible to the client, + * false 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 implements Iterator { + + @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 collectPendingRpcCalls( + List rpcPendingQueue) { + List pendingInvocations = new ArrayList(); + for (ClientConnector connector : rpcPendingQueue) { + List paintablePendingRpc = connector + .retrievePendingRpcCalls(); + if (null != paintablePendingRpc && !paintablePendingRpc.isEmpty()) { + List oldPendingRpc = pendingInvocations; + int totalCalls = pendingInvocations.size() + + paintablePendingRpc.size(); + pendingInvocations = new ArrayList( + 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) 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 enabledConnectors = new HashSet(); + + List 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 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 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 parseInvocations( + ConnectorTracker connectorTracker, final String burst) + throws JSONException { + JSONArray invocationsJson = new JSONArray(burst); + + ArrayList invocations = new ArrayList(); + + 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 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 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 getDirtyVisibleConnectors( + ConnectorTracker connectorTracker) { + ArrayList dirtyConnectors = new ArrayList(); + 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(); + 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, Integer> typeToKey = new HashMap, Integer>(); + private int nextTypeKey = 0; + + private BootstrapHandler bootstrapHandler; + + String getTagForType(Class 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 res = new HashSet(); + + /** + * + * @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>(); + } + Map nameToStreamVariable = pidToNameToStreamVariable + .get(paintableId); + if (nameToStreamVariable == null) { + nameToStreamVariable = new HashMap(); + pidToNameToStreamVariable.put(paintableId, nameToStreamVariable); + } + nameToStreamVariable.put(name, value); + + if (streamVariableToSeckey == null) { + streamVariableToSeckey = new HashMap(); + } + 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 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 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 getAddonContextListeners() { + // Called once for init and then no more, so there's no point in caching + // the instance + ServiceLoader 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 listeners = Collections + .synchronizedList(new LinkedList()); + + protected final HashSet applications = new HashSet(); + + protected WebBrowser browser = new WebBrowser(); + + protected HashMap applicationToAjaxAppMgrMap = new HashMap(); + + 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 currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList(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 exceptions = null; + + ArrayList currentListeners; + synchronized (listeners) { + currentListeners = new ArrayList(listeners); + } + + for (TransactionListener listener : currentListeners) { + try { + listener.transactionEnd(application, request); + } catch (final RuntimeException t) { + if (exceptions == null) { + exceptions = new LinkedList(); + } + 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 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 bootstrapListeners = new ArrayList(); + + private List initedListeners = new ArrayList(); + + public AddonContext(DeploymentConfiguration deploymentConfiguration) { + this.deploymentConfiguration = deploymentConfiguration; + deploymentConfiguration.setAddonContext(this); + } + + public DeploymentConfiguration getDeploymentConfiguration() { + return deploymentConfiguration; + } + + public void init() { + AddonContextEvent event = new AddonContextEvent(this); + Iterator 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 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 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 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 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 fragmentNodes; + + public BootstrapFragmentResponse(BootstrapHandler handler, + WrappedRequest request, List fragmentNodes, + Application application, Integer rootId) { + super(handler, request, application, rootId); + this.fragmentNodes = fragmentNodes; + } + + public List 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 headers = new LinkedHashMap(); + Document document = Document.createShell(""); + BootstrapPageResponse pageResponse = new BootstrapPageResponse( + this, request, document, headers, context.getApplication(), + context.getRootId()); + List 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 headers) { + Set> entrySet = headers.entrySet(); + for (Entry 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(), 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. + *

      + * 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- + *- Additionally added from javascript: + * .v-theme- + */ + + String appClass = "v-app-" + + context.getApplication().getClass().getSimpleName(); + + String classNames = "v-app " + appClass; + List 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("//"); + 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 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 headers; + private final Document document; + + public BootstrapPageResponse(BootstrapHandler handler, + WrappedRequest request, Document document, + Map 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 variableChanges; + + public ChangeVariablesErrorEvent(Component component, Throwable throwable, + Map variableChanges) { + this.component = component; + this.throwable = throwable; + this.variableChanges = variableChanges; + } + + @Override + public Throwable getThrowable() { + return throwable; + } + + public Component getComponent() { + return component; + } + + public Map 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 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 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. + * + *

      + * 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. + *

      + *

      + * 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. + *

      + * + *

      + * It is not possible to change the parent without first setting the parent + * to {@code null}. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * The attachment logic is implemented in {@link AbstractClientConnector}. + *

      + */ + public void attach(); + + /** + * Notifies the component that it is detached from the application. + * + *

      + * 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. + *

      + */ + public void detach(); + + /** + * Get a read-only collection of all extensions attached to this connector. + * + * @return a collection of extensions + */ + public Collection 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 { + 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 validateComponentRelativeSizes( + Component component, List 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(); + } + 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 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 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 subErrors = new Vector(); + + 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 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 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 getHeightAttributes(Component component) { + Stack attributes = new Stack(); + 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 getWidthAttributes(Component component) { + Stack attributes = new Stack(); + 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 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 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 creationLocations = new HashMap(); + private static Map widthLocations = new HashMap(); + private static Map heightLocations = new HashMap(); + + 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 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 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 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 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 variables) { + Map rawDragDropDetails = (Map) 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 variables) { + return getRequestType(variables) == DragEventType.DROP; + } + + private DragEventType getRequestType(Map variables) { + int type = (Integer) variables.get("type"); + return DragEventType.values()[type]; + } + + @SuppressWarnings("unchecked") + private Transferable constructTransferable(DropTarget dropHandlerOwner, + Map variables) { + final Component sourceComponent = (Component) variables + .get("component"); + + variables = (Map) 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 retrievePendingRpcCalls() { + return null; + } + + @Override + public RpcManager getRpcManager(Class rpcInterface) { + // TODO Use rpc for drag'n'drop + return null; + } + + @Override + public Class 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 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: + * + *
      + *      <servlet>
      + *              <servlet-name>HelloWorld</servlet-name>
      + *              <servlet-class>com.vaadin.terminal.gwt.server.GAEApplicationServlet</servlet-class>
      + *              <init-param>
      + *                      <param-name>application</param-name>
      + *                      <param-value>com.vaadin.demo.HelloWorld</param-value>
      + *              </init-param>
      + *      </servlet>
      + * 
      + * + * Session support must be enabled in appengine-web.xml: + * + *
      + *      <sessions-enabled>true</sessions-enabled>
      + * 
      + * + * 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): + * + *
      + * <cronentries>
      + *   <cron>
      + *     <url>/HelloWorld/CLEAN</url>
      + *     <description>Clean up sessions</description>
      + *     <schedule>every 2 hours</schedule>
      + *   </cron>
      + * </cronentries>
      + * 
      + * + * 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: + * + *
      + *      <static-files>
      + *           <include path="/VAADIN/**" />
      + *      </static-files>
      + * 
      + * + * Additional limitations: + *
        + *
      • Do not change application state when serving an ApplicationResource. + *
      • Avoid changing application state in transaction handlers, unless you're + * confident you fully understand the synchronization issues in App Engine. + *
      • The application remains locked while uploading - no progressbar is + * possible. + *
      + */ +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 entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired Vaadin sessions."); + List keys = new ArrayList(); + 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 entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired appengine sessions."); + List keys = new ArrayList(); + 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. + *

      + * Interface can be used for several helper tasks including: + *

        + *
      • Opening and closing database connections + *
      • Implementing {@link ThreadLocal} + *
      • Setting/Getting {@link Cookie} + *
      + *

      + * 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, String> typeToTransportType = new HashMap, String>(); + + /** + * Note! This does not contain primitives. + *

      + */ + private static Map> transportTypeToType = new HashMap>(); + + 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. + *

      + * Ensures the encoded value is of the same type as target type. + *

      + *

      + * 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. + *

      + * + * @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 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(); + } + } + + 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 decodeObjectMap(Type keyType, + Type valueType, JSONArray jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map map = new HashMap(); + + 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 decodeConnectorMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map map = new HashMap(); + + 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 decodeStringMap(Type valueType, + JSONObject jsonMap, ConnectorTracker connectorTracker) + throws JSONException { + Map map = new HashMap(); + + 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 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 tokens = new ArrayList(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 decodeList(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + List list = new ArrayList(); + 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 decodeSet(Type targetType, + boolean restrictToInternalTypes, JSONArray jsonArray, + ConnectorTracker connectorTracker) throws JSONException { + HashSet set = new HashSet(); + 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 mOpenTags; + + private final Stack openJsonTags; + + // these match each other element-wise + private final Stack openPaintables; + private final Stack openPaintableTags; + + private final PrintWriter uidlBuffer; + + private boolean closed = false; + + private final AbstractCommunicationManager manager; + + private int changes = 0; + + private final Set usedResources = new HashSet(); + + private boolean customLayoutArgumentsOpen = false; + + private JsonTag tag; + + private boolean cacheEnabled = false; + + private final Set> usedClientConnectors = new HashSet>(); + + /** + * 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(); + openJsonTags = new Stack(); + + openPaintables = new Stack(); + openPaintableTags = new Stack(); + + cacheEnabled = cachingRequired; + } + + @Override + public void startTag(String tagName) throws PaintException { + startTag(tagName, false); + } + + /** + * Prints the element start tag. + * + *
      +     *   Todo:
      +     *    Checking of input values
      +     * 
      +     * 
      + * + * @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 "&"; // & => & + case '>': + return ">"; // > => > + case '<': + return "<"; // < => < + case '"': + return """; // " => " + case '\'': + return "'"; // ' => ' + 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 getUIDL 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 + * getUIDL 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 variables = new Vector(); + + Vector children = new Vector(); + + Vector attr = new Vector(); + + 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 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 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 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 getUsedResources() { + return usedResources; + } + + @Override + @SuppressWarnings("unchecked") + public String getTag(ClientConnector clientConnector) { + Class clientConnectorClass = clientConnector + .getClass(); + while (clientConnectorClass.isAnonymousClass()) { + clientConnectorClass = (Class) clientConnectorClass + .getSuperclass(); + } + Class clazz = clientConnectorClass; + while (!usedClientConnectors.contains(clazz) + && clazz.getSuperclass() != null + && ClientConnector.class.isAssignableFrom(clazz)) { + usedClientConnectors.add((Class) clazz); + clazz = clazz.getSuperclass(); + } + return manager.getTagForType(clientConnectorClass); + } + + Collection> 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 variableChanges = new HashMap(); + + 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 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> portletListeners = new HashMap>(); + + protected transient PortletSession session; + protected transient PortletConfig portletConfig; + + protected HashMap portletWindowIdToApplicationMap = new HashMap(); + + private transient PortletResponse response; + + private final Map eventActionDestinationMap = new HashMap(); + private final Map eventActionValueMap = new HashMap(); + + private final Map sharedParameterActionNameMap = new HashMap(); + private final Map sharedParameterActionValueMap = new HashMap(); + + @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 + // false + // 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 l = portletListeners.get(app); + if (l == null) { + l = new LinkedHashSet(); + portletListeners.put(app, l); + } + l.add(listener); + } + + public void removePortletListener(Application app, PortletListener listener) { + Set l = portletListeners.get(app); + if (l != null) { + l.remove(listener); + } + } + + public void firePortletRenderRequest(Application app, Root root, + RenderRequest request, RenderResponse response) { + Set 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 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 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 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. + *

      + * Only JSR 286 style Portlets are supported. + *

      + * The interface can be used for several helper tasks including: + *

        + *
      • Opening and closing database connections + *
      • Implementing {@link ThreadLocal} + *
      • Inter-portlet communication + *
      + *

      + * 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 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 implements RpcManager { + + private final T implementation; + private final Class rpcInterface; + + private static final Map, Class> boxedTypes = new HashMap, 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 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 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 invocationMethodCache = new ConcurrentHashMap( + 128, 0.75f, 1); + + private final Method method; + + private Class interfaceClass; + + public ServerRpcMethodInvocation(String connectorId, String interfaceName, + String methodName, int parameterCount) { + super(connectorId, interfaceName, methodName); + + interfaceClass = findClass(); + method = findInvocationMethod(interfaceClass, methodName, + parameterCount); + } + + private Class 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) rpcInterface; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("The server RPC interface " + + getInterfaceName() + " could not be found", e); + } finally { + + } + } + + public Class 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 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) 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 SystemMessageException with the specified + * detail message. + * + * @param msg + * the detail message. + */ + public SystemMessageException(String msg) { + super(msg); + } + + /** + * Constructs a new SystemMessageException 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 SystemMessageException 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. + * + *

      + * This handler is usually added to the application by + * {@link AbstractCommunicationManager}. + *

      + */ +@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("

      I'm sorry, but your browser is not supported

      " + + "

      The version (" + + b.getBrowserMajorVersion() + + "." + + b.getBrowserMinorVersion() + + ") of the browser you are using " + + " is outdated and not supported.

      " + + "

      You should consider upgrading to a more up-to-date browser.

      " + + "

      The most popular browsers are " + + " Chrome," + + " Firefox," + + (b.isWindows() ? " Internet Explorer," + : "") + + " Opera" + + " and Safari.
      " + + "Upgrading to the latest version of one of these will make the web safer, faster and better looking.

      " + + (b.isIE() ? "" + + "

      If you can not upgrade your browser, please consider trying Chrome Frame.

      " + : "") // + + "

      Continue without updating (not recommended)

      " + + "\n" + ""); + + 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 attrs = new HashMap(); + for (Enumeration 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. + * + *

      + * Note that Internet Explorer in IE7 compatibility mode might return 8 in + * some cases even though it should return 7. + *

      + * + * @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. + *

      + * You can use this to figure out which TimeZones the user could actually be + * in by calling {@link TimeZone#getAvailableIDs(int)}. + *

      + *

      + * 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. + *

      + * + * @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 WrappedHttpServletRequest from a + * WrappedRequest. 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 HttpServletResponse + * + * @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 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 null 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 WrappedPortlettRequest from a + * WrappedRequest. 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 @@ + + + + + + + + + + +

      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.)

      + +

      Package Specification

      + + + + + + + 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. + *

      + * 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. + *

      + * 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 componentToCoordinates = new LinkedHashMap(); + + /** + * 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 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. + *

      + * 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. + *

      + * + * @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 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 connectorToPosition = new HashMap(); + for (Iterator 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. + *

      + * Note that you cannot update the position by updating this object. Call + * {@link #setPosition(Component, ComponentPosition)} with the updated + * {@link ComponentPosition} object. + *

      + * + * @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. + * + *
      +         * setCSSString("top:10px;left:20%;z-index:16;");
      +         * 
      + * + * @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 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()); + } + List 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()); + } + List 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 String. Caption is the visible + * name of the component. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param caption + * the new caption String 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. + * + *
      +     * // Component for which the locale is meaningful
      +     * InlineDateField date = new InlineDateField("Datum");
      +     * 
      +     * // German language specified with ISO 639-1 language
      +     * // code and ISO 3166-1 alpha-2 country code.
      +     * date.setLocale(new Locale("de", "DE"));
      +     * 
      +     * date.setResolution(DateField.RESOLUTION_DAY);
      +     * layout.addComponent(date);
      +     * 
      + * + * + * @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(); + } + } + + /** + *

      + * 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: + *

      + * + *

      + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
      TagDescriptionExample
      <b>boldbold text
      <i>italicitalic text
      <u>underlinedunderlined text
      <br>linebreakN/A
      <ul>
      + * <li>item1
      + * <li>item1
      + * </ul>
      item list + *
        + *
      • item1 + *
      • item2 + *
      + *
      + *

      + * + *

      + * These tags may be nested. + *

      + * + * @return component's description String + */ + 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. + *

      + * To find the Window that contains the component, use {@code Window w = + * getParent(Window.class);} + *

      + * + * @param + * 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 findAncestor(Class 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 ErrorMessage 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. + * + *

      + * 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. + *

      + *

      + * This method is not meant to be overridden. Due to CDI requirements we + * cannot declare it as final even though it should be final. + *

      + * + * @return the parent application of the component or null. + * @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); + + /** + *

      + * 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. + *

      + * + *

      + * This method additionally informs the event-api to route events with the + * given eventIdentifier to the components handleEvent function call. + *

      + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @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 object's methods that are + * registered to listen to events of type eventType generated + * by this component. + * + *

      + * This method additionally informs the event-api to stop routing events + * with the given eventIdentifier to the components handleEvent function + * call. + *

      + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @param eventIdentifier + * the identifier of the event to stop listening for + * @param eventType + * the exact event type the object listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType 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(); + } + } + } + + /** + *

      + * 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. + *

      + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @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); + } + + /** + *

      + * 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. + *

      + * + *

      + * This version of addListener gets the name of the activation + * method as a parameter. The actual method is reflected from + * object, and unless exactly one match is found, + * java.lang.IllegalArgumentException is thrown. + *

      + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + *

      + * 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. + *

      + * + * @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 object's methods that are + * registered to listen to events of type eventType generated + * by this component. + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @param eventType + * the exact event type the object listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType 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. + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @param eventType + * the exact event type the object listens to. + * @param target + * target object that has registered to listen to events of type + * eventType with one or more methods. + * @param method + * the method owned by target that's registered to + * listen to events of type eventType. + */ + @Override + public void removeListener(Class eventType, Object target, Method method) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, method); + } + } + + /** + *

      + * 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. + *

      + * + *

      + * This version of removeListener gets the name of the + * activation method as a parameter. The actual method is reflected from + * target, and unless exactly one match is found, + * java.lang.IllegalArgumentException is thrown. + *

      + * + *

      + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + *

      + * + * @param eventType + * the exact event type the object listens to. + * @param target + * the target object that has registered to listen to events of + * type eventType with one or more methods. + * @param methodName + * the name of the method owned by target that's + * registered to listen to events of type eventType. + */ + @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 l = new LinkedList(); + + // Adds all components + for (final Iterator i = getComponentIterator(); i.hasNext();) { + l.add(i.next()); + } + + // Removes all component + for (final Iterator 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 components = new LinkedList(); + for (final Iterator i = source.getComponentIterator(); i + .hasNext();) { + components.add(i.next()); + } + + for (final Iterator 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 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 invalidChildren, + boolean childrenMayBecomeUndefined, boolean vertical) { + if (childrenMayBecomeUndefined) { + Collection previouslyInvalidComponents = invalidChildren; + invalidChildren = getInvalidSizedChildren(vertical); + if (previouslyInvalidComponents != null && invalidChildren != null) { + for (Iterator 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 stillInvalidChildren = getInvalidSizedChildren(vertical); + if (stillInvalidChildren != null) { + for (Component component : stillInvalidChildren) { + // didn't become valid + invalidChildren.remove(component); + } + } + } + if (invalidChildren != null) { + repaintChildTrees(invalidChildren); + } + } + + private Collection getInvalidSizedChildren(final boolean vertical) { + HashSet 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(1); + components.add(content); + } + } else { + for (Iterator 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(); + } + components.add(component); + } + } + } + return components; + } + + private void repaintChildTrees(Collection 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 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 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; + +/** + *

      + * 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. + * AbstractField implements that interface itself, too, so + * accessing the Property value represented by it is straightforward. + *

      + * + *

      + * 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. + *

      + * + *

      + * The class also supports {@link com.vaadin.data.Validator validators} to make + * sure the value contained in the field is valid. + *

      + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractField extends AbstractComponent implements + Field, 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 converter = null; + /** + * Connected data-source. + */ + private Property dataSource = null; + + /** + * The list of validators. + */ + private LinkedList 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 getValue and + * setValue must be compatible with this type: one must be able + * to safely cast the value returned from getValue to the given + * type and pass any variable assignable to this type as an argument to + * setValue. + * + * @return the type of the Field + */ + @Override + public abstract Class 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. + *

      + * When the field is in buffered mode, changes will not be committed to the + * property data source until {@link #commit()} is called. + *

      + *

      + * Changing buffered mode will change the read through and write through + * state for the field. + *

      + *

      + * Mixing calls to {@link #setBuffered(boolean)} and + * {@link #setReadThrough(boolean)} or {@link #setWriteThrough(boolean)} is + * generally a bad idea. + *

      + * + * @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. + *

      + * 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. + * + *

      + * This is the visible, modified and possible invalid value the user have + * entered to the field. + *

      + * + *

      + * 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. + *

      + * + *

      + * Since Vaadin 7.0, no implicit conversions between other data types and + * String are performed, but a converter is used if set. + *

      + * + * @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 null if + * none defined. + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + *

      + * Sets the specified Property as the data source for the field. All + * uncommitted changes are replaced with a value from the new data source. + *

      + * + *

      + * 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. + *

      + * + *

      + * 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}. + *

      + * + *

      + * 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(). + *

      + * + * @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 validators = ((Validatable) dataSource) + .getValidators(); + if (validators != null) { + for (final Iterator 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 c = (Converter) 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) 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. + *

      + * 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. + *

      + * + * @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(); + } + validators.add(validator); + requestRepaint(); + } + + /** + * Gets the validators of the field. + * + * @return the Unmodifiable collection that holds all validators for the + * field. + */ + @Override + public Collection 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 true if all registered validators claim that the + * current value is valid or if the field is empty and not required, + * false 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. + *

      + * 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 validationExceptions = new ArrayList(); + 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. + *

      + * 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. + *

      + * + * @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 Event 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 true if the field is required, otherwise + * false. + */ + @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 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 converter) { + this.converter = (Converter) 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 + * isListeningToPropertyEvents == true. + */ + 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 isListeningToPropertyEvents == false. + */ + 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. + *

      + * 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 com_example_MyComponent for the + * server-side + * com.example.MyComponent extends AbstractJavaScriptComponent + * class. If MyComponent instead extends com.example.SuperComponent + * , then com_example_SuperComponent will also be attempted if + * com_example_MyComponent has not been defined. + *

      + * JavaScript components have a very simple GWT widget ({@link JavaScriptWidget} + * ) just consisting of a div element to which the JavaScript code + * should initialize its own user interface. + *

      + * The initialization function will be called with this pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + *

        + *
      • getConnectorId() - returns a string with the id of the + * connector.
      • + *
      • getParentId([connectorId]) - returns a string with the id of + * the connector's parent. If connectorId is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.
      • + *
      • getElement([connectorId]) - returns the DOM Element that is + * the root of a connector's widget. null is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * connectorId is not provided, the connector id of the current + * connector will be used.
      • + *
      • getState() - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.
      • + *
      • registerRpc([name, ] rpcObject) - registers the + * rpcObject as a RPC handler. rpcObject should be an + * object with field containing functions for all eligible RPC functions. If + * name is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * name 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.
      • + *
      • getRpcProxy([name]) - returns an RPC proxy object. If + * name 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 + * name 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.
      • + *
      • translateVaadinUri(uri) - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}
      • + *
      + * The connector wrapper also supports these special functions: + *
        + *
      • onStateChange - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.
      • + *
      • 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.
      • + *
      • 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.
      • + *
      + *

      + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + *

        + *
      • Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.
      • + *
      • The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.
      • + *
      • Java Strings are represented by JavaScript strings.
      • + *
      • List, Set and all arrays in Java are represented by JavaScript arrays.
      • + *
      • Map in Java is represented by JavaScript object with fields + * corresponding to the map keys.
      • + *
      • 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.
      • + *
      • A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.
      • + *
      • A Java Connector is represented by a JavaScript string containing the + * connector's id.
      • + *
      • A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.
      • + *
      + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptComponent extends AbstractComponent { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected void registerRpc(T implementation, Class 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 this). 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 wikipedia 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 wikipedia 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 getSources() { + ArrayList sources = new ArrayList(); + 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 Mozilla Developer Network 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 components = new LinkedList(); + + /* 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 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 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(); + } + + /** + *

      + * 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. + * + *

      + * Example how to distribute 1:3 (33%) for component1 and 2:3 (67%) for + * component2 : + * + * + * layout.setExpandRatio(component1, 1);
      + * layout.setExpandRatio(component2, 2); + *
      + * + *

      + * If no ratios have been set, the excess space is distributed evenly among + * all components. + * + *

      + * 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; + +/** + *

      + * 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}. + *

      + * + *

      + * A Select component may be in single- or multiselect mode. + * Multiselect mode means that more than one item can be selected + * simultaneously. + *

      + * + * @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 implements + Container, Container.Viewer, Container.PropertySetChangeListener, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier, + Container.ItemSetChangeListener, Vaadin6Component { + + public enum ItemCaptionMode { + /** + * Item caption mode: Item's ID's String representation is + * used as caption. + */ + ID, + /** + * Item caption mode: Item's String 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 String representation is used as caption. This + * is the default. + */ + 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 setItemCaptionPropertyId. + */ + 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. + * FILTERINGMODE_OFF (0) turns the filtering off. + * FILTERINGMODE_STARTSWITH (1) matches from the start of the + * caption. FILTERINGMODE_CONTAINS (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 itemIdMapper = new KeyMapper(); + + /** + * Item icons. + */ + private final HashMap itemIcons = new HashMap(); + + /** + * Item captions. + */ + private final HashMap itemCaptions = new HashMap(); + + /** + * 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 propertySetEventListeners = null; + + /** + * List of item set change event listeners. + */ + private Set itemSetEventListeners = null; + + /** + * Item id that represents null selection of this select. + * + *

      + * 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. + *

      + */ + 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 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 acceptedSelections = new LinkedList(); + 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(visibleNotSelected); + // Don't remove those that will be added to preserve order + visibleNotSelected.removeAll(acceptedSelections); + + @SuppressWarnings("unchecked") + Set newsel = (Set) getValue(); + if (newsel == null) { + newsel = new LinkedHashSet(); + } else { + newsel = new LinkedHashSet(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. getValue and + * setValue methods must be compatible with this type: one can + * safely cast getValue to given type and pass any variable + * assignable to this type as a parameter to setValue. + * + * @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(); + } + if (retValue instanceof Set) { + return Collections.unmodifiableSet((Set) retValue); + } else if (retValue instanceof Collection) { + return new HashSet((Collection) retValue); + } else { + final Set s = new HashSet(); + if (items.containsId(retValue)) { + s.add(retValue); + } + return s; + } + + } else { + return retValue; + } + } + + /** + * Sets the visible value of the property. + * + *

      + * 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * @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(), repaintIsNotNeeded); + } else if (Collection.class.isAssignableFrom(newValue.getClass())) { + super.setValue(new LinkedHashSet( + (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 s = new HashSet(); + 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 setItemCaptionMode() 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. + * + *

      + * The mode can be one of the following ones: + *

        + *
      • ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID : Items + * Id-objects toString is used as item caption. If caption is + * explicitly specified, it overrides the id-caption. + *
      • ITEM_CAPTION_MODE_ID : Items Id-objects + * toString is used as item caption.
      • + *
      • ITEM_CAPTION_MODE_ITEM : Item-objects + * toString is used as item caption.
      • + *
      • ITEM_CAPTION_MODE_INDEX : The index of the item is used + * as item caption. The index mode can only be used with the containers + * implementing Container.Indexed interface.
      • + *
      • ITEM_CAPTION_MODE_EXPLICIT : The item captions must be + * explicitly specified.
      • + *
      • ITEM_CAPTION_MODE_PROPERTY : The item captions are read + * from property, that must be specified with + * setItemCaptionPropertyId.
      • + *
      + * The ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID is the default + * mode. + *

      + * + * @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. + * + *

      + * The mode can be one of the following ones: + *

        + *
      • ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID : Items + * Id-objects toString is used as item caption. If caption is + * explicitly specified, it overrides the id-caption. + *
      • ITEM_CAPTION_MODE_ID : Items Id-objects + * toString is used as item caption.
      • + *
      • ITEM_CAPTION_MODE_ITEM : Item-objects + * toString is used as item caption.
      • + *
      • ITEM_CAPTION_MODE_INDEX : The index of the item is used + * as item caption. The index mode can only be used with the containers + * implementing Container.Indexed interface.
      • + *
      • ITEM_CAPTION_MODE_EXPLICIT : The item captions must be + * explicitly specified.
      • + *
      • ITEM_CAPTION_MODE_PROPERTY : The item captions are read + * from property, that must be specified with + * setItemCaptionPropertyId.
      • + *
      + * The ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID is the default + * mode. + *

      + * + * @return the One of the modes listed above. + */ + public ItemCaptionMode getItemCaptionMode() { + return itemCaptionMode; + } + + /** + * Sets the item caption property. + * + *

      + * Setting the id to a existing property implicitly sets the item caption + * mode to ITEM_CAPTION_MODE_PROPERTY. If the object is in + * ITEM_CAPTION_MODE_PROPERTY mode, setting caption property id + * null resets the item caption mode to + * ITEM_CAPTION_EXPLICIT_DEFAULTS_ID. + *

      + *

      + * Note that the type of the property used for caption must be String + *

      + *

      + * Setting the property id to null disables this feature. The id is null by + * default + *

      + * . + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * Note : The icons set with setItemIcon function override the + * icons from the property. + *

      + * + *

      + * Setting the property id to null disables this feature. The id is null by + * default + *

      + * . + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * Note : The icons set with setItemIcon function override the + * icons from the property. + *

      + * + *

      + * Setting the property id to null disables this feature. The id is null by + * default + *

      + * . + * + * @return the Id of the property containing the item icons. + */ + public Object getItemIconPropertyId() { + return itemIconPropertyId; + } + + /** + * Tests if an item is selected. + * + *

      + * In single select mode testing selection status of the item identified by + * {@link #getNullSelectionItemId()} returns true if the value of the + * property is null. + *

      + * + * @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. + * + *

      + * In single select mode selecting item identified by + * {@link #getNullSelectionItemId()} sets the value of the property to null. + *

      + * + * @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 s = new HashSet((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 s = new HashSet((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(); + } + 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(); + } + 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 setNullSelectionItemId(). 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. + * + *

      + * 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * @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 captionChangeNotifiers = new HashSet(); + + 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 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 itemIds = new HashSet(); + 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 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. + *

      + * 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. + * + * AbstractSplitPanel 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, + 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 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 true if locked, false 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 true if locked, false otherwise. + */ + public boolean isLocked() { + return getSplitterState().isLocked(); + } + + /** + * SplitterClickListener interface for listening for + * SplitterClickEvent fired by a SplitPanel. + * + * @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 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 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 getType() { + return String.class; + } + + /** + * Gets the null-string representation. + * + *

      + * 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. + *

      + * + *

      + * The default value is string 'null'. + *

      + * + * @return the String Textual representation for null strings. + * @see TextField#isNullSettingAllowed() + */ + public String getNullRepresentation() { + return nullRepresentation; + } + + /** + * Is setting nulls with null-string representation allowed. + * + *

      + * 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. + *

      + * + *

      + * By default this setting is false + *

      + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * The default value is string 'null' + *

      + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * By default this setting is false. + *

      + * + * @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 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. + *

      + * 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. + *

      + * 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. + *

      + * 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. + * + *

      + * 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 <audio> 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 Mozilla Developer Network. + * + * Multiple sources can be specified. Which of the sources is used is selected + * by the browser depending on which file formats it supports. See wikipedia 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.
      + * 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 + * true if caption is rendered as HTML, + * false otherwise + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + if (getState().isHtmlContentAllowed() != htmlContentAllowed) { + getState().setHtmlContentAllowed(htmlContentAllowed); + requestRepaint(); + } + } + + /** + * Return HTML rendering setting + * + * @return true if the caption text is to be rendered as HTML, + * false 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 { + + 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 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. + * + *

      + * 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 + * parent of the contained components. + *

      + * + *

      + * 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. + *

      + * + *

      + * A component becomes attached 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * @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. + * + *
      +     * Label label = new Label("This text has a lot of style");
      +     * label.setStyleName("myonestyle myotherstyle");
      +     * 
      + * + *

      + * 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: + *

      + * + *
      +     * .myonestyle {background: blue;}
      +     * 
      + * + *

      + * or + *

      + * + *
      +     * .v-button-myonestyle {background: blue;}
      +     * 
      + * + *

      + * 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. + *

      + * + *

      + * This method will trigger a {@link RepaintRequestEvent}. + *

      + * + * @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. + * + *
      +     * Label label = new Label("This text has style");
      +     * label.addStyleName("mystyle");
      +     * 
      + * + *

      + * 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: + *

      + * + *
      +     * .mystyle {font-style: italic;}
      +     * 
      + * + *

      + * or + *

      + * + *
      +     * .v-button-mystyle {font-style: italic;}
      +     * 
      + * + *

      + * This method will trigger a {@link RepaintRequestEvent}. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * * 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. + * + *

      + * As a security feature, all updates for disabled components are blocked on + * the server-side. + *

      + * + *

      + * 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. + *

      + * + * @return true if the component and its parent are enabled, + * false 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. + * + *
      +     * Button enabled = new Button("Enabled");
      +     * enabled.setEnabled(true); // The default
      +     * layout.addComponent(enabled);
      +     * 
      +     * Button disabled = new Button("Disabled");
      +     * disabled.setEnabled(false);
      +     * layout.addComponent(disabled);
      +     * 
      + * + *

      + * This method will trigger a {@link RepaintRequestEvent} for the component + * and, if it is a {@link ComponentContainer}, for all its children + * recursively. + *

      + * + * @param enabled + * a boolean value specifying if the component should be enabled + * or not + */ + public void setEnabled(boolean enabled); + + /** + * Tests the visibility property of the component. + * + *

      + * 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. + *

      + * + *

      + * 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. + *

      + * + * @return true if the component has been set to be visible in + * the user interface, false if not + * @see #setVisible(boolean) + * @see #attach() + */ + public boolean isVisible(); + + /** + * Sets the visibility of the component. + * + *

      + * 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. + *

      + * + *
      +     * TextField readonly = new TextField("Read-Only");
      +     * readonly.setValue("You can't see this!");
      +     * readonly.setVisible(false);
      +     * layout.addComponent(readonly);
      +     * 
      + * + *

      + * 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * @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. + * + *

      + * Notice that the read-only mode only affects whether the user can change + * the value of the component; it is possible to, for example, scroll + * a read-only table. + *

      + * + *

      + * The method will return {@code true} if the component or any of its + * parents is in the read-only mode. + *

      + * + * @return true if the component or any of its parents is in + * read-only mode, false 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. + * + *

      + * 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. + *

      + * + *

      + * Notice that the read-only mode only affects whether the user can change + * the value of the component; it is possible to, for example, scroll + * a read-only table. + *

      + * + *

      + * This method will trigger a {@link RepaintRequestEvent}. + *

      + * + * @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. + * + *

      + * See {@link #setCaption(String)} for a detailed description of the + * caption. + *

      + * + * @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. + * + *

      + * A caption is an explanatory textual label accompanying a user + * interface component, usually shown above, left of, or inside the + * component. Icon (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. + *

      + * + *

      + * The caption can usually also be given as the first parameter to a + * constructor, though some components do not support it. + *

      + * + *
      +     * RichTextArea area = new RichTextArea();
      +     * area.setCaption("You can edit stuff here");
      +     * area.setValue("<h1>Helpful Heading</h1>"
      +     *         + "<p>All this is for you to edit.</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. + *

      + * + *

      + * 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. + *

      + * + *

      + * This method will trigger a {@link RepaintRequestEvent}. A + * reimplementation should call the superclass implementation. + *

      + * + * @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. + * + *

      + * See {@link #setIcon(Resource)} for a detailed description of the icon. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + *

      + * The image is loaded by the browser from a resource, typically a + * {@link com.vaadin.terminal.ThemeResource}. + *

      + * + *
      +     * // Component with an icon from a custom theme
      +     * TextField name = new TextField("Name");
      +     * name.setIcon(new ThemeResource("icons/user.png"));
      +     * layout.addComponent(name);
      +     * 
      +     * // Component with an icon from another theme ('runo')
      +     * Button ok = new Button("OK");
      +     * ok.setIcon(new ThemeResource("../runo/icons/16/ok.png"));
      +     * layout.addComponent(ok);
      +     * 
      + * + *

      + * 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. + *

      + * + *

      + * 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} . + *

      + * + * 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. + * + *

      + * If the component is not attached to a Root through a component + * containment hierarchy, null is returned. + *

      + * + * @return the Root of the component or null if it is not + * attached to a Root + */ + @Override + public Root getRoot(); + + /** + * Gets the application object to which the component is attached. + * + *

      + * The method will return {@code null} if the component is not currently + * attached to an application. + *

      + * + *

      + * 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. + *

      + * + * @return the parent application of the component or null. + * @see #attach() + */ + public Application getApplication(); + + /** + * {@inheritDoc} + * + *

      + * 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: + *

      + * + *
      +     * public class AttachExample extends CustomComponent {
      +     *     public AttachExample() {
      +     *         // ERROR: We can't access the application object yet.
      +     *         ClassResource r = new ClassResource("smiley.jpg", getApplication());
      +     *         Embedded image = new Embedded("Image:", r);
      +     *         setCompositionRoot(image);
      +     *     }
      +     * }
      +     * 
      + * + *

      + * 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)}. + *

      + *

      + * This method must call {@link Root#componentAttached(Component)} to let + * the Root know that a new Component has been attached. + *

      + * + * + *
      +     * public class AttachExample extends CustomComponent {
      +     *     public AttachExample() {
      +     *     }
      +     * 
      +     *     @Override
      +     *     public void attach() {
      +     *         super.attach(); // Must call.
      +     * 
      +     *         // Now we know who ultimately owns us.
      +     *         ClassResource r = new ClassResource("smiley.jpg", getApplication());
      +     *         Embedded image = new Embedded("Image:", r);
      +     *         setCompositionRoot(image);
      +     *     }
      +     * }
      +     * 
      + */ + @Override + public void attach(); + + /** + * Gets the locale of the component. + * + *

      + * 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 Locale.getDefault(). + *

      + * + *

      + * 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. + *

      + * + * @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. + *

      + * This method must not alter the component hierarchy in any way. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + *
      +     * Button button = new Button("Click Me!");
      +     * button.addListener(new Button.ClickListener() {
      +     *     public void buttonClick(ClickEvent event) {
      +     *         getWindow().showNotification("Thank You!");
      +     *     }
      +     * });
      +     * layout.addComponent(button);
      +     * 
      + * + *

      + * 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. + *

      + * + * @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 Component.Events. + * + *

      + * 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. + *

      + * + *

      + * 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}. + *

      + * + *
      +     * 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("Say it all here");
      +     *         name.addListener(this);
      +     *         name.setImmediate(true);
      +     *         layout.addComponent(name);
      +     * 
      +     *         // Handle button clicks as generic events instead
      +     *         // of Button.ClickEvent events
      +     *         ok = new Button("OK");
      +     *         ok.addListener(this);
      +     *         layout.addComponent(ok);
      +     * 
      +     *         // For displaying information about an event
      +     *         status = new Label("");
      +     *         layout.addComponent(status);
      +     * 
      +     *         setCompositionRoot(layout);
      +     *     }
      +     * 
      +     *     public void componentEvent(Event event) {
      +     *         // Act according to the source of the event
      +     *         if (event.getSource() == ok
      +     *                 && event.getClass() == Button.ClickEvent.class)
      +     *             getWindow().showNotification("Click!");
      +     * 
      +     *         // Display source component and event class names
      +     *         status.setValue("Event from " + event.getSource().getClass().getName()
      +     *                 + ": " + event.getClass().getName());
      +     *     }
      +     * }
      +     * 
      +     * Listening listening = new Listening();
      +     * layout.addComponent(listening);
      +     * 
      + * + * @see Component#addListener(Listener) + */ + public interface Listener extends EventListener, Serializable { + + /** + * Notifies the listener of a component event. + * + *

      + * 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. + *

      + * + *
      +         * public void componentEvent(Event event) {
      +         *     // Act according to the source of the event
      +         *     if (event.getSource() == ok && event.getClass() == Button.ClickEvent.class)
      +         *         getWindow().showNotification("Click!");
      +         * 
      +         *     // Display source component and event class names
      +         *     status.setValue("Event from " + event.getSource().getClass().getName()
      +         *             + ": " + event.getClass().getName());
      +         * }
      +         * 
      + * + * @param event + * the event that has occured. + */ + public void componentEvent(Component.Event event); + } + + /** + * Registers a new (generic) component event listener for the component. + * + *
      +     * 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("Say it all here");
      +     *         name.addListener(this);
      +     *         name.setImmediate(true);
      +     *         layout.addComponent(name);
      +     * 
      +     *         // Handle button clicks as generic events instead
      +     *         // of Button.ClickEvent events
      +     *         ok = new Button("OK");
      +     *         ok.addListener(this);
      +     *         layout.addComponent(ok);
      +     * 
      +     *         // For displaying information about an event
      +     *         status = new Label("");
      +     *         layout.addComponent(status);
      +     * 
      +     *         setCompositionRoot(layout);
      +     *     }
      +     * 
      +     *     public void componentEvent(Event event) {
      +     *         // Act according to the source of the event
      +     *         if (event.getSource() == ok)
      +     *             getWindow().showNotification("Click!");
      +     * 
      +     *         status.setValue("Event from " + event.getSource().getClass().getName()
      +     *                 + ": " + event.getClass().getName());
      +     *     }
      +     * }
      +     * 
      +     * Listening listening = new Listening();
      +     * layout.addComponent(listening);
      +     * 
      + * + * @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. + * + *

      + * 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. + *

      + */ + @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 Component.Errorss. + */ + 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}. + * + *

      + * 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. + *

      + * + * @see FieldEvents + */ + public interface Focusable extends Component { + + /** + * Sets the focus to this component. + * + *
      +         * Form loginBox = new Form();
      +         * loginBox.setCaption("Login");
      +         * layout.addComponent(loginBox);
      +         * 
      +         * // Create the first field which will be focused
      +         * TextField username = new TextField("User name");
      +         * loginBox.addField("username", username);
      +         * 
      +         * // Set focus to the user name
      +         * username.focus();
      +         * 
      +         * TextField password = new TextField("Password");
      +         * loginBox.addField("password", password);
      +         * 
      +         * Button login = new Button("Login");
      +         * loginBox.getFooter().addComponent(login);
      +         * 
      + * + *

      + * 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. + *

      + * + * @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 tabulator index of the {@code Focusable} component. + * + * @return tab index set for the {@code Focusable} component + * @see #setTabIndex(int) + */ + public int getTabIndex(); + + /** + * Sets the tabulator index 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. + * + *
      +         * Form loginBox = new Form();
      +         * loginBox.setCaption("Login");
      +         * layout.addComponent(loginBox);
      +         * 
      +         * // Create the first field which will be focused
      +         * TextField username = new TextField("User name");
      +         * loginBox.addField("username", username);
      +         * 
      +         * // Set focus to the user name
      +         * username.focus();
      +         * 
      +         * TextField password = new TextField("Password");
      +         * loginBox.addField("password", password);
      +         * 
      +         * Button login = new Button("Login");
      +         * loginBox.getFooter().addComponent(login);
      +         * 
      +         * // An additional component which natural focus order would
      +         * // be after the button.
      +         * CheckBox remember = new CheckBox("Remember me");
      +         * loginBox.getFooter().addComponent(remember);
      +         * 
      +         * username.setTabIndex(1);
      +         * password.setTabIndex(2);
      +         * remember.setTabIndex(3); // Different than natural place
      +         * login.setTabIndex(4);
      +         * 
      + * + *

      + * 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. + *

      + * + *

      + * 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. + *

      + * + *

      + * 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + * + * @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 source. + * + * @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. + *

      + * 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. + *

      + *

      + * 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}. + *

      + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public class ConnectorTracker implements Serializable { + + private final HashMap connectorIdToConnector = new HashMap(); + private Set dirtyConnectors = new HashSet(); + + 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. + *

      + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + *

      + * + * @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. + * + *

      + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + *

      + * + * @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 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. + *

      + * The state and pending RPC calls for dirty connectors are sent to the + * client in the following request. + *

      + * + * @return A collection of all dirty connectors for this root. This list may + * contain invisible connectors. + */ + public Collection 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. + *

      + * In comparison to {@link HorizontalLayout} and {@link VerticalLayout} + *

        + *
      • rather similar server side api + *
      • no spacing, alignment or expand ratios + *
      • much simpler DOM that can be styled by skilled web developer + *
      • no abstraction of browser differences (developer must ensure that the + * result works properly on each browser) + *
      • different kind of handling for relative sizes (that are set from server + * side) (*) + *
      • noticeably faster rendering time in some situations as we rely more on + * the browser's rendering engine. + *
      + *

      + * 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. + *

      + * By extending CssLayout one can also inject some css rules straight to child + * components using {@link #getCss(Component)}. + * + *

      + * (*) 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. + *

      + * 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 components = new LinkedList(); + + 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 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 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. + * + *

      + * Note that styles are injected over previous styles before actual child + * rendering. Previous styles are not cleared, but overridden. + * + *

      + * 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 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. + *

      + * 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. + *

      + * + * @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. + * + *

      + * 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. + *

      + */ + public CustomComponent() { + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructs a new custom component. + * + *

      + * 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. + *

      + * + * @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. + *

      + * The composition root must be set to non-null value before the component + * can be used. The composition root can only be set once. + *

      + * + * @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, + 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 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 + * field value type + * + * @since 7.0 + */ +public abstract class CustomField extends AbstractField implements + ComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Constructs a new custom field. + * + *

      + * 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. + *

      + */ + 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, + 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 getComponentIterator() { + return new ComponentIterator(); + } + + @Override + public Iterator 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; + +/** + *

      + * 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. + *

      + * + *

      + * 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. + *

      + * + *

      + * The default theme handles the styles that are not defined by drawing the + * subcomponents just as in OrderedLayout. + *

      + * + * @author Vaadin Ltd. + * @author Duy B. Vo (devduy@gmail.com) + * @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 slots = new HashMap(); + + /** + * 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("